0

I'm currently working on a GUI which embeds a text editor, which is a QTextEdit (not QPlainTextEdit because I need to use some rich text). This QTextEdit embeds a line number area. There are tons of content on doing this with a QPlainTextEdit, but I struggled to find anything with QTextEdit, but ended to find a reliable answer on this topic:

It's in C++, but I translated it with this version in Python:

class SyntaxHighlighter(QtGui.QSyntaxHighlighter):
    def __init__(self, parent:typing.Union[QtCore.QObject, QtGui.QTextDocument, None]=None):
        super().__init__(parent)
        self.spaceFmt = QtGui.QTextCharFormat()
        self.spaceFmt.setForeground(QtGui.QColor('orange'))
        self.expression = re.compile(r'\s+', re.U | re.S | re.M)


    def highlightBlock(self, text:str):
        for match in self.expression.finditer(text):
            start, end = match.span()
            self.setFormat(start, end - start, self.spaceFmt)


class LineNumberArea(QtWidgets.QWidget):

    def __init__(self, editor):
        super().__init__(editor)
        self.myeditor = editor


    def sizeHint(self):
        return QtCore.Qsize(self.editor.lineNumberAreaWidth(), 0)


    def paintEvent(self, event):
        self.myeditor.lineNumberAreaPaintEvent(event)


class text_editor_line(QtWidgets.QTextEdit):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.lineNumberArea = LineNumberArea(self)

        # generic document setting for hidden marks
        self.req_doc = QtGui.QTextDocument()
        option = QtGui.QTextOption()
        #option.setFlags(QtGui.QTextOption.ShowTabsAndSpaces | QtGui.QTextOption.ShowLineAndParagraphSeparators)
        self.req_doc.setDefaultTextOption(option)
        #doc.setPlainText(req_tab_widget.req_editor.toPlainText())

        # to Change color of space characters.
        highlighter = SyntaxHighlighter(self.req_doc)

        # highlight color
        self.editor_palette = self.palette()
        self.editor_palette.setColor(QtGui.QPalette().Highlight, QtGui.QColor(11, 215, 11)) # green
        self.editor_palette.setColor(QtGui.QPalette().HighlightedText, QtGui.QColor(QtCore.Qt.black))
        self.setPalette(self.editor_palette)

        self.setDocument(self.req_doc)

        self.req_doc.blockCountChanged.connect(self.updateLineNumberAreaWidth)
        self.verticalScrollBar().valueChanged.connect(self.updateLineNumberArea)
        self.textChanged.connect(self.updateLineNumberArea)
        self.cursorPositionChanged.connect(self.updateLineNumberArea)
        self.updateLineNumberAreaWidth(0)



    def keyPressEvent(self,event):
        if event.key() == QtCore.Qt.Key_Tab:
            tc = self.textCursor()
            tc.insertText("    ")
            return
        return QtWidgets.QTextEdit.keyPressEvent(self,event)



    def lineNumberAreaWidth(self):
        digits = 1
        max_value = max(1, self.req_doc.blockCount())
        while max_value >= 10:
            max_value /= 10
            digits += 1
        space = 13 + self.fontMetrics().width('9') * digits
        return space

    def updateLineNumberAreaWidth(self, _):
        self.setViewportMargins(self.lineNumberAreaWidth(), 0, 0, 0)


    def updateLineNumberArea(self):

        rect = self.contentsRect()
        self.lineNumberArea.update(0, rect.y(), self.lineNumberArea.width(), rect.height())
        self.updateLineNumberAreaWidth(0)
        dy = self.verticalScrollBar().sliderPosition()
        if dy:
            self.lineNumberArea.scroll(0, dy)

        first_block_id = self.getFirstVisibleBlockId()
        if first_block_id == 0 or self.textCursor().block().blockNumber() == first_block_id - 1:
            self.verticalScrollBar().setSliderPosition(dy - self.req_doc.documentMargin())


    def resizeEvent(self, event):
        super().resizeEvent(event)
        cr = self.contentsRect()
        self.lineNumberArea.setGeometry(QtCore.QRect(cr.left(), cr.top(), self.lineNumberAreaWidth(), cr.height()))

    # see if we do it?
    # def highlightCurrentLine(self):
    #     extraSelections = []
    #     if not self.isReadOnly():
    #         selection = QtWidgets.QTextEdit.ExtraSelection()
    #         lineColor = QtGui.QColor(QtCore.Qt.yellow).lighter(160)
    #         selection.format.setBackground(lineColor)
    #         selection.format.setProperty(QtGui.QTextFormat.FullWidthSelection, True)
    #         selection.cursor = self.textCursor()
    #         selection.cursor.clearSelection()
    #         extraSelections.append(selection)
    #     self.setExtraSelections(extraSelections)

    def getFirstVisibleBlockId(self):
        cursor = QtGui.QTextCursor(self.req_doc)
        cursor.movePosition(QtGui.QTextCursor.Start)

        for block_idx in range(self.req_doc.blockCount()):
            block = cursor.block()
            r1 = self.viewport().geometry()
            r2 = self.req_doc.documentLayout().blockBoundingRect(block).translated(r1.x(), r1.y() - self.verticalScrollBar().sliderPosition()).toRect()

            if r1.contains(r2, True):
                return block_idx

            cursor.movePosition(QtGui.QTextCursor.NextBlock)

        return 0

    def lineNumberAreaPaintEvent(self, event):

        self.verticalScrollBar().setSliderPosition(self.verticalScrollBar().sliderPosition())

        painter = QtGui.QPainter(self.lineNumberArea)

        painter.fillRect(event.rect(), QtCore.Qt.lightGray)

        blockNumber = self.getFirstVisibleBlockId()
        block = self.req_doc.findBlockByNumber(blockNumber)
        prev_block = self.req_doc.findBlockByNumber(blockNumber - 1) if blockNumber > 0 else block
        translate_y = -self.verticalScrollBar().sliderPosition() if blockNumber > 0 else 0;

        top = self.viewport().geometry().top()

        # Adjust text position according to the previous "non entirely visible" block
        # if applicable. Also takes in consideration the document's margin offset.
        if blockNumber == 0:
            # simply adjust to document's margin
            additional_margin = int(self.req_doc.documentMargin() - 1 - self.verticalScrollBar().sliderPosition())
        else:
            # getting the height of the visible part of the previous "non entirely visible" block
            additional_margin = int(self.req_doc.documentLayout().blockBoundingRect(prev_block).translated(0, translate_y).intersected(QtCore.QRectF(self.viewport().geometry())).height())

        top += additional_margin

        bottom = top + int(self.req_doc.documentLayout().blockBoundingRect(block).height())

        col_1 = QtGui.QColor(90, 255, 30) # current line
        col_0 = QtGui.QColor(120, 120, 120) # other line

        # Draw the numbers (displaying the current line number in green)
        while block.isValid() and (top <= event.rect().bottom()):
            if block.isVisible() and (bottom >= event.rect().top()):
                number = str(blockNumber + 1)
                painter.setPen(col_0)
                painter.setPen(col_1 if self.textCursor().blockNumber() == blockNumber else col_0)
                painter.drawText(-5, top, self.lineNumberArea.width(), self.fontMetrics().height(), QtCore.Qt.AlignRight, number)

            block = block.next()
            top = bottom
            bottom = top + int(self.req_doc.documentLayout().blockBoundingRect(block).height())
            blockNumber += 1

It seemed to work perfectly well, but on a 700+ lines text file from which I append all the content to editor, I ended up have the following exception

in updateLineNumberArea
    rect = self.contentsRect()

RecursionError: maximum recursion depth exceeded while calling a Python object

I'm struggling finding how to solve this issue, I'd be glad to have some help.

ThylowZ
  • 55
  • 7
  • I cannot reproduce your problem on Linux (PyQt5.7 and 5.12) even with more than 3k lines, so it *might* be a bug. What is your PyQt version and OS? – musicamante Sep 04 '20 at 10:19
  • At first, thank you for your help. I'm on Python 3.8.2, PyQt5.14.1, Windows10. Also, I realize: when I get content from my text file and append it to the text editor (through append method basically), I block signals. Edit: nope it is not a problem, I removed the blocksignals stuff and problem still exists. – ThylowZ Sep 04 '20 at 10:29
  • 1
    I only tried by using copy/paste, not appending from a file (but, theoretically, it should be the same). How do you add the text? Have you tried to you isolate which one of the 3 connections is creating the problem? I believe it *might* be the one related to the vertical scroll bar, but I'm not sure. – musicamante Sep 04 '20 at 10:35
  • Indeed, you're right, by copy pasting the content it works fine. Basically, I'm just getting the content from a text file through open(path, 'r').read(), and then i append it to the editor (let's call my editor "text_editor", I just do text_editor.append(content)). But there must be something indeed with this way of doing, because when I print the string I get from this text file, it's not as big as it should be (approximately half of the content is reetrieved) – ThylowZ Sep 04 '20 at 10:55
  • Now I have to find why my character number is limited and what causes the exception when I append the content. – ThylowZ Sep 04 '20 at 11:24
  • sorry, I'm mistaken, there is no problem with the number of characters, it's just my debugging tool which is limited in how many characters it's able to display. – ThylowZ Sep 04 '20 at 11:29
  • 1
    If the problem only occurs when appending text programmatically, edit the code to include that part so that we can see how you do it and possibly [reproduce the error](https://stackoverflow.com/help/minimal-reproducible-example). – musicamante Sep 04 '20 at 11:34
  • Thank you for your help. I would have done that, but it turns out that the problem seems to come from the "append" method. By using _setText_ instead of _append_, it made all work perfectly fine. – ThylowZ Sep 04 '20 at 13:10
  • Nonetheless this might still be a bug or worth some inspection, I suggest you to do some debugging to understand what happens when you call `append` (maybe connecting only one signal each time, as I suggested you before). – musicamante Sep 04 '20 at 13:33

0 Answers0