QTextCursor doesn't return current position

294 views Asked by At

I am working on an app with a text editor based on QTextEdit. I try to achieve a behavior which is similar to Word: When you move the text cursor to any position in the text the QTextCharFormat of the QTextCursor is changed to the CharFormat of the character right before the cursor, and the Buttons for Bold/Italic/Underlined, the QFontComboBox and the QComboBox for the point size of the editor-widget are checked and set accrodingly.

Originally I connected the signal cursorPositionChanged() to the method lastCharFormat which handles the character format of the character before the cursor by calling charFormat(). But this signal turned out to be unsuitable because it emits every time I type in a character. So I used keyPressEvent() to catch inputs of the arrow keys and emit a custom signal.

I haven't implemented the text cursor position change by mouse because I noticed a problem with the text cursor position when moving the cursor with the arrow keys. When I move the cursor from the end of the document to the start the cursor return 1 but when I move the cursor one position to the right it returns 0.

Edit: Found out that position() does not return the current position but the position before.

This is a problem because I assumed the start position of the text editor to be 1 and I had to implement a check because returning the properties of the QTextCharFormat at the start position crashes the app and also if there's no text. Apparently this is due to getting stuck in a loop. Anyway the main problem is the strange behavior of the text cursor position.

Why does QTextCursor behaves this way? Is it a bug? And how can I get the properties of QTextCharFormat of the character right before the text cursor when the user moves the cursor with the arrow keys or the mouse without crashing the app?

Here is part of the code of the custom text editor element that inherits from QTextEdit.

class TextElement(QTextEdit):
    cursorMoved = pyqtSignal()

    def __init__(self, parent=None) -> None:
        super().__init__(parent)
        self.editor = parent.editor
        self.connectSignals()

    def connectSignals(self) -> None:
        self.cursorMoved.connect(self.lastCharFormat)

    def keyPressEvent(self, e: QKeyEvent) -> None:        
        # Cursor Changed by Key
        if e.key() == Qt.Key.Key_Up or e.key() == Qt.Key.Key_Left or e.key() == Qt.Key.Key_Down or    e.key() == Qt.Key.Key_Right:
            self.cursorMoved.emit()

        return super().keyPressEvent(e)

    def lastCharFormat(self) -> None:
        cursor = self.textCursor()
        # Printing text cursor position to test behavior
        print(cursor.position())
        if cursor.position() > 1:
            tformat = cursor.charFormat()

            # From this point the app crashes if position is the start position (0 or 1)
            family = tformat.fontFamily()
            size = tformat.fontPointSize()
            if tformat.fontWeight() == 700:
                bold = True
            else:
                bold = False
            italic = tformat.fontItalic()
            underlined = tformat.fontUnderline()
            print(f"Char format before cursor: FontFamily: {family}, Size: {size}, Bold: {bold}, Italic: {italic}, Underlined: {underlined}")

            # This function sets the buttons, QFontComboBox and QComboBox with the point sizes of the editor accordingly
            self.editor.setFontProps(family, size, bold, italic, underlined)


I tried to set the text cursor position in the keyPressEvent but this didn't correct the text cursor behavior.

2

There are 2 answers

1
user1928747 On

QTextCursor's position is a 0-based index, meaning the first position is 0, not 1. In your code, when you check if the position is greater than 1, you are checking if it's the second position or later. This causes the code to crash when the cursor is at position 0, because you are trying to access the character format of a non-existing character.

To fix the issue, you should change the condition in the lastCharFormat method to check if the position is greater than or equal to 0 instead of greater than 1. This will allow the code to access the character format of the character right before the cursor even when the cursor is at the start position.

def lastCharFormat(self) -> None:
cursor = self.textCursor()
print(cursor.position())
if cursor.position() >= 0:
    tformat = cursor.charFormat()
    ...

Also, in the keyPressEvent method, you are checking for arrow key presses to emit the cursorMoved signal. This is a good approach, but you should also consider emitting the signal for other types of cursor movement, such as clicking the mouse to move the cursor or using the mouse wheel to scroll. You can do this by connecting to the cursorPositionChanged signal and emitting the cursorMoved signal in the slot connected to cursorPositionChanged.

class TextElement(QTextEdit):
cursorMoved = pyqtSignal()

def __init__(self, parent=None) -> None:
    super().__init__(parent)
    self.editor = parent.editor
    self.connectSignals()

def connectSignals(self) -> None:
    self.cursorPositionChanged.connect(self.onCursorPositionChanged)

def onCursorPositionChanged(self):
    self.cursorMoved.emit()

def keyPressEvent(self, e: QKeyEvent) -> None:
    return super().keyPressEvent(e)

def lastCharFormat(self) -> None:
    cursor = self.textCursor()
    if cursor.position() >= 0:
        tformat = cursor.charFormat()
        ...

This way, the lastCharFormat method will be called whenever the cursor position changes, whether it's due to a key press, mouse click, or mouse wheel scroll.

1
Deator On

Ok thanks to musicamante and user1928747 I found the problem.

First I didn't realize that I get the cursor position before the key inputs are passed to the QTextCursor. So I used the native signal cursorPositionChanged again and it now works without any crashes because I get the current text format properties from the QTextEdit itself. I didn't know that both getters of QTextEdit and QTextCursor had basically the same function.

I edited the code so the text editor doesn't lose the focus when the user sets the text character format with the buttons from the editor.

The lastCharFormat function is a little bit messy but it was difficult to handle the text character format at index 0.

Here is the complete edited code of the "TextElement" widget:

class TextElement(QTextEdit, BaseElement):
    cursorMoved = pyqtSignal()
    focussed = pyqtSignal(QTextEdit)

    def __init__(self, row, parent=None) -> None:
        super().__init__(parent)
        self.editor = parent.editor
        self.block = parent
        self.row = row
        self.lastChar: str
        self.lastFormat: dict

        self.setStyleSheet("background-color: white;")
        self.defaultCursor()
        self.typeLang = get("General", "Language")
        self.connectSignals()

    def connectSignals(self) -> None:
        self.cursorPositionChanged.connect(self.onCursorPositionChanged)
        self.cursorMoved.connect(self.lastCharFormat)

        self.editor.colormenu.colorChanged.connect(self.setTColor)
        self.editor.fontChanged.connect(self.setTFont)

    # Initial cursor settings
    def defaultCursor(self) -> None:
        self.lastFormat = self.editor.getFontProps()
        self.setTextColor(QColor().fromString("#000000"))

    # Gets signal from the editor when the font properties are changed by the user
    @pyqtSlot(dict)
    def setTFont(self, props: dict) -> None:
        if self.row.focussedElement == self:
            self.setFocus()
            self.lastFormat = props
            charformat = QTextCharFormat()
            charformat.setFontFamily(props["FontFamily"].family())
            charformat.setFontPointSize(props["FontSize"])
            if props["Bold"]:
                charformat.setFontWeight(QFont.Weight.Bold)
            charformat.setFontItalic(props["Italic"])
            charformat.setFontUnderline(props["Underlined"])
            self.setCurrentCharFormat(charformat)
            self.setTextColor(props["Color"])

    def mousePressEvent(self, e: QMouseEvent) -> None:
        # Because there can be multiple QTextEdits this signal tells the parent which one is used
        self.focussed.emit(self)
        return super().mousePressEvent(e)

    # Created another function for future purposes
    def onCursorPositionChanged(self):
        self.cursorMoved.emit()

    def lastCharFormat(self) -> None:
        cursor = self.textCursor()
        print(cursor.position())
        if self.fontWeight() == 700: 
            bold = True
        else:
            bold = False
        italic = self.fontItalic()
        underlined = self.fontUnderline()
        family = self.hasFormat(self.fontFamily(), self.lastFormat["FontFamily"].family())
        if self.fontPointSize() > 0:
            if self.fontPointSize() % 1 == 0:
                size = int(self.fontPointSize())
            else:
                size = self.fontPointSize()
        else:
            if self.fontPointSize() % 1 == 0:
                size = int(self.lastFormat["FontSize"])
            else:
                size = self.lastFormat["FontSize"]
        
        print("Text color: ", self.textColor().name())
        print(f"Char format before cursor: FontFamily: {family}, Size: {size}, Bold: {bold}, Italic: {italic}, Underlined: {underlined} Color: {self.textColor()}")
        self.editor.setFontProps(family, str(size), bold, italic, underlined, self.textColor())

    def hasFormat(self, fmt, default):
        if fmt:
            return fmt
        else: 
            return default