Search code examples
qtqmlqtquick2kde-plasma

Line numbers/ line height for a Qml TextArea


We want to implement an embedded code editor in our QtQuick based application. For highlighting we use a QSyntaxHighlighter based on KSyntaxHighlighting. We found no way to determine the line height and line spacing that would allow us to display line numbers next to the code. Supporting dynamic line-wrap would also be a great addition.

    Flickable {
            id: flickable
            flickableDirection: Flickable.VerticalFlick
            Layout.preferredWidth: parent.width
            Layout.maximumWidth: parent.width
            Layout.minimumHeight: 200
            Layout.fillHeight: true
            Layout.fillWidth: true

            boundsBehavior: Flickable.StopAtBounds
            clip: true
            ScrollBar.vertical: ScrollBar {
                width: 15
                active: true
                policy: ScrollBar.AlwaysOn
            }

            property int rowHeight: textArea.font.pixelSize+3
            property int marginsTop: 10
            property int marginsLeft: 4
            property int lineCountWidth: 40

            Column {
                id: lineNumbers
                anchors.left: parent.left
                anchors.leftMargin: flickable.marginsLeft
                anchors.topMargin:   flickable.marginsTop
                y:  flickable.marginsTop
                width: flickable.lineCountWidth

                function range(start, end) {
                    var rangeArray = new Array(end-start);
                    for(var i = 0; i < rangeArray.length; i++){
                        rangeArray[i] = start+i;
                    }
                    return rangeArray;
                }

                Repeater {
                    model: textArea.lineCount
                    delegate:
                    Label {
                        color: (!visualization.urdfPreviewIsOK && (index+1) === visualization.urdfPreviewErrorLine) ? "white" :  "#666"
                        font: textArea.font
                        width: parent.width
                        horizontalAlignment: Text.AlignRight
                        verticalAlignment: Text.AlignVCenter
                        height: flickable.rowHeight
                        renderType: Text.NativeRendering
                        text: index+1
                        background: Rectangle {
                            color: (!visualization.urdfPreviewIsOK && (index+1) === visualization.urdfPreviewErrorLine) ? "red" : "white"
                        }
                    }
                }
            }
            Rectangle {
                y: 4
                height: parent.height
                anchors.left: parent.left
                anchors.leftMargin: flickable.lineCountWidth + flickable.marginsLeft
                width: 1
                color: "#ddd"
            }

        TextArea.flickable: TextArea {
                id: textArea

                property bool differentFromSavedState: fileManager.textDifferentFromSaved

                text: fileManager.textTmpState
                textFormat: Qt.PlainText
                //dont wrap to allow for easy line annotation wrapMode: TextArea.Wrap
                focus: false
                selectByMouse: true
                leftPadding: flickable.marginsLeft+flickable.lineCountWidth
                rightPadding: flickable.marginsLeft
                topPadding: flickable.marginsTop
                bottomPadding: flickable.marginsTop

                background: Rectangle {
                    color: "white"
                    border.color: "green"
                    border.width: 1.5
                }

                Component.onCompleted: {
                    fileManager.textEdit = textArea.textDocument
                }

                onTextChanged: {
                    fileManager.textTmpState = text
                }

                function update()
                {
                    text = fileManager.textTmpState
                }
            }
        }

As you can see we use property int rowHeight: textArea.font.pixelSize+3 to guess the line height and line spacing but that of course breaks as soon as DPI or other properties of the system change.


Solution

  • The TextArea type has two properties contentWidth and contentHeight which contains the size of the text content.

    So, if you divide the height by the number of lines (which you can get with the property lineCount), you will get the height of a line:

    property int rowHeight: textArea.contentHeight / textArea.lineCount
    

    But, if you plan to have multiple line spacing in the same document, you will have to handle each line by manipulating the QTextDocument:

    class LineManager: public QObject
    {
        Q_OBJECT
        Q_PROPERTY(int lineCount READ lineCount NOTIFY lineCountChanged)
    public:
        LineManager(): QObject(), document(nullptr)
        {}
        Q_INVOKABLE void setDocument(QQuickTextDocument* qdoc)
        {
            document = qdoc->textDocument();
            connect(document, &QTextDocument::blockCountChanged, this, &LineManager::lineCountChanged);
        }
    
        Q_INVOKABLE int lineCount() const
        {
            if (!document)
                return 0;
            return document->blockCount();
        }
    
        Q_INVOKABLE int height(int lineNumber) const
        {
            return int(document->documentLayout()->blockBoundingRect(document->findBlockByNumber(lineNumber)).height());
        }
    signals:
        void lineCountChanged();
    private:
        QTextDocument* document;
    };
    
        LineManager* mgr = new LineManager();
        QQuickView *view = new QQuickView;
        view->rootContext()->setContextProperty("lineCounter", mgr);
        view->setSource(QUrl("qrc:/main.qml"));
        view->show();
    
    Repeater {
        model: lineCounter.lineCount
        delegate:
            Label {
                color: "#666"
                font: textArea.font
                width: parent.width
                height: lineCounter.height(index)
                horizontalAlignment: Text.AlignRight
                verticalAlignment: Text.AlignVCenter
                renderType: Text.NativeRendering
                text: index+1
                background: Rectangle {
                    border.color: "black"
                }
        }
    }