From aa695f84945a3c145c9b869afc4297be7f2caefd Mon Sep 17 00:00:00 2001 From: Ivan Pelipenko Date: Sat, 12 Sep 2020 01:08:13 +0300 Subject: [PATCH] Big QCodeEdit update: supports for autocomplete/help hint and custom data. Add dynamic links on Ctrl+mouse by 2 new virtual functions. --- CMakeLists.txt | 2 +- libs/widgets/qcodeedit.cpp | 132 +++++++++++++++++++------ libs/widgets/qcodeedit.h | 47 ++++++--- libs/widgets/qcodeedit_completer_p.cpp | 6 +- libs/widgets/qcodeedit_completer_p.h | 2 +- 5 files changed, 144 insertions(+), 45 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b74a510..696cc25 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.0) cmake_policy(SET CMP0017 NEW) # need include() with .cmake project(qad) set(qad_MAJOR 1) -set(qad_MINOR 11) +set(qad_MINOR 12) set(qad_REVISION 0) set(qad_SUFFIX ) set(qad_COMPANY SHS) diff --git a/libs/widgets/qcodeedit.cpp b/libs/widgets/qcodeedit.cpp index 582f7ca..970d5b5 100644 --- a/libs/widgets/qcodeedit.cpp +++ b/libs/widgets/qcodeedit.cpp @@ -18,9 +18,11 @@ #include "ui_qcodeedit.h" Q_DECLARE_METATYPE(QTextCursor) +Q_DECLARE_METATYPE(QCodeEdit::ACEntry) QCodeEdit::QCodeEdit(QWidget * parent): QWidget(parent) { qRegisterMetaType(); + qRegisterMetaType(); ui = new Ui::QCodeEdit(); ui->setupUi(this); ui->widgetSearch->hide(); @@ -38,6 +40,8 @@ QCodeEdit::QCodeEdit(QWidget * parent): QWidget(parent) { es_search.format.setBackground(QColor(255, 240, 10)); es_range.format.setBackground(QColor(230, 246, 255)); es_range.format.setProperty(QTextFormat::FullWidthSelection, true); + es_link.format.setForeground(Qt::blue); + es_link.format.setFontUnderline(true); widget_help = new QFrame(); widget_help->setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint); widget_help->setFocusPolicy(Qt::NoFocus); @@ -45,18 +49,19 @@ QCodeEdit::QCodeEdit(QWidget * parent): QWidget(parent) { widget_help->setFrameShape(QFrame::StyledPanel); widget_help->setLayout(new QBoxLayout(QBoxLayout::TopToBottom)); widget_help->layout()->setContentsMargins(0, 0, 0, 0); - for (int i = 0; i < 2; ++i) { + for (int i = 0; i < 3; ++i) { lbl_help[i] = new IconedLabel(); lbl_help[i]->setFrameShadow(QFrame::Plain); lbl_help[i]->setFrameShape(QFrame::NoFrame); lbl_help[i]->setDirection(IconedLabel::RightToLeft); widget_help->layout()->addWidget(lbl_help[i]); } - lbl_help[1]->setIcon(QIcon(":/icons/f1.png")); - lbl_help[1]->setText(tr("Press F1 for details")); + lbl_help[lhF1]->setIcon(QIcon(":/icons/f1.png")); + lbl_help[lhF1]->setText(tr("Press F1 for details")); completer = new QCodeEditCompleter(); ui->textCode->setCursorWidth(qMax(qRound(fontHeight() / 10.), 1)); + ui->textCode->viewport()->setMouseTracking(true); ui->textLines->viewport()->setAutoFillBackground(false); ui->textLines->viewport()->setCursor(Qt::ArrowCursor); #if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0) @@ -246,6 +251,11 @@ QPlainTextEdit * QCodeEdit::textEdit() const { } +void QCodeEdit::registerAutoCompletitionClass(int id, QCodeEdit::ACClassType ac_class, const QString & name, const QIcon & icon) { + ac_classes[id] = ACClass(id, ac_class, name, icon); +} + + int QCodeEdit::skipRange(const QString & s, int pos, QChar oc, QChar cc, QChar sc) { int cnt = 0; bool skip = false; @@ -400,6 +410,18 @@ bool QCodeEdit::eventFilter(QObject * o, QEvent * e) { completer->hide(); hideHelp(); } + if (e->type() == QEvent::MouseMove) { + QMouseEvent * me = (QMouseEvent*)e; + if (me->modifiers().testFlag(Qt::ControlModifier)) { + showLink(); + } + } + if (e->type() == QEvent::MouseButtonPress) { + QMouseEvent * me = (QMouseEvent*)e; + if (me->modifiers().testFlag(Qt::ControlModifier) && (me->button() == Qt::LeftButton)) { + gotoLink(); + } + } return QWidget::eventFilter(o, e); } if (o == ui->textCode) { @@ -503,6 +525,9 @@ bool QCodeEdit::eventFilter(QObject * o, QEvent * e) { return true; } break; + case Qt::Key_Control: + showLink(); + break; default: break; } if (!ke->text().isEmpty()) @@ -515,6 +540,9 @@ bool QCodeEdit::eventFilter(QObject * o, QEvent * e) { QMetaObject::invokeMethod(this, "invokeAutoCompletition", Qt::QueuedConnection, Q_ARG(bool, false)); } break; + case QEvent::KeyRelease: + hideLink(); + break; case QEvent::FocusOut: if (_ignore_focus_out) { _ignore_focus_out = false; @@ -522,6 +550,7 @@ bool QCodeEdit::eventFilter(QObject * o, QEvent * e) { } case QEvent::Hide: case QEvent::HideToParent: + hideLink(); case QEvent::MouseButtonPress: //qDebug() << e; completer->hide(); @@ -563,7 +592,7 @@ void QCodeEdit::changeEvent(QEvent * e) { switch (e->type()) { case QEvent::LanguageChange: ui->retranslateUi(this); - lbl_help[1]->setText(tr("Press F1 for details")); + lbl_help[lhF1]->setText(tr("Press F1 for details")); break; default: break; @@ -628,7 +657,7 @@ void QCodeEdit::highlightBrackets() { void QCodeEdit::applyExtraSelection() { ui->textCode->setExtraSelections(QList() << es_line << es_selected - << es_custom << es_brackets << es_search_list << es_cursor); + << es_custom << es_brackets << es_search_list << es_cursor << es_link); } @@ -953,6 +982,41 @@ void QCodeEdit::autoIndent() { } +void QCodeEdit::showLink() { + QTextCursor tc = ui->textCode->cursorForPosition(ui->textCode->mapFromGlobal(QCursor::pos())); + tc.select(QTextCursor::WordUnderCursor); + link_entry = findEntryOnCursor(tc); + if (link_entry.isNull()) { + hideLink(); + return; + } + if (!linkValidate(link_entry)) { + hideLink(); + return; + } + es_link.cursor = tc; + ui->textCode->viewport()->setCursor(tc.isNull() ? Qt::IBeamCursor : Qt::PointingHandCursor); + applyExtraSelection(); + //qDebug() << "showLink" << tc.selectedText() << link_entry.type << link_entry.name; +} + + +void QCodeEdit::hideLink() { + es_link.cursor = QTextCursor(); + ui->textCode->viewport()->setCursor(Qt::IBeamCursor); + link_entry = ACEntry(); + applyExtraSelection(); + //qDebug() << "hideLink"; +} + + +void QCodeEdit::gotoLink() { + if (link_entry.isNull()) return; + QMetaObject::invokeMethod(this, "_activateLink", Qt::QueuedConnection, Q_ARG(QCodeEdit::ACEntry, link_entry)); + hideLink(); +} + + void QCodeEdit::scrollToTop() { prev_lc = -1; updateLines(); @@ -1036,20 +1100,20 @@ QString QCodeEdit::selectArg(QString s, int arg) { } -void QCodeEdit::raiseHelp(QTextCursor tc, int arg) { - bool ok; - QPair scope = getScope(tc, &ok); +QCodeEdit::ACEntry QCodeEdit::findEntryOnCursor(QTextCursor tc, int arg, ACClass * acc, QPair * scope) { + bool ok = false; + QPair sc = getScope(tc, &ok); + if (scope) *scope = sc; QString st = tc.selectedText(); - if (arg >= 0) st = scope.second; + if (arg >= 0) st = sc.second; if (!ok || st.isEmpty()) { - hideHelp(); - return; + return ACEntry(); } ok = false; - ACList acl(autoCompletitionList(scope.first, scope.second)); - foreach (const ACPair & i, acl) { - foreach (const StringsPair & s, i.second) { - QString ts = s.second; + ACList acl(autoCompletitionList(sc.first, sc.second)); + foreach (const ACSection & i, acl) { + foreach (const ACEntry & s, i.second) { + QString ts = s.name; //qDebug() << ts << st; if (ts != st) { if (ts.startsWith(st)) { @@ -1063,31 +1127,43 @@ void QCodeEdit::raiseHelp(QTextCursor tc, int arg) { continue; } //qDebug() << s.second << st; - ACClass acc = ac_classes.value(i.first); - lbl_help[0]->setIcon(acc.icon); - lbl_help[0]->setText(QString("[%1] %2 %3").arg(acc.name, s.first, selectArg(s.second, arg))); - ok = true; - break; + if (acc) *acc = ac_classes.value(i.first); + return s; } - if (ok) break; } - if (!ok) { + return ACEntry(); +} + + +void QCodeEdit::raiseHelp(QTextCursor tc, int arg) { + ACClass acc; + QPair scope; + ACEntry e = findEntryOnCursor(tc, arg, &acc, &scope); + if (e.isNull()) { hideHelp(); return; } + lbl_help[lhMain]->setText(QString("[%1] %2 %3").arg(acc.name, e.type, selectArg(e.name, arg))); + lbl_help[lhMain]->setIcon(acc.icon); + QString hint; + if (!e.hint.isEmpty()) + hint = QString("%1").arg(e.hint); + lbl_help[lhHint]->setText(hint); + lbl_help[lhHint]->setHidden(hint.isEmpty()); //qDebug() << "help found" << tc.selectionStart() << tc.selectionEnd(); es_cursor.cursor = tc; applyExtraSelection(); //tc.movePosition(QTextCursor::StartOfWord, QTextCursor::MoveAnchor); - lbl_help[0]->setFont(font()); + lbl_help[lhMain]->setFont(font()); qApp->processEvents(); + widget_help->adjustSize(); widget_help->resize(widget_help->sizeHint()); qApp->processEvents(); QRect whr = ui->textCode->cursorRect(tc); #if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0) - whr.setWidth(ui->textCode->fontMetrics().horizontalAdvance(st)); + whr.setWidth(ui->textCode->fontMetrics().horizontalAdvance(tc.selectedText())); #else - whr.setWidth(ui->textCode->fontMetrics().width(st)); + whr.setWidth(ui->textCode->fontMetrics().width(tc.selectedText())); #endif QPoint whp; whp.setX(whr.left() - whr.width() - (widget_help->width() - whr.width()) / 2); @@ -1149,10 +1225,10 @@ QCodeEdit::ACList QCodeEdit::wordsCompletitionList(const QString & written) cons acwl << stc.selectedText(); } acwl.removeDuplicates(); - ACPair acl; + ACSection acl; acl.first = -1; foreach (const QString & s, acwl) - acl.second << StringsPair("", s); + acl.second << ACEntry("", s); ret << acl; } return ret; @@ -1234,7 +1310,7 @@ void QCodeEdit::invokeAutoCompletition(bool force) { if (word_complet) acl << wordsCompletitionList(scope.second); QFont bf(font()); bf.setBold(true); - foreach (const ACPair & ac, acl) { + foreach (const ACSection & ac, acl) { if (ac.second.isEmpty()) continue; ACClass acc = ac_classes.value(ac.first); completer->addItems(bf, acc, ac); diff --git a/libs/widgets/qcodeedit.h b/libs/widgets/qcodeedit.h index dd8a599..0e0c4c1 100644 --- a/libs/widgets/qcodeedit.h +++ b/libs/widgets/qcodeedit.h @@ -54,7 +54,17 @@ public: Function, Namespace }; - + struct QAD_WIDGETS_EXPORT ACEntry { + ACEntry(const QString & t = QString(), const QString & n = QString(), const QString & h = QString()): + type(t), name(n), hint(h) {declaration_pos = -1; custom_data = 0;} + bool isNull() const {return type.isEmpty() && name.isEmpty();} + QString type; + QString name; + QString hint; + int declaration_pos; + void * custom_data; + }; + QTextCursor textCursor() const; QTextDocument * document() const; void setDocument(QTextDocument * doc); @@ -75,19 +85,22 @@ public: QPlainTextEdit * textEdit() const; - void registerAutoCompletitionClass(int id, ACClassType ac_class, const QString & name, const QIcon & icon = QIcon()) {ac_classes[id] = ACClass(id, ac_class, name, icon);} + void registerAutoCompletitionClass(int id, ACClassType ac_class, const QString & name, const QIcon & icon = QIcon()); bool wordCompletitionEnabled() const {return word_complet;} protected: typedef QPair StringsPair; - typedef QPair > ACPair; - typedef QList ACList; + typedef QPair > ACSection; // section, ACClass.id -> list of entries + typedef QList ACList; // list of sections virtual ACList autoCompletitionList(const QStringList & scope, const QString & written) const {return ACList();} virtual void parse() {} virtual void documentUnset() {} virtual void documentChanged(QTextDocument * d) {} + virtual void linkClicked(ACEntry entry) {} + virtual bool linkValidate(ACEntry e) {return true;} + QString selectArg(QString s, int arg); void raiseHelp(QTextCursor tc, int arg = -1); QTextCursor functionStart(QTextCursor tc, int * arg); @@ -108,30 +121,38 @@ private: QString name; QIcon icon; }; + enum LabelHelpType { + lhMain, + lhHint, + lhF1 + }; QCodeEditCompleter * completer; - IconedLabel * lbl_help[2]; + IconedLabel * lbl_help[3]; QFrame * widget_help; - QTextEdit::ExtraSelection es_line, es_cursor, es_bracket, es_range, es_search; + QTextEdit::ExtraSelection es_line, es_cursor, es_bracket, es_range, es_search, es_link; QList es_selected, es_custom, es_brackets, es_search_list; QMap ac_classes; QStringList cursor_scope; + ACEntry link_entry; int prev_lc, auto_comp_pl, timer, cur_search_ind, pos_press, pos_el_press; bool spaces_, _ignore_focus_out, _first, _destructor, _replacing; bool word_complet; - bool eventFilter(QObject * o, QEvent * e); - void showEvent(QShowEvent * ); - void timerEvent(QTimerEvent * ); - void leaveEvent(QEvent * ); - void changeEvent(QEvent * e); + bool eventFilter(QObject * o, QEvent * e) override; + void showEvent(QShowEvent * ) override; + void timerEvent(QTimerEvent * ) override; + void leaveEvent(QEvent * ) override; + void changeEvent(QEvent * e) override; void highlightBrackets(); void applyExtraSelection(); void clearSearch(); void moveToSearch(); int searchIndFromCursor(); + ACEntry findEntryOnCursor(QTextCursor tc, int arg = -1, ACClass * acc = 0, QPair * scope = 0); private slots: + void _activateLink(QCodeEdit::ACEntry e) {linkClicked(e);} void syncScrolls(); void scrollUp(); void scrollDown(); @@ -144,6 +165,9 @@ private slots: void indent(); void deindent(); void autoIndent(); + void showLink(); + void hideLink(); + void gotoLink(); void invokeAutoCompletition(bool force = false); void commitCompletition(); void searchAll(); @@ -170,7 +194,6 @@ public slots: void searchPrevious(); void hideSearch(); void setWordCompletitionEnabled(bool on) {word_complet = on;} - signals: void textChanged(); diff --git a/libs/widgets/qcodeedit_completer_p.cpp b/libs/widgets/qcodeedit_completer_p.cpp index 7d8a02f..b4b2282 100644 --- a/libs/widgets/qcodeedit_completer_p.cpp +++ b/libs/widgets/qcodeedit_completer_p.cpp @@ -50,7 +50,7 @@ void QCodeEditCompleter::previousCompletition() { } } -void QCodeEditCompleter::addItems(QFont f, const QCodeEdit::ACClass & cl, const QCodeEdit::ACPair & items) { +void QCodeEditCompleter::addItems(QFont f, const QCodeEdit::ACClass & cl, const QCodeEdit::ACSection & items) { QTreeWidgetItem * gi = new QTreeWidgetItem(); gi->setText(0, cl.name); gi->setTextAlignment(0, Qt::AlignCenter); @@ -63,8 +63,8 @@ void QCodeEditCompleter::addItems(QFont f, const QCodeEdit::ACClass & cl, const foreach (const auto & s, items.second) { QTreeWidgetItem * ni = new QTreeWidgetItem(); ni->setIcon(0, cl.icon); - ni->setText(0, s.first); - ni->setText(1, s.second); + ni->setText(0, s.type); + ni->setText(1, s.name); addTopLevelItem(ni); } } diff --git a/libs/widgets/qcodeedit_completer_p.h b/libs/widgets/qcodeedit_completer_p.h index cd464b2..5100125 100644 --- a/libs/widgets/qcodeedit_completer_p.h +++ b/libs/widgets/qcodeedit_completer_p.h @@ -33,7 +33,7 @@ public: void nextCompletition(); void previousCompletition(); - void addItems(QFont f, const QCodeEdit::ACClass & cl, const QCodeEdit::ACPair & items); + void addItems(QFont f, const QCodeEdit::ACClass & cl, const QCodeEdit::ACSection & items); bool isEmpty() const; void invoke(QPoint global_pos);