python-3.8qtexteditpyside6pyqt6qtextcursor

Checking formatting of selected text in QTextEdit - PySide6


I' m working on a rich text editor and came across a small problem which I kind of solved with what seems like a "hack".

So I wanted to check if the selected text or the text in cursor position has any formatting, bold, italics etc and check the corresponding button that does the corresponding job but it gets weird.

if I do something like

                       if self.ed.fontItalic() is True:
                            self.italic.setChecked(True)
                        else:
                            self.italic.setChecked(False)
            
                        if self.ed.fontUnderline() is True:
                            self.uline.setChecked(True)
                        else:
                            self.uline.setChecked(False)

where "self.italic" and "self.uline" are checkable QPushButtons;

Only the format under the cursor gets checked, but not both buttons for italic and underlined, ie, if the selected text has bot formatting.

Basically, it ignores the selected text and looks for formatting of text under cursor.

So I I'm currently parsing the html to see if it has formatting;

   def check_formatting(self):

        if self.ed.textCursor().hasSelection():
            print(self.ed.textCursor().selection().toHtml())
            if "font-style:italic;" in self.ed.textCursor().selection().toHtml():
                self.italic.setChecked(True)
            else:
                self.italic.setChecked(False)

            if "text-decoration: underline;" in self.ed.textCursor().selection().toHtml():
                self.uline.setChecked(True)
            else:
                self.uline.setChecked(False)

        else:
            if self.ed.fontItalic() is True:
                self.italic.setChecked(True)
            else:
                self.italic.setChecked(False)

            if self.ed.fontUnderline() is True:
                self.uline.setChecked(True)
            else:
                self.uline.setChecked(False)

and code for formatting selected text or text under cursor on button click is as follows

    def set_italic(self):
                if self.italic.isChecked():
                    self.ed.setFontItalic(True)
                    if self.ed.textCursor().hasSelection():
                        if self.ed.fontItalic() != QTextFormat.FontItalic:
                            self.ed.setFontItalic(True)
                        else:
                            self.ed.setFontItalic(False)
                else:
                    self.ed.setFontItalic(False)

All of this works nearly as intended but it all feels super hacky. And with the html parsing, obviously, if the user enters "font-style:italic;" in the text edit and selects the entered text, button for italic gets checked.

I've been looking through a lot of stuff on the internet but none of them helps me achieve as much as the above code does.

enter image description here enter image description here


Solution

  • The default behavior is expected, from the programming point of view it's not possible to know if the developer is interested in knowing whether the selection has a format set at any point, or they want to know if it does not contain that format.

    Also, checking the html of the document is conceptually wrong, as the format could be set outside the selection (including the usage of a default stylesheet), and it's also important to remember that the toHtml() function returns a "translation" of the underlying QTextDocument, it's not the document object itself.
    The only proper way to verify the format at a specified position is by getting the QTextCharFormat of the text cursor for a given character position.

    If you really want to check if the selection contains any format change at any position, no matter the cursor position, then you need to cycle through all characters in the selection and verify their format.

    Note that in this case you need to connect to both cursorPositionChanged and selectionChanged signals, as the user could click on the editor at the cursor position, which would clear the selection but won't change the cursor position.

    With the following code, the check_formatting function will automatically toggle buttons, so that you don't need to check the current format in the button function, but you can directly connect to the text edit functions as they will toggle the format exclusively based on the state set by check_formatting, thus overriding the default behavior.

    class Editor(QtWidgets.QWidget):
        def __init__(self):
            # ...
            self.ed.cursorPositionChanged.connect(self.check_formatting)
            self.ed.selectionChanged.connect(self.check_formatting)
    
            self.bold.toggled.connect(self.toggle_bold)
            self.italic.toggled.connect(self.ed.setFontItalic)
            self.underline.toggled.connect(self.ed.setFontUnderline)
    
        def toggle_bold(self, bold):
            self.ed.setFontWeight(QtGui.QFont.Bold if bold else QtGui.QFont.Normal)
    
        def check_formatting(self):
            # assume no format by default
            bold = italic = underline = False
            cursor = self.ed.textCursor()
            if cursor.hasSelection():
                rangeEnd = cursor.selectionEnd() + 1
            else:
                rangeEnd = cursor.selectionStart() + 1
            for pos in range(cursor.selectionStart(), rangeEnd):
                cursor.setPosition(pos)
                fmt = cursor.charFormat()
                if fmt.fontWeight() >= QtGui.QFont.Bold:
                    bold = True
                if fmt.fontItalic():
                    italic = True
                if fmt.fontUnderline():
                    underline = True
                if all((bold, italic, underline)):
                    # all formats are set, no need for further checking
                    break
    
            # set check states of buttons but block their signals to avoid
            # unnecessary calls and unwanted behavior when using keyboard
            # navigation for selection (eg: shift+arrows)
            with QtCore.QSignalBlocker(self.bold):
                self.bold.setChecked(bold)
            with QtCore.QSignalBlocker(self.italic):
                self.italic.setChecked(italic)
            with QtCore.QSignalBlocker(self.underline):
                self.underline.setChecked(underline)