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.