#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)) , colorTheme(GetColorTheme()) { 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; selection.format.setBackground(colorTheme.currentLine); 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; selection.format.setBackground(colorTheme.errorLine); 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); } bool CodeEditor::lineHasError(int blockNumber) const { return errorLines.count(blockNumber) > 0; } void CodeEditor::keyPressEvent(QKeyEvent *e) { auto k = e->key(); switch (k) { case Qt::Key_Tab: if (indentSelection(true)) return; break; case Qt::Key_Backtab: if (indentSelection(false)) return; break; case Qt::Key_Equal: case Qt::Key_Plus: if (e->modifiers().testFlag(Qt::ControlModifier)) { auto f = font(); f.setPointSize(f.pointSize() + 1); setFont(f); } break; case Qt::Key_Minus: if (e->modifiers().testFlag(Qt::ControlModifier)) { auto f = font(); f.setPointSize(f.pointSize() - 1); setFont(f); } break; } 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) insertIndentation(cursor); else removeIndentation(cursor); cursor.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor); } cursor.endEditBlock(); 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); } void CodeEditor::setTabSize(int chars) { m_tabSize = chars; int pixels = fontMetrics().horizontalAdvance(QString(chars, '0')); this->setTabStopDistance(pixels); }