From a5563949e5d3e3130fdd12d7314633f3d48c49f1 Mon Sep 17 00:00:00 2001 From: eelke Date: Sun, 10 Apr 2022 10:29:40 +0200 Subject: [PATCH] Refactor painting of editor gutter --- pglab/codeeditor/CodeEditor.cpp | 148 +++++++++++++---------------- pglab/codeeditor/CodeEditor.h | 10 +- pglab/codeeditor/EditorGutter.cpp | 4 +- pglab/codeeditor/GutterPainter.cpp | 69 ++++++++++++++ pglab/codeeditor/GutterPainter.h | 38 ++++++++ pglab/crud/CrudTab.cpp | 2 +- pglab/pglab.pro | 2 + 7 files changed, 188 insertions(+), 85 deletions(-) create mode 100644 pglab/codeeditor/GutterPainter.cpp create mode 100644 pglab/codeeditor/GutterPainter.h diff --git a/pglab/codeeditor/CodeEditor.cpp b/pglab/codeeditor/CodeEditor.cpp index 844ec05..ac75455 100644 --- a/pglab/codeeditor/CodeEditor.cpp +++ b/pglab/codeeditor/CodeEditor.cpp @@ -86,8 +86,8 @@ void CodeEditor::onTextChanged() void CodeEditor::addErrorMarker(int position, int length) { - QTextEdit::ExtraSelection selection; - QColor lineColor = QColor(Qt::red).lighter(160); + QTextEdit::ExtraSelection selection; + QColor lineColor = QColor(Qt::red).lighter(160); selection.format.setBackground(lineColor); selection.format.setFontItalic(true); selection.cursor = textCursor(); @@ -120,45 +120,17 @@ void CodeEditor::updateExtraSelections() setExtraSelections(extraSelections); } -void CodeEditor::gutterAreaPaintEvent(QPaintEvent *event) +void CodeEditor::drawGutterErrorMarker(QPainter &painter, int top) { - QPainter painter(gutterArea); - painter.fillRect(event->rect(), Qt::lightGray); + int s = fontMetrics().height() - 8; + painter.setBrush(QBrush(Qt::red)); + painter.drawEllipse(4, top + 4, s, s); - QTextBlock block = firstVisibleBlock(); - int blockNumber = block.blockNumber(); - int top = (int) blockBoundingGeometry(block).translated(contentOffset()).top(); - int bottom = top + (int) blockBoundingRect(block).height(); +} - // We will now loop through all visible lines and paint the line numbers in the - // extra area for each line. Notice that in a plain text edit each line will - // consist of one QTextBlock; though, if line wrapping is enabled, a line may span - // several rows in the text edit's viewport. - // - // We get the top and bottom y-coordinate of the first text block, and adjust these - // values by the height of the current text block in each iteration in the loop. - while (block.isValid() && top <= event->rect().bottom()) - { - if (block.isVisible() && bottom >= event->rect().top()) - { - QString number = QString::number(blockNumber + 1); - painter.setPen(Qt::black); - painter.drawText(0, top, gutterArea->width(), fontMetrics().height(), - Qt::AlignRight, number); - - if (errorLines.count(blockNumber) > 0) - { - int s = fontMetrics().height() - 8; - painter.setBrush(QBrush(Qt::red)); - painter.drawEllipse(4, top + 4, s, s); - } - } - - block = block.next(); - top = bottom; - bottom = top + (int) blockBoundingRect(block).height(); - ++blockNumber; - } +bool CodeEditor::lineHasError(int blockNumber) const +{ + return errorLines.count(blockNumber) > 0; } void CodeEditor::keyPressEvent(QKeyEvent *e) @@ -194,6 +166,8 @@ void CodeEditor::keyPressEvent(QKeyEvent *e) QPlainTextEdit::keyPressEvent(e); } + + bool CodeEditor::indentSelection(bool indent) { auto cursor = textCursor(); @@ -222,63 +196,75 @@ bool CodeEditor::indentSelection(bool indent) { cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor); if (indent) - { - if (m_useTab) - cursor.insertText("\t"); - else - cursor.insertText(QString(m_tabSize, ' ')); - } + insertIndentation(cursor); else - { - // remove tab if there is a tab - cursor.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor); - const QString text = cursor.selectedText(); - int index = 0; - int pos = 0; - while (pos < m_tabSize && index < text.length()) - { - QChar c = text[index++]; - if (c == ' ') - ++pos; - else if (c == '\t') - pos = ((pos + m_tabSize) / m_tabSize) * m_tabSize; - else - { - --index; - break; - } - } - cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor); - for (int i = 0; i < index; ++i) - cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor); - cursor.removeSelectedText(); - } + removeIndentation(cursor); cursor.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor); } cursor.endEditBlock(); - cursor.setPosition(first_pos, QTextCursor::MoveAnchor); - cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor); - while(cursor.block().blockNumber() < end_block) - cursor.movePosition(QTextCursor::NextBlock, QTextCursor::KeepAnchor); - - cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); + makeSelection(cursor, first_pos, end_block); setTextCursor(cursor); return true; } +void CodeEditor::insertIndentation(QTextCursor &cursor) +{ + if (m_useTab) + cursor.insertText("\t"); + else + cursor.insertText(QString(m_tabSize, ' ')); +} + +void CodeEditor::removeIndentation(QTextCursor &cursor) +{ + // remove tab if there is a tab + cursor.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor); + const QString text = cursor.selectedText(); + int index = 0; + int pos = 0; + while (pos < m_tabSize && index < text.length()) + { + QChar c = text[index++]; + if (c == ' ') + ++pos; + else if (c == '\t') + pos = ((pos + m_tabSize) / m_tabSize) * m_tabSize; + else + { + --index; + break; + } + } + cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor); + for (int i = 0; i < index; ++i) + cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor); + cursor.removeSelectedText(); + +} + +void CodeEditor::makeSelection(QTextCursor &cursor, int first_pos, int end_block) +{ + cursor.setPosition(first_pos, QTextCursor::MoveAnchor); + cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor); + while(cursor.block().blockNumber() < end_block) + cursor.movePosition(QTextCursor::NextBlock, QTextCursor::KeepAnchor); + + cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); +} + void CodeEditor::setFont(const QFont &f) { - QWidget::setFont(f); - auto orig_tab = m_tabSize; - m_tabSize = 0; - setTabSize(orig_tab); + QWidget::setFont(f); + auto orig_tab = m_tabSize; + m_tabSize = 0; + setTabSize(orig_tab); } void CodeEditor::setTabSize(int chars) { - m_tabSize = chars; - int pixels = fontMetrics().horizontalAdvance(QString(chars, '0')); - this->setTabStopDistance(pixels); + m_tabSize = chars; + int pixels = fontMetrics().horizontalAdvance(QString(chars, '0')); + this->setTabStopDistance(pixels); } diff --git a/pglab/codeeditor/CodeEditor.h b/pglab/codeeditor/CodeEditor.h index 6bfc64b..2f40d5f 100644 --- a/pglab/codeeditor/CodeEditor.h +++ b/pglab/codeeditor/CodeEditor.h @@ -13,7 +13,6 @@ class CodeEditor : public QPlainTextEdit public: explicit CodeEditor(QWidget *parent = nullptr); - void gutterAreaPaintEvent(QPaintEvent *event); int gutterAreaWidth(); void addErrorMarker(int position, int length); @@ -47,9 +46,16 @@ private: int m_tabSize = 0; // tabSize in characters bool m_useTab = false; - void updateExtraSelections(); + bool lineHasError(int blockNumber) const; + void updateExtraSelections(); bool indentSelection(bool indent); + void drawGutterErrorMarker(QPainter &painter, int top); + void insertIndentation(QTextCursor &cursor); + void removeIndentation(QTextCursor &cursor); + void makeSelection(QTextCursor &cursor, int first_pos, int end_block); + + friend class GutterPainter; }; #endif // CODEEDITOR_H diff --git a/pglab/codeeditor/EditorGutter.cpp b/pglab/codeeditor/EditorGutter.cpp index c67804f..c15efa7 100644 --- a/pglab/codeeditor/EditorGutter.cpp +++ b/pglab/codeeditor/EditorGutter.cpp @@ -1,5 +1,6 @@ #include "EditorGutter.h" #include "CodeEditor.h" +#include "GutterPainter.h" EditorGutter::EditorGutter(CodeEditor *editor) : QWidget(editor) @@ -13,5 +14,6 @@ QSize EditorGutter::sizeHint() const void EditorGutter::paintEvent(QPaintEvent *event) { - codeEditor->gutterAreaPaintEvent(event); + GutterPainter gutterpainter(codeEditor, event); + gutterpainter.Paint(); } diff --git a/pglab/codeeditor/GutterPainter.cpp b/pglab/codeeditor/GutterPainter.cpp new file mode 100644 index 0000000..59a4198 --- /dev/null +++ b/pglab/codeeditor/GutterPainter.cpp @@ -0,0 +1,69 @@ +#include "GutterPainter.h" +#include "CodeEditor.h" + + +GutterPainter::GutterPainter(CodeEditor *editor, QPaintEvent *event) + : editor(editor) + , painter(editor->gutterArea) + , event(event) + , fontMetrics(editor->fontMetrics()) +{} + +void GutterPainter::Paint() +{ + painter.fillRect(event->rect(), Qt::lightGray); + + LoopState loopState(editor); + // We will now loop through all visible lines and paint the line numbers in the + // extra area for each line. Notice that in a plain text edit each line will + // consist of one QTextBlock; though, if line wrapping is enabled, a line may span + // several rows in the text edit's viewport. + // + // We get the top and bottom y-coordinate of the first text block, and adjust these + // values by the height of the current text block in each iteration in the loop. + while (loopState.block.isValid() && loopState.top <= event->rect().bottom()) + { + if (loopState.block.isVisible() && loopState.bottom >= event->rect().top()) + { + drawLineNumber(loopState); + if (editor->lineHasError(loopState.blockNumber())) + drawGutterErrorMarker(loopState); + } + + loopState.advanceToNextLine(); + } +} + +void GutterPainter::drawLineNumber(const LoopState &loopState) +{ + QString number = QString::number(loopState.blockNumber() + 1); + painter.setPen(Qt::black); + painter.drawText(0, loopState.top, editor->gutterArea->width(), fontMetrics.height(), + Qt::AlignRight, number); +} + +void GutterPainter::drawGutterErrorMarker(const LoopState &loopState) +{ + int s = fontMetrics.height() - 8; + painter.setBrush(QBrush(Qt::red)); + painter.drawEllipse(4, loopState.top + 4, s, s); +} + +GutterPainter::LoopState::LoopState(CodeEditor *editor) + : editor(editor) + , block(editor->firstVisibleBlock()) + , top((int) editor->blockBoundingGeometry(block).translated(editor->contentOffset()).top()) + , bottom(top + (int) editor->blockBoundingRect(block).height()) +{} + +int GutterPainter::LoopState::blockNumber() const +{ + return block.blockNumber(); +} + +void GutterPainter::LoopState::advanceToNextLine() +{ + block = block.next(); + top = bottom; + bottom = top + (int) editor->blockBoundingRect(block).height(); +} diff --git a/pglab/codeeditor/GutterPainter.h b/pglab/codeeditor/GutterPainter.h new file mode 100644 index 0000000..3594ea3 --- /dev/null +++ b/pglab/codeeditor/GutterPainter.h @@ -0,0 +1,38 @@ +#include +#include + +#pragma once + +class CodeEditor; +class QPaintEvent; + +class GutterPainter +{ +public: + GutterPainter(CodeEditor *editor, QPaintEvent *event); + + void Paint(); +private: + CodeEditor *editor; + QPainter painter; + QPaintEvent *event; + QFontMetrics fontMetrics; + + // Loop state + struct LoopState { + CodeEditor *editor; + QTextBlock block; + int top; + int bottom; + + LoopState(CodeEditor *editor); + + int blockNumber() const; + + void advanceToNextLine(); + }; + + void drawLineNumber(const LoopState &loopState); + + void drawGutterErrorMarker(const LoopState &loopState); +}; diff --git a/pglab/crud/CrudTab.cpp b/pglab/crud/CrudTab.cpp index 3467597..3c7585b 100644 --- a/pglab/crud/CrudTab.cpp +++ b/pglab/crud/CrudTab.cpp @@ -70,7 +70,7 @@ void CrudTab::on_actionRemove_rows_triggered() { std::set> row_ranges; auto selection = m_SortFilterProxy->mapSelectionToSource(ui->tableView->selectionModel()->selection()); - for (auto range : selection) { + for (auto& range : selection) { row_ranges.emplace(range.top(), range.height()); } std::set> merged_ranges; diff --git a/pglab/pglab.pro b/pglab/pglab.pro index 1baec66..0b5944a 100644 --- a/pglab/pglab.pro +++ b/pglab/pglab.pro @@ -45,6 +45,7 @@ SOURCES += main.cpp\ catalog/widgets/ColumnPage.cpp \ catalog/widgets/DependantsPage.cpp \ catalog/widgets/TriggerPage.cpp \ + codeeditor/GutterPainter.cpp \ crud/CrudModel.cpp \ crud/CrudTab.cpp \ querytool/QueryExplainModel.cpp \ @@ -119,6 +120,7 @@ HEADERS += \ catalog/widgets/ColumnPage.h \ catalog/widgets/DependantsPage.h \ catalog/widgets/TriggerPage.h \ + codeeditor/GutterPainter.h \ crud/CrudModel.h \ crud/CrudTab.h \ querytool/QueryExplainModel.h \