WingHexPy/QCodeEditor/QCodeEditor.cpp

643 lines
18 KiB
C++

// QCodeEditor
#include "QCodeEditor.hpp"
#include "QFramedTextAttribute.hpp"
#include "QLineNumberArea.hpp"
#include "QPythonHighlighter.hpp"
#include "QStyleSyntaxHighlighter.hpp"
#include "QSyntaxStyle.hpp"
// Qt
#include <QAbstractItemView>
#include <QAbstractTextDocumentLayout>
#include <QCompleter>
#include <QCursor>
#include <QFontDatabase>
#include <QMimeData>
#include <QPaintEvent>
#include <QScrollBar>
#include <QShortcut>
#include <QTextBlock>
#include <QTextCharFormat>
// PythonQt
#include <QStringListModel>
static QVector<QPair<QString, QString>> parentheses = {
{"(", ")"}, {"{", "}"}, {"[", "]"}, {"\"", "\""}, {"'", "'"}};
QCodeEditor::QCodeEditor(QWidget *widget)
: QTextEdit(widget), m_highlighter(nullptr), m_syntaxStyle(nullptr),
m_lineNumberArea(new QLineNumberArea(this)),
m_completer(new QCompleter(this)),
m_framedAttribute(new QFramedTextAttribute(this)),
m_autoIndentation(true), m_autoParentheses(true), m_replaceTab(true),
m_tabReplace(QString(4, ' ')) {
initDocumentLayoutHandlers();
initFont();
performConnections();
setSyntaxStyle(QSyntaxStyle::defaultStyle());
py = PythonQt::self();
_context = py->getMainModule();
connect(m_completer, QOverload<const QString &>::of(&QCompleter::activated),
this, &QCodeEditor::insertCompletion);
}
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<QTextEdit::ExtraSelection> extra;
highlightCurrentLine(extra);
highlightParenthesis(extra);
setExtraSelections(extra);
}
void QCodeEditor::highlightParenthesis(
QList<QTextEdit::ExtraSelection> &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<QTextEdit::ExtraSelection> &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: { // added by wingsummer
if (!m_completer->popup()->currentIndex().isValid()) {
insertCompletion(m_completer->currentCompletion());
m_completer->popup()->hide();
e->accept();
}
e->ignore();
return true;
break;
}
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());
handleTabCompletion();
}
void QCodeEditor::keyPressEvent(QKeyEvent *e) {
#if QT_VERSION >= 0x050A00
const int defaultIndent =
int(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 = int(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, 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::handleTabCompletion() {
QTextCursor textCursor = this->textCursor();
int pos = textCursor.position();
textCursor.movePosition(QTextCursor::StartOfLine);
textCursor.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor);
int startPos = textCursor.selectionStart();
int offset = pos - startPos;
QString text = textCursor.selectedText();
QString textToComplete;
int cur = offset;
while (cur--) {
QChar c = text.at(cur);
if (c.isLetterOrNumber() || c == '.' || c == '_') {
textToComplete.prepend(c);
} else {
break;
}
}
QString lookup;
QString compareText = textToComplete;
int dot = compareText.lastIndexOf('.');
if (dot != -1) {
lookup = compareText.mid(0, dot);
compareText = compareText.mid(dot + 1, offset);
}
if (!lookup.isEmpty() || !compareText.isEmpty()) {
compareText = compareText.toLower();
QStringList found;
QStringList l = py->introspection(_context, lookup, PythonQt::Anything);
Q_FOREACH (QString n, l) {
if (n.toLower().startsWith(compareText)) {
found << n;
}
}
if (!found.isEmpty()) {
m_completer->setCompletionPrefix(compareText);
m_completer->setCompletionMode(QCompleter::PopupCompletion);
m_completer->setModel(new QStringListModel(found, m_completer));
m_completer->setCaseSensitivity(Qt::CaseInsensitive);
QTextCursor c = this->textCursor();
c.movePosition(QTextCursor::StartOfWord);
QRect cr = cursorRect(c);
cr.setWidth(
m_completer->popup()->sizeHintForColumn(0) +
m_completer->popup()->verticalScrollBar()->sizeHint().width());
cr.translate(0, 8);
m_completer->complete(cr);
} else {
m_completer->popup()->hide();
}
} else {
m_completer->popup()->hide();
}
}
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::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);
}
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;
}