#include "CodeEditor.h" #include "EditorGutter.h" #include #include #include // // Adapted from codeeditor example // http://doc.qt.io/qt-5/qtwidgets-widgets-codeeditor-example.html // // Used term gutter as I want to expand it to do other things to like error // position marking. // CodeEditor::CodeEditor(QWidget *parent) : QPlainTextEdit(parent) , gutterArea(new EditorGutter(this)) { connect(this, SIGNAL(blockCountChanged(int)), this, SLOT(updateGutterAreaWidth(int))); connect(this, SIGNAL(updateRequest(QRect,int)), this, SLOT(updateGutterArea(QRect,int))); connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(highlightCurrentLine())); connect(this, SIGNAL(textChanged()), this, SLOT(onTextChanged())); setWordWrapMode(QTextOption::NoWrap); updateGutterAreaWidth(0); setTabSize(4); highlightCurrentLine(); } int CodeEditor::gutterAreaWidth() { int digits = 1; int max = qMax(1, blockCount()); while (max >= 10) { max /= 10; ++digits; } int space = 3 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * (digits + 2); return space; } void CodeEditor::updateGutterAreaWidth(int /* newBlockCount */) { setViewportMargins(gutterAreaWidth(), 0, 0, 0); } void CodeEditor::updateGutterArea(const QRect &rect, int dy) { if (dy) gutterArea->scroll(0, dy); else gutterArea->update(0, rect.y(), gutterArea->width(), rect.height()); if (rect.contains(viewport()->rect())) updateGutterAreaWidth(0); } void CodeEditor::resizeEvent(QResizeEvent *e) { QPlainTextEdit::resizeEvent(e); QRect cr = contentsRect(); gutterArea->setGeometry(QRect(cr.left(), cr.top(), gutterAreaWidth(), cr.height())); } void CodeEditor::highlightCurrentLine() { QTextEdit::ExtraSelection selection; QColor lineColor = QColor(Qt::yellow).lighter(160); selection.format.setBackground(lineColor); selection.format.setProperty(QTextFormat::FullWidthSelection, true); selection.cursor = textCursor(); selection.cursor.clearSelection(); currentLine = selection; updateExtraSelections(); } void CodeEditor::onTextChanged() { clearErrorMarkers(); } void CodeEditor::addErrorMarker(int position, int length) { QTextEdit::ExtraSelection selection; QColor lineColor = QColor(Qt::red).lighter(160); selection.format.setBackground(lineColor); selection.format.setFontItalic(true); selection.cursor = textCursor(); selection.cursor.setPosition(position); int lineno = selection.cursor.blockNumber(); errorLines.insert(lineno); selection.cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, length); errorMarkers.append(selection); update(); updateExtraSelections(); } void CodeEditor::clearErrorMarkers() { errorMarkers.clear(); errorLines.clear(); update(); updateExtraSelections(); } void CodeEditor::updateExtraSelections() { QList extraSelections; extraSelections.append(currentLine); for (auto&& e : errorMarkers) extraSelections.append(e); setExtraSelections(extraSelections); } void CodeEditor::gutterAreaPaintEvent(QPaintEvent *event) { QPainter painter(gutterArea); painter.fillRect(event->rect(), Qt::lightGray); 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; } } void CodeEditor::keyPressEvent(QKeyEvent *e) { auto k = e->key(); qDebug() << QString("%1").arg(k, 0, 16); if (k == Qt::Key_Tab) { // Function returns false if there was no selection to indent if (indentSelection(true)) return; } else if (k == Qt::Key_Backtab) { // Function returns false if there was no selection to indent if (indentSelection(false)) return; } else if (k == Qt::Key_Equal || k == Qt::Key_Plus) { if (e->modifiers().testFlag(Qt::ControlModifier)) { auto f = font(); f.setPointSize(f.pointSize() + 1); setFont(f); } } else if (k == Qt::Key_Minus) { if (e->modifiers().testFlag(Qt::ControlModifier)) { auto f = font(); f.setPointSize(f.pointSize() - 1); setFont(f); } } QPlainTextEdit::keyPressEvent(e); } bool CodeEditor::indentSelection(bool indent) { auto cursor = textCursor(); if(!cursor.hasSelection()) return false; auto first_pos = cursor.anchor(); auto end_pos = cursor.position(); if(first_pos > end_pos) std::swap(first_pos, end_pos); cursor.setPosition(first_pos, QTextCursor::MoveAnchor); auto start_block = cursor.block().blockNumber(); cursor.setPosition(end_pos, QTextCursor::MoveAnchor); auto end_block = cursor.block().blockNumber(); if (end_block == start_block) return false; cursor.beginEditBlock(); cursor.setPosition(first_pos, QTextCursor::MoveAnchor); const auto block_count = end_block - start_block; for(int block = 0; block <= block_count; ++block) { cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor); if (indent) { if (m_useTab) cursor.insertText("\t"); else cursor.insertText(QString(m_tabSize, ' ')); } 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(); } 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); setTextCursor(cursor); return true; } void CodeEditor::setFont(const QFont &f) { 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); }