Search code examples
pythonqtpyqt5pyside2qtextedit

Why are my charFormat styles only working on selections, and only those made in a specific direction?


I've been trying to be more explicit in my assignment of character formats for a text editor so that I can understand what I might be able to customize with my current skill range. While the basic copy-paste versions of my format methods worked pretty well, the version below keeps working and then not working in frustrating ways and need help figuring out what might be causing it.

The editor was originally intended to be a WYSIWYG editor styled via tags for documentation. Qt's confusing use of Html hasn't made that easy.


My basic flow is to extract a copy of the current format, check its current state, invert it, and reapply the format to the position or selection it was extracted from.

# textEdit is a QTextEdit with a loaded document.

# This function is one of several related pairs called by a switchboard.
# It's intent is to invert the italic state of the current position/selection.

def toggle_italic_text(textEdit):
    # Get the cursor, and the format's state at its current selection/position.
    cursor = textEdit.textCursor()
    charFormat = cursor.charFormat()
    currentState = charFormat.fontItalic()

    # Invert the state within the format.
    print(currentState)
    charFormat.setFontItalic(not currentState)
    print(charFormat.fontItalic())

    # Reapply the format to the cursor's current selection/position.
    cursor.mergeCharFormat(charFormat)

When I first implemented it, this worked find. Now, it only works on selections, and even then it seems to identify the wrong state depending which direction I make a selection. After experimenting with it, it appears that if I make a selection to the right, it inverts correctly. If I make a selection to the left, it doesn't.

When trying to assign it to a position without a selection, the printed state changes from False to True, which is desired, yet the effect doesn't apply as I type. If I run it repeatedly in place, it continues to change from False to True, meaning the change is being lost.

The function is being called consistently and running through completely. The stored state of the charFormat copy does change.

Why has this pattern stopped working? Am I using charFormats wrong? Why does the direction of selection change the results?

As far as what changed on my end, I had been getting lost in my styling efforts after needing to apply styles through QFonts, QCharFormats, QPalette, and CSS stylesheets (and doc.defaultStylesheet) targeting both widgets and html tags. I desperately wanted my styles to be controlled through one approach, but couldn't figure out the hierarchy or find an approach that applied widely enough. In the end, I stripped out everything except for the stylesheet assigned to the window.

If there's no issue with the code itself, I'm really hoping for hints at what might be disrupting things. It took me awhile to get used to the idea that cursors and formats are copies meant to be changed and reapplied, while the document and its blocks are the real structure.


Solution

  • The important thing that must be considered about QTextCursor.charFormat() is this:

    Returns the format of the character immediately before the cursor position().

    So, not only this doesn't work very well with selections that include multiple character formats, but you also have to consider the cursor position, which might change in a selection: it could be at the beginning (so it would return the format of the character before the selection), or at the end (returning the format of the last character in the selection).

    If you want to invert the state based on the current cursor position (if at the beginning, use the first character, if at the end, use the last), then you can use the following:

        def toggle_italic_text(self):
            cursor = self.textEdit.textCursor()
            if not cursor.hasSelection():
                charFormat = cursor.charFormat()
                charFormat.setFontItalic(not charFormat.fontItalic())
                cursor.setCharFormat(charFormat)
                # in this case, the cursor has to be applied to the textEdit to ensure
                # that the following typed characters use the new format
                self.textEdit.setTextCursor(cursor)
                return
    
            start = cursor.selectionStart()
            end = cursor.selectionEnd()
            newCursor = QtGui.QTextCursor(self.textEdit.document())
            newCursor.setPosition(start)
    
            if cursor.position() == start:
                cursor.setPosition(start + 1)
            charFormat = cursor.charFormat()
            charFormat.setFontItalic(not charFormat.fontItalic())
            newCursor.setPosition(end, cursor.KeepAnchor)
            newCursor.mergeCharFormat(charFormat)
    

    If you want to invert all states in the selection, you need to cycle through all characters.
    While you could just change the char format for each character, that wouldn't be a very good thing for very large selections, so the solution is to apply the italic only when the char format actually changes from the previous state, and when at the end of the selection.

        def toggle_italic_text(self):
            # ...
            start = cursor.selectionStart()
            end = cursor.selectionEnd()
            newCursor = QtGui.QTextCursor(self.textEdit.document())
            newCursor.setPosition(start)
    
            cursor.setPosition(start)
            prevState = cursor.charFormat().fontItalic()
            while cursor.position() < end:
                cursor.movePosition(cursor.Right)
                charFormat = cursor.charFormat()
                if charFormat.fontItalic() != prevState or cursor.position() >= end:
                    newPos = cursor.position()
                    if cursor.position() < end:
                        newPos -= 1
                    newCursor.setPosition(newPos, cursor.KeepAnchor)
                    charFormat.setFontItalic(not prevState)
                    newCursor.mergeCharFormat(charFormat)
                    prevState = not prevState
                    newCursor.setPosition(cursor.position() - 1)