// QCodeEditor #include #include #include #include #include #include // Qt #include #include #include #include #include #include #include #include #include #include #include static QVector> parentheses = { {"(", ")"}, {"{", "}"}, {"[", "]"}, {"\"", "\""}, {"'", "'"} }; QCodeEditor::QCodeEditor(QWidget* widget) : QTextEdit(widget), m_highlighter(nullptr), m_syntaxStyle(nullptr), m_lineNumberArea(new QLineNumberArea(this)), m_completer(nullptr), m_framedAttribute(new QFramedTextAttribute(this)), m_autoIndentation(true), m_autoParentheses(true), m_replaceTab(true), m_tabReplace(QString(4, ' ')) { initDocumentLayoutHandlers(); initFont(); performConnections(); setSyntaxStyle(QSyntaxStyle::defaultStyle()); } void QCodeEditor::initDocumentLayoutHandlers() { document() ->documentLayout() ->registerHandler( QFramedTextAttribute::type(), m_framedAttribute ); } void QCodeEditor::initFont() { auto fnt = QFontDatabase::systemFont(QFontDatabase::FixedFont); fnt.setFixedPitch(true); fnt.setPointSize(10); setFont(fnt); } void QCodeEditor::performConnections() { connect( document(), &QTextDocument::blockCountChanged, this, &QCodeEditor::updateLineNumberAreaWidth ); connect( verticalScrollBar(), &QScrollBar::valueChanged, [this](int){ m_lineNumberArea->update(); } ); connect( this, &QTextEdit::cursorPositionChanged, this, &QCodeEditor::updateExtraSelection ); connect( this, &QTextEdit::selectionChanged, this, &QCodeEditor::onSelectionChanged ); } void QCodeEditor::setHighlighter(QStyleSyntaxHighlighter* highlighter) { if (m_highlighter) { m_highlighter->setDocument(nullptr); } m_highlighter = highlighter; if (m_highlighter) { m_highlighter->setSyntaxStyle(m_syntaxStyle); m_highlighter->setDocument(document()); } } void QCodeEditor::setSyntaxStyle(QSyntaxStyle* style) { m_syntaxStyle = style; m_framedAttribute->setSyntaxStyle(m_syntaxStyle); m_lineNumberArea->setSyntaxStyle(m_syntaxStyle); if (m_highlighter) { m_highlighter->setSyntaxStyle(m_syntaxStyle); } updateStyle(); } void QCodeEditor::updateStyle() { if (m_highlighter) { m_highlighter->rehighlight(); } if (m_syntaxStyle) { auto currentPalette = palette(); // Setting text format/color currentPalette.setColor( QPalette::ColorRole::Text, m_syntaxStyle->getFormat("Text").foreground().color() ); // Setting common background currentPalette.setColor( QPalette::Base, m_syntaxStyle->getFormat("Text").background().color() ); // Setting selection color currentPalette.setColor( QPalette::Highlight, m_syntaxStyle->getFormat("Selection").background().color() ); setPalette(currentPalette); } updateExtraSelection(); } void QCodeEditor::onSelectionChanged() { auto selected = textCursor().selectedText(); auto cursor = textCursor(); // Cursor is null if setPlainText was called. if (cursor.isNull()) { return; } cursor.movePosition(QTextCursor::MoveOperation::Left); cursor.select(QTextCursor::SelectionType::WordUnderCursor); QSignalBlocker blocker(this); m_framedAttribute->clear(cursor); if (selected.size() > 1 && cursor.selectedText() == selected) { auto backup = textCursor(); // Perform search selecting handleSelectionQuery(cursor); setTextCursor(backup); } } void QCodeEditor::resizeEvent(QResizeEvent* e) { QTextEdit::resizeEvent(e); updateLineGeometry(); } void QCodeEditor::updateLineGeometry() { QRect cr = contentsRect(); m_lineNumberArea->setGeometry( QRect(cr.left(), cr.top(), m_lineNumberArea->sizeHint().width(), cr.height() ) ); } void QCodeEditor::updateLineNumberAreaWidth(int) { setViewportMargins(m_lineNumberArea->sizeHint().width(), 0, 0, 0); } void QCodeEditor::updateLineNumberArea(const QRect& rect) { m_lineNumberArea->update( 0, rect.y(), m_lineNumberArea->sizeHint().width(), rect.height() ); updateLineGeometry(); if (rect.contains(viewport()->rect())) { updateLineNumberAreaWidth(0); } } void QCodeEditor::handleSelectionQuery(QTextCursor cursor) { auto searchIterator = cursor; searchIterator.movePosition(QTextCursor::Start); searchIterator = document()->find(cursor.selectedText(), searchIterator); while (searchIterator.hasSelection()) { m_framedAttribute->frame(searchIterator); searchIterator = document()->find(cursor.selectedText(), searchIterator); } } void QCodeEditor::updateExtraSelection() { QList extra; highlightCurrentLine(extra); highlightParenthesis(extra); setExtraSelections(extra); } void QCodeEditor::highlightParenthesis(QList& extraSelection) { auto currentSymbol = charUnderCursor(); auto prevSymbol = charUnderCursor(-1); for (auto& pair : parentheses) { int direction; QChar counterSymbol; QChar activeSymbol; auto position = textCursor().position(); if (pair.first == currentSymbol) { direction = 1; counterSymbol = pair.second[0]; activeSymbol = currentSymbol; } else if (pair.second == prevSymbol) { direction = -1; counterSymbol = pair.first[0]; activeSymbol = prevSymbol; position--; } else { continue; } auto counter = 1; while (counter != 0 && position > 0 && position < (document()->characterCount() - 1)) { // Moving position position += direction; auto character = document()->characterAt(position); // Checking symbol under position if (character == activeSymbol) { ++counter; } else if (character == counterSymbol) { --counter; } } auto format = m_syntaxStyle->getFormat("Parentheses"); // Found if (counter == 0) { ExtraSelection selection{}; auto directionEnum = direction < 0 ? QTextCursor::MoveOperation::Left : QTextCursor::MoveOperation::Right; selection.format = format; selection.cursor = textCursor(); selection.cursor.clearSelection(); selection.cursor.movePosition( directionEnum, QTextCursor::MoveMode::MoveAnchor, std::abs(textCursor().position() - position) ); selection.cursor.movePosition( QTextCursor::MoveOperation::Right, QTextCursor::MoveMode::KeepAnchor, 1 ); extraSelection.append(selection); selection.cursor = textCursor(); selection.cursor.clearSelection(); selection.cursor.movePosition( directionEnum, QTextCursor::MoveMode::KeepAnchor, 1 ); extraSelection.append(selection); } break; } } void QCodeEditor::highlightCurrentLine(QList& extraSelection) { if (!isReadOnly()) { QTextEdit::ExtraSelection selection{}; selection.format = m_syntaxStyle->getFormat("CurrentLine"); selection.format.setForeground(QBrush()); selection.format.setProperty(QTextFormat::FullWidthSelection, true); selection.cursor = textCursor(); selection.cursor.clearSelection(); extraSelection.append(selection); } } void QCodeEditor::paintEvent(QPaintEvent* e) { updateLineNumberArea(e->rect()); QTextEdit::paintEvent(e); } int QCodeEditor::getFirstVisibleBlock() { // Detect the first block for which bounding rect - once translated // in absolute coordinated - is contained by the editor's text area // Costly way of doing but since "blockBoundingGeometry(...)" doesn't // exists for "QTextEdit"... QTextCursor curs = QTextCursor(document()); curs.movePosition(QTextCursor::Start); for(int i=0; i < document()->blockCount(); ++i) { QTextBlock block = curs.block(); QRect r1 = viewport()->geometry(); QRect r2 = document() ->documentLayout() ->blockBoundingRect(block) .translated( viewport()->geometry().x(), viewport()->geometry().y() - verticalScrollBar()->sliderPosition() ).toRect(); if (r1.intersects(r2)) { return i; } curs.movePosition(QTextCursor::NextBlock); } return 0; } bool QCodeEditor::proceedCompleterBegin(QKeyEvent *e) { if (m_completer && m_completer->popup()->isVisible()) { switch (e->key()) { case Qt::Key_Enter: case Qt::Key_Return: case Qt::Key_Escape: case Qt::Key_Tab: case Qt::Key_Backtab: e->ignore(); return true; // let the completer do default behavior default: break; } } // todo: Replace with modifiable QShortcut auto isShortcut = ((e->modifiers() & Qt::ControlModifier) && e->key() == Qt::Key_Space); return !(!m_completer || !isShortcut); } void QCodeEditor::proceedCompleterEnd(QKeyEvent *e) { auto ctrlOrShift = e->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier); if (!m_completer || (ctrlOrShift && e->text().isEmpty()) || e->key() == Qt::Key_Delete) { return; } static QString eow(R"(~!@#$%^&*()_+{}|:"<>?,./;'[]\-=)"); auto isShortcut = ((e->modifiers() & Qt::ControlModifier) && e->key() == Qt::Key_Space); auto completionPrefix = wordUnderCursor(); if (!isShortcut && (e->text().isEmpty() || completionPrefix.length() < 2 || eow.contains(e->text().right(1)))) { m_completer->popup()->hide(); return; } if (completionPrefix != m_completer->completionPrefix()) { m_completer->setCompletionPrefix(completionPrefix); m_completer->popup()->setCurrentIndex(m_completer->completionModel()->index(0, 0)); } auto cursRect = cursorRect(); cursRect.setWidth( m_completer->popup()->sizeHintForColumn(0) + m_completer->popup()->verticalScrollBar()->sizeHint().width() ); m_completer->complete(cursRect); } void QCodeEditor::keyPressEvent(QKeyEvent* e) { #if QT_VERSION >= 0x050A00 const int defaultIndent = tabStopDistance() / fontMetrics().averageCharWidth(); #else const int defaultIndent = tabStopWidth() / fontMetrics().averageCharWidth(); #endif auto completerSkip = proceedCompleterBegin(e); if (!completerSkip) { if (m_replaceTab && e->key() == Qt::Key_Tab && e->modifiers() == Qt::NoModifier) { insertPlainText(m_tabReplace); return; } // Auto indentation int indentationLevel = getIndentationSpaces(); #if QT_VERSION >= 0x050A00 int tabCounts = indentationLevel * fontMetrics().averageCharWidth() / tabStopDistance(); #else int tabCounts = indentationLevel * fontMetrics().averageCharWidth() / tabStopWidth(); #endif // Have Qt Edior like behaviour, if {|} and enter is pressed indent the two // parenthesis if (m_autoIndentation && (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) && charUnderCursor() == '}' && charUnderCursor(-1) == '{') { int charsBack = 0; insertPlainText("\n"); if (m_replaceTab) insertPlainText(QString(indentationLevel + defaultIndent, ' ')); else insertPlainText(QString(tabCounts + 1, '\t')); insertPlainText("\n"); charsBack++; if (m_replaceTab) { insertPlainText(QString(indentationLevel, ' ')); charsBack += indentationLevel; } else { insertPlainText(QString(tabCounts, '\t')); charsBack += tabCounts; } while (charsBack--) moveCursor(QTextCursor::MoveOperation::Left); return; } // Shortcut for moving line to left if (m_replaceTab && e->key() == Qt::Key_Backtab) { indentationLevel = std::min(indentationLevel, (int) m_tabReplace.size()); auto cursor = textCursor(); cursor.movePosition(QTextCursor::MoveOperation::StartOfLine); cursor.movePosition(QTextCursor::MoveOperation::Right, QTextCursor::MoveMode::KeepAnchor, indentationLevel); cursor.removeSelectedText(); return; } QTextEdit::keyPressEvent(e); if (m_autoIndentation && (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter)) { if (m_replaceTab) insertPlainText(QString(indentationLevel, ' ')); else insertPlainText(QString(tabCounts, '\t')); } if (m_autoParentheses) { for (auto&& el : parentheses) { // Inserting closed brace if (el.first == e->text()) { insertPlainText(el.second); moveCursor(QTextCursor::MoveOperation::Left); break; } // If it's close brace - check parentheses if (el.second == e->text()) { auto symbol = charUnderCursor(); if (symbol == el.second) { textCursor().deletePreviousChar(); moveCursor(QTextCursor::MoveOperation::Right); } break; } } } } proceedCompleterEnd(e); } void QCodeEditor::setAutoIndentation(bool enabled) { m_autoIndentation = enabled; } bool QCodeEditor::autoIndentation() const { return m_autoIndentation; } void QCodeEditor::setAutoParentheses(bool enabled) { m_autoParentheses = enabled; } bool QCodeEditor::autoParentheses() const { return m_autoParentheses; } void QCodeEditor::setTabReplace(bool enabled) { m_replaceTab = enabled; } bool QCodeEditor::tabReplace() const { return m_replaceTab; } void QCodeEditor::setTabReplaceSize(int val) { m_tabReplace.clear(); m_tabReplace.fill(' ', val); } int QCodeEditor::tabReplaceSize() const { return m_tabReplace.size(); } void QCodeEditor::setCompleter(QCompleter *completer) { if (m_completer) { disconnect(m_completer, nullptr, this, nullptr); } m_completer = completer; if (!m_completer) { return; } m_completer->setWidget(this); m_completer->setCompletionMode(QCompleter::CompletionMode::PopupCompletion); connect( m_completer, QOverload::of(&QCompleter::activated), this, &QCodeEditor::insertCompletion ); } void QCodeEditor::focusInEvent(QFocusEvent *e) { if (m_completer) { m_completer->setWidget(this); } QTextEdit::focusInEvent(e); } void QCodeEditor::insertCompletion(QString s) { if (m_completer->widget() != this) { return; } auto tc = textCursor(); tc.select(QTextCursor::SelectionType::WordUnderCursor); tc.insertText(s); setTextCursor(tc); } QCompleter *QCodeEditor::completer() const { return m_completer; } QChar QCodeEditor::charUnderCursor(int offset) const { auto block = textCursor().blockNumber(); auto index = textCursor().positionInBlock(); auto text = document()->findBlockByNumber(block).text(); index += offset; if (index < 0 || index >= text.size()) { return {}; } return text[index]; } QString QCodeEditor::wordUnderCursor() const { auto tc = textCursor(); tc.select(QTextCursor::WordUnderCursor); return tc.selectedText(); } void QCodeEditor::insertFromMimeData(const QMimeData* source) { insertPlainText(source->text()); } int QCodeEditor::getIndentationSpaces() { auto blockText = textCursor().block().text(); int indentationLevel = 0; for (auto i = 0; i < blockText.size() && QString("\t ").contains(blockText[i]); ++i) { if (blockText[i] == ' ') { indentationLevel++; } else { #if QT_VERSION >= 0x050A00 indentationLevel += tabStopDistance() / fontMetrics().averageCharWidth(); #else indentationLevel += tabStopWidth() / fontMetrics().averageCharWidth(); #endif } } return indentationLevel; }