-1

making code editor for python, the line number counter for the code is in error it only works when the enter key is pressed if the letters are pressed past the line the new line number doesn't trigger. Also one last problem on the status bar after setting the text 'Ready', there is a white bar beside it on the right. so the statusbar ends up looking like 'Ready|'. Help would be appreciated, i'm stressing in bulk.

import sys
from PySide6.QtCore import *
from PySide6.QtGui import *
from PySide6.QtWidgets import *

class TextEditor(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("e-dit")
        self.setGeometry(850, 780, 190, 190)
        # Set the main window and tab title bar background color
        style_sheet = """
            QMainWindow {
                background-color: black;
            }
        """ 
        self.setStyleSheet(style_sheet)
        
        self.label = QLabel()
        self.label.setText('Ready')
        self.label.setStyleSheet('color: green; border: none;')
        
        # create a status bar and add the label to it
        self.statusbar = self.statusBar()
        self.statusbar.setLayoutDirection(Qt.RightToLeft)
        self.statusbar.setStyleSheet('background-color: black;')

        self.statusbar.addWidget(self.label)
        # create a text edit       
        self.txt = QTextEdit(self)
        self.txt.setStyleSheet("background-color: #000; color: green; font-family: Hack; font-size: 14px; border: none;")
        self.txt.setAlignment(Qt.AlignmentFlag.AlignRight)
        self.txt.textChanged.connect(self.update_line_numbers)

        # create the label widget for line numbers
        self.line_numbers = QLabel(self)
        self.line_numbers.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignRight)
        self.line_numbers.setStyleSheet("background-color: #000; color: green; font-family: Hack; font-size: 14px; border: none;")

        # create the main layout
        main_layout = QHBoxLayout()
        main_layout.setContentsMargins(0, 0, 0, 0)
        main_layout.addWidget(self.line_numbers)
        main_layout.addWidget(self.txt)
        
        # create the main widget
        lyn0 = QWidget()
        lyn0.setLayout(main_layout)

        # set the central widget of the main window
        self.setCentralWidget(lyn0)

    def update_line_numbers(self):
        # update the line numbers label
        cursor = self.txt.textCursor()
        block_num = cursor.blockNumber() + 1
        text = ''
        while block_num > 0:
            text = f'{block_num}\n{text}'
            block_num -= 1
        self.line_numbers.setText(text)

    def mousePressEvent(self, event):
        if event.button() == Qt.MouseButton.LeftButton:
            self._move()
            return super().mousePressEvent(event)

    def _move(self):
        window = self.window().windowHandle()
        window.startSystemMove()
        
if __name__ == '__main__':
    app = QApplication([])
    editor = TextEditor()
    editor.show()
    app.exec()

I tried many methods bouncing off of chatgpt and google but it isn't working. it's probably something small and dumb, pls dont make me feel bad. ng- ya its kulal b...14

  • For the line numbers that is the way it is supposed to be. Think about your text editor/ide you used to write this script. It likely behaves the same way. For the white line just add `QStatusBar::item {border: 0px;}` to the end of your `style_sheet` variable – Alexander May 11 '23 at 05:27
  • thanks for the statusbar tip but for the line numbers its not supposed to act that way as also noted by Hoodlum. its not registering the new line number when the characters are pressed passed the line. so if i held '...' for 3 lines, the line number would still remain as 1 even though it is 3 lines. – warsame farah May 11 '23 at 10:14
  • @warsamefarah That ***is*** the supposed behavior for basic text editors (even if they support rich text): an actual line ends with an explicit new line/paragraph character, otherwise it's just a simple line, the fact that the *pagination* could show "new lines" due to word wrapping is a **completely** different thing and is unrelated to the line count, which normally refers to the lines stored in a file: if you save a single line text, it will **always** have only one line, the fact that resizing the window will show more *wrapped* lines is completely irrelevant to the *content* of the data. – musicamante May 12 '23 at 01:19

2 Answers2

0

Small and dumb is the name of the game when it comes to Qt, especially when messing with stylesheets. We've all been there :)

Edited because I jumped the gun and did not fully understand the question before answering

Your line number issues require several things :

  1. Uninitialized line numbers : run your update_line_numbers function once at the end of your __init__, to make sure that your line_numbers widget has been filled in before is drawn for the first time.
  2. Line numbers not updating to reflect line wraps : The easiest way to fix this is to switch from a QTextEdit to a QPlainTextEdit. This lets you know about the "blocks" (i.e. code lines ending in a newline) that are visible, as well as their line number, making everything way easier. See the code below for an example.
  3. widget resize and text scrolling does not work : you did not mention it in your question, but the resizing the widget or scrolling breaks the line counter too. Again, the simplest fix is to switch to a QPlainTextEdit, as this will emit the updateRequest signal whenever something in the viewport changes (which is more general than the textChanged signal, which only triggers on a text change.

Note: While QPlainTextEdit is the officially recommended widget for code editors because it has many useful features for dealing with plain text, the downside of using it is that you loose easy access to to things like rich text formatting.

As for getting rid of the little white line in the status bar, it is just a question of updating the global stylesheet, as was noted in the comment by @Alexander.

import sys
from PySide6.QtCore import Qt
from PySide6.QtGui import *
import PySide6.QtGui
from PySide6.QtWidgets import *


class TextEditor(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("e-dit")
        # self.setGeometry(850, 780, 190, 190)
        self.setGeometry(150, 150, 190, 190)
        # Set the main window and tab title bar background color
        style_sheet = """
        QMainWindow {background-color: black;}
        QStatusBar::item {border: 0px;}
        """
        self.setStyleSheet(style_sheet)

        self.label = QLabel()
        self.label.setText('Ready')
        self.label.setStyleSheet('color: green; border: none;')

        # create a status bar and add the label to it
        self.statusbar = self.statusBar()
        self.statusbar.setLayoutDirection(Qt.LayoutDirection.RightToLeft)
        self.statusbar.setStyleSheet('background-color: black; border: none;')

        self.statusbar.addWidget(self.label)

        # create a text edit
        self.txt = QPlainTextEdit(self)
        self.txt.setStyleSheet(
            "background-color: #000; color: green; font-family: Hack; font-size: 14px; border: none;")
        # *** this signal is specifically intended for line numbers / breakpoints / etc...
        self.txt.updateRequest.connect(self.update_line_numbers)

        # create the label widget for line numbers
        self.line_numbers = QLabel(self)
        self.line_numbers.setAlignment(
            Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignRight)
        self.line_numbers.setStyleSheet(
            "background-color: #000; color: green; font-family: Hack; font-size: 14px; border: none;")
        # *** by setting a minumum height, we allow the user to resize the widget
        # *** this makes scrolling possible. 
        # *** Otherwise, the QLabel will just get bigger and bigger with each new line

        self.line_numbers.setMinimumHeight(50)

        # create the main layout
        main_layout = QHBoxLayout()
        main_layout.setContentsMargins(0, 0, 0, 0)
        main_layout.addWidget(self.line_numbers)
        main_layout.addWidget(self.txt)

        # create the main widget
        lyn0 = QWidget()
        lyn0.setLayout(main_layout)

        # set the central widget of the main window
        self.setCentralWidget(lyn0)

        # *** run the line number update once
        self.update_line_numbers()

    def update_line_numbers(self):
        text = ''

        # *** only write the line numbers from the first visible block onwards
        block = self.txt.firstVisibleBlock()

        # *** WARNING : MAGIC NUMBER BELOW
        # *** the first block has a larger offset that all the others
        # *** so we need to treat is specially
        # *** ideally, this would be done better, but I can't be bothered to try and figure out how right now
        if block.blockNumber() == 0:
            self.line_numbers.setContentsMargins(0, 4, 0, 0)
        else:
            self.line_numbers.setContentsMargins(0, 0, 0, 0)

        # *** write line numbers. unfortunately this requires a while loop
        while block.isValid() and block.isVisible():
            text += str(block.blockNumber()+1)  # actual line number
            text += block.lineCount()*'\n'  # newlines to pad
            block = block.next()

        # update the line numbers label
        self.line_numbers.setText(text)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    editor = TextEditor()
    editor.show()
    app.exec()

Some small notes :

  • Are you sure you want the Qt.AlignRight flag for the text editor ? It is definitely not a standard python editor behavior.
  • I'm not sure what your _move function is supposed to do. I'm guessing you havent finished it yet.

Anyway, seems like a cool project! Best of luck

Hoodlum
  • 950
  • 2
  • 13
  • ah, wait, I'm seeing what you meant by " if the letters are pressed past the line the new line number doesn't trigger". This will require a bit of thought. I'll get back to you if/when I find something – Hoodlum May 11 '23 at 10:07
  • yes thanks for the help, was just experimenting with style for qt.alignright. the _move function is used to move the window since it has a custom menu bar. if you can help me with the line number it would be greatly appreciated, thanks for the help thus far. – warsame farah May 11 '23 at 10:18
  • aright, I got called away for a bit, but I've had time to figure it out (more or less). I'll update the answer, as I have some slightly "radical" changes to propose – Hoodlum May 11 '23 at 15:53
0

Premise: this is more an explanation than a solution, but being aware of these aspects is extremely important.

tl;dr

Your perception of "line count" is mislead, as lines are counted based on the presence of actual "new line" objects, not just as they are displayed.

If you want to know the visual line count, you need to go through the QTextDocument API (specifically, through the QTextLayout interface).

Line count in computing is not about display

In computing, lines are not counted based on how they are shown (that's also one of the reason for which you cannot evaluate the quality of code based on the line count).

You're trying to get the visual lines, but standard editors count lines based on their characters, not on how those characters are rendered. The concept is similar to model and view: what you see is an abstraction of the actual data, and since that abstraction can have complex ways of displaying that data, counting the "lines" makes little sense in normal conditions.

Suppose that you have a table with lots of columns, each cell is so small that it can only show one character and that forces its displaying to wrap every single letter. How would you count "lines" in that case? You just can't, and it would be pointless.

The concept of line count is an old reminiscence of written text layout (notably become common with typewriters, which had fixed character sizes), but it's just a simplification that in modern terms has little meaning.

Still, if you really want the "visible" line count, you have to go through the QTextDocument API and get the visual line count from it.

How are lines actually counted

A common, standard text editor does not count visible lines, but actual ones: those that explicitly end with a new line as the user intended.

For basic text editors, that means that the actual line count is the number of line terminators, eventually including the last character that is not a new line. Note that on *nix all plain text files normally end with a line terminator (see this related post).

This is an important assumption when dealing with visual editors: they must not show the "visible" line count whenever they support visual wrapping.

Suppose that you have this content in a plain text file:

This is a short line.  
This is a very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, extremely and annoyingly long line.

You will probably see only two lines, and will need to scroll the above in order to see its contents. That's because there is only one "new line character", which is the one at the end of the first "short" line. Or, to be precise, there are actually two, as there is another at the end of the longer line.

Now, let's see the same content in a more "readable" way:

This is a short line.
This is a very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, extremely and annoyingly long line.

If you see it in a standard computer screen it will be probably shown in at least three lines, and even much more if you're using a mobile device.

In reality, the content is exactly the same, the difference is only how the visual rendering decides to wrap those lines.

That's not unlike comparing concepts or phrases and how they are printed on a book or displayed on a screen.

Does
this
seem
like
an
eight
lines
phrase?

QTextDocument and its layout interface

The QTextDocument framework is primarily intended for displaying purposes, while keeping a consistent API for upper level implementation. This means that it still exposes the "line count" concept as explained above. Each text block represents a paragraph that has at least a "line" (but it can have multiple lines in it, not only visual ones based on the wrapping mode, but also actual ones).

A QTextEdit widget internally sets a textWidth on the document whenever it's resized and based on its wrapping rules (including specific no-wrap rules for text fragments).

Since the internal QTextDocument is paired with the QTextEdit, you can get the visual line count of each block by accessing its layout().

You can have the total visual line count by iterating through each text block and get the line count from the block text layout:

class LineCountTextEdit(QTextEdit):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.textChanged.connect(self.updateLineNumbers)

    def updateLineNumbers(self):
        lineCount = 0
        block = self.document().begin()
        while block.isValid():
            lineCount += block.layout().lineCount()
            block = block.next()
        if not lineCount:
            lineCount = 1 # there is *always* at least one line!
        print('visual line count: {}'.format(lineCount))

    def resizeEvent(self, event):
        super().resizeEvent(event)
        self.updateLineNumbers()

Implementing a valid line count visualization

Even considering the above, your implementation for the line count visualization is wrong, as you did not consider the following aspects:

  • the most important one, vertical scrolling: if the text contents become taller than the widget height and the user scrolls it, the contents of the label will be inconsistent;
  • the height of each line (QTextEdit supports rich text, and a line could have fonts with a different heights);
  • the margins of the QTextEdit widget frame, and those of the document;

A proper line visualization should be embedded within the widget itself (either as a directly related subclass, or with custom internal behavior), and it must implement custom painting.

The following example shows a possible implementation that considers all aspects explained above.

class LineCountTextEdit(QTextEdit):
    lineCount = 0
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.textChanged.connect(self.updateLineNumbers)
        self.verticalScrollBar().rangeChanged.connect(self.update)
        self.verticalScrollBar().valueChanged.connect(self.update)

    def event(self, event):
        if event.type() == event.Paint:
            # we cannot override paintEvent(), because it has effect on the 
            # viewport and we need to directly paint on the widget.
            super().event(event)
            self.paintLineNumbers()
            return True
        return super().event(event)

    def paintLineNumbers(self):
        qp = QPainter(self)
        fm = self.fontMetrics()
        margins = self.contentsMargins()
        left = margins.left()
        viewMargin = self.viewportMargins().left()
        rightMargin = viewMargin - (fm.horizontalAdvance(' ') + left)

        viewTop = margins.top()
        viewHeight = self.height() - (viewTop + margins.bottom())
        qp.setClipRect(
            viewTop, margins.left(), 
            viewMargin, viewHeight
        )
        qp.translate(margins.left(), 0)

        top = self.verticalScrollBar().value()
        bottom = top + viewHeight
        offset = viewTop - self.verticalScrollBar().value()
        lineCount = 1
        doc = self.document()
        docLayout = doc.documentLayout()
        block = doc.begin()

        while block.isValid():
            blockRect = docLayout.blockBoundingRect(block)
            blockTop = blockRect.y()
            blockLayout = block.layout()
            blockLineCount = blockLayout.lineCount()
            if (
                blockRect.bottom() >= top
                and blockTop + offset <= bottom
            ):
                # only draw a text block if its bounding rect is visible
                for l in range(blockLineCount):
                    line = blockLayout.lineAt(l)
                    qp.drawText(
                        left, offset + blockTop + line.y(), 
                        rightMargin, line.height(), 
                        Qt.AlignRight, str(lineCount)
                    )
                    lineCount += 1
            else:
                lineCount += blockLineCount

            block = block.next()

    def updateLineNumbers(self):
        lineCount = 0
        block = self.document().begin()
        while block.isValid():
            lineCount += block.layout().lineCount()
            block = block.next()
        if lineCount < 1:
            lineCount = 1

        if self.lineCount != lineCount:
            self.lineCount = lineCount
            countLength = len(str(self.lineCount))
            margin = self.fontMetrics().horizontalAdvance('  ' + '0' * countLength)
            if self.viewportMargins().left() != margin:
                self.setViewportMargins(margin, 0, 0, 0)

        self.update()

    def resizeEvent(self, event):
        super().resizeEvent(event)
        self.updateLineNumbers()


class TextEditor(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("e-dit")
        
        self.label = QLabel()
        self.label.setText('Ready')
        
        self.statusBar().addPermanentWidget(self.label)

        self.txt = LineCountTextEdit(self)
        self.txt.setAlignment(Qt.AlignRight)

        self.setCentralWidget(self.txt)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    editor = TextEditor()
    editor.show()
    sys.exit(app.exec())

Note that I ignored the QSS aspect of your question: you shall only ask one question per post, unless they are strictly related. Besides, you should never set generic properties on a parent widget unless you know what you're doing and how style sheet properties propagate on children. Rmove the top level setStyleSheet and see the difference in the scroll bar as soon as it's shown.

musicamante
  • 41,230
  • 6
  • 33
  • 58