From 782913407ac0136643fa633d4e33a4b3fef18a2a Mon Sep 17 00:00:00 2001 From: "Zheng, Lei" Date: Thu, 22 Aug 2019 18:39:18 +0800 Subject: [PATCH] Gui: improve expression completer Proper support of completing an edit in the middel of an expression. Also support 'noProperty' mode in the completer, where no completion is offered for property names. This will be used by tree view object search. --- src/Gui/ExpressionCompleter.cpp | 188 +++++++++++++++++++++++--------- src/Gui/ExpressionCompleter.h | 28 +++-- 2 files changed, 159 insertions(+), 57 deletions(-) diff --git a/src/Gui/ExpressionCompleter.cpp b/src/Gui/ExpressionCompleter.cpp index 26b3d82772..5f16907b1e 100644 --- a/src/Gui/ExpressionCompleter.cpp +++ b/src/Gui/ExpressionCompleter.cpp @@ -30,14 +30,26 @@ using namespace Gui; class ExpressionCompleterModel: public QAbstractItemModel { public: - ExpressionCompleterModel(QObject *parent, const App::DocumentObject *obj) - :QAbstractItemModel(parent) + ExpressionCompleterModel(QObject *parent, const App::DocumentObject *obj, bool noProperty) + :QAbstractItemModel(parent), noProperty(noProperty) { + setDocumentObject(obj); + } + + void setDocumentObject(const App::DocumentObject *obj) { + beginResetModel(); if(obj) { currentDoc = obj->getDocument()->getName(); currentObj = obj->getNameInDocument(); - inList = obj->getInListEx(true); + if(!noProperty) + inList = obj->getInListEx(true); + } else { + currentDoc.clear(); + currentObj.clear(); + inList.clear(); } + endResetModel(); + } // This ExpressionCompleter model works without any pysical items. @@ -139,7 +151,7 @@ public: obj = objs[idx/2]; if(inList.count(obj)) return; - } else { + } else if (!noProperty) { auto cobj = doc->getObject(currentObj.c_str()); if(cobj) { idx -= objSize; @@ -168,9 +180,9 @@ public: res = QString::fromUtf8(quote(obj->Label.getStrValue()).c_str()); else res = QString::fromLatin1(obj->getNameInDocument()); - if(sep) + if(sep && !noProperty) res += QLatin1Char('.'); - }else { + } else { if(idx & 1) res = QString::fromUtf8(quote(doc->Label.getStrValue()).c_str()); else @@ -199,7 +211,7 @@ public: res = QString::fromUtf8(quote(obj->Label.getStrValue()).c_str()); else res = QString::fromLatin1(obj->getNameInDocument()); - if(sep) + if(sep && !noProperty) res += QLatin1Char('.'); v->setValue(res); } @@ -207,6 +219,8 @@ public: } } + if(noProperty) + return; if(!prop) { idx = row; obj->getPropertyList(props); @@ -217,8 +231,13 @@ public: if(count) *count = propSize; } - if(v) - *v = QString::fromLatin1(prop->getName()); + if(v) { + QString res; + if(sep) + res = QLatin1String("."); + res += QString::fromLatin1(prop->getName()); + *v = res; + } return; } @@ -288,6 +307,7 @@ private: std::set inList; std::string currentDoc; std::string currentObj; + bool noProperty; }; /** @@ -297,8 +317,9 @@ private: * @param parent Parent object owning the completer. */ -ExpressionCompleter::ExpressionCompleter(const App::DocumentObject * currentDocObj, QObject *parent) - : QCompleter(parent), prefixStart(0), currentObj(currentDocObj) +ExpressionCompleter::ExpressionCompleter(const App::DocumentObject * currentDocObj, + QObject *parent, bool noProperty) + : QCompleter(parent), currentObj(currentDocObj), noProperty(noProperty) { setCaseSensitivity(Qt::CaseInsensitive); } @@ -307,9 +328,19 @@ void ExpressionCompleter::init() { if(model()) return; - setModel(new ExpressionCompleterModel(this,currentObj.getObject())); + setModel(new ExpressionCompleterModel(this,currentObj.getObject(),noProperty)); } +void ExpressionCompleter::setDocumentObject(const App::DocumentObject *obj) { + if(!obj || !obj->getNameInDocument()) + currentObj = App::DocumentObjectT(); + else + currentObj = obj; + setCompletionPrefix(QString()); + auto m = model(); + if(m) + static_cast(m)->setDocumentObject(obj); +} QString ExpressionCompleter::pathFromIndex ( const QModelIndex & index ) const { @@ -385,7 +416,7 @@ QStringList ExpressionCompleter::splitPath ( const QString & input ) const // Code below inspired by blog entry: // https://john.nachtimwald.com/2009/07/04/qcompleter-and-comma-separated-tags/ -void ExpressionCompleter::slotUpdate(const QString & prefix) +void ExpressionCompleter::slotUpdate(const QString & prefix, int pos) { init(); @@ -395,34 +426,84 @@ void ExpressionCompleter::slotUpdate(const QString & prefix) // Compute start; if prefix starts with =, start parsing from offset 1. int start = (prefix.size() > 0 && prefix.at(0) == QChar::fromLatin1('=')) ? 1 : 0; - // Tokenize prefix - std::vector > tokens = ExpressionParser::tokenize(Base::Tools::toStdString(prefix.mid(start))); + std::string expression = Base::Tools::toStdString(prefix.mid(start)); - // No tokens, or last char is a space? - if (tokens.size() == 0 || (prefix.size() > 0 && prefix[prefix.size() - 1] == QChar(32))) { + // Tokenize prefix + std::vector > tokens = ExpressionParser::tokenize(expression); + + // No tokens + if (tokens.size() == 0) { if (popup()) popup()->setVisible(false); return; } - // Extract last tokens that can be rebuilt to a variable - ssize_t i = static_cast(tokens.size()) - 1; - while (i >= 0) { - if (get<0>(tokens[i]) != ExpressionParser::IDENTIFIER && - get<0>(tokens[i]) != ExpressionParser::STRING && - get<0>(tokens[i]) != ExpressionParser::UNIT && - get<0>(tokens[i]) != '.') + prefixEnd = prefix.size(); + + // Pop those trailing tokens depending on the given position, which may be + // in the middle of a token, and we shall include that token. + for(auto it=tokens.begin();it!=tokens.end();++it) { + if(get<1>(*it) >= pos) { + // Include the immediatly followed '.' or '#', because we'll be + // inserting these separater too, in ExpressionCompleteModel::pathFromIndex() + if(it!=tokens.begin() && get<0>(*it)!='.' && get<0>(*it)!='#') + it = it-1; + tokens.resize(it-tokens.begin()+1); + prefixEnd = start + get<1>(*it) + (int)get<2>(*it).size(); break; - --i; + } } - ++i; + int trim = 0; + if(prefixEnd > pos) + trim = prefixEnd - pos; + + // Extract last tokens that can be rebuilt to a variable + ssize_t i = static_cast(tokens.size()) - 1; + + // First, check if we have unclosing string starting from the end + bool stringing = false; + for(; i>=0; --i) { + int token = get<0>(tokens[i]); + if(token == ExpressionParser::STRING) { + stringing = false; + break; + } + if(token==ExpressionParser::LT + && i && get<0>(tokens[i-1])==ExpressionParser::LT) + { + --i; + stringing = true; + break; + } + } + + // Not an unclosed string and the last character is a space + if(!stringing && prefix.size() && prefix[prefixEnd-1] == QChar(32)) { + if (popup()) + popup()->setVisible(false); + return; + } + + if(!stringing) { + i = static_cast(tokens.size()) - 1; + for(;i>=0;--i) { + int token = get<0>(tokens[i]); + if (token != '.' && token != '#' && + token != ExpressionParser::IDENTIFIER && + token != ExpressionParser::STRING && + token != ExpressionParser::UNIT) + break; + } + ++i; + + } // Set prefix start for use when replacing later if (i == static_cast(tokens.size())) - prefixStart = prefix.size(); + prefixStart = prefixEnd; else - prefixStart = (prefix.at(0) == QChar::fromLatin1('=') ? 1 : 0) + get<1>(tokens[i]); + prefixStart = start + get<1>(tokens[i]); // Build prefix from tokens while (i < static_cast(tokens.size())) { @@ -430,6 +511,9 @@ void ExpressionCompleter::slotUpdate(const QString & prefix) ++i; } + if(trim && trim<(int)completionPrefix.size() ) + completionPrefix.resize(completionPrefix.size()-trim); + // Set completion prefix setCompletionPrefix(Base::Tools::fromStdString(completionPrefix)); @@ -441,10 +525,11 @@ void ExpressionCompleter::slotUpdate(const QString & prefix) } } -ExpressionLineEdit::ExpressionLineEdit(QWidget *parent) +ExpressionLineEdit::ExpressionLineEdit(QWidget *parent, bool noProperty) : QLineEdit(parent) , completer(0) , block(true) + , noProperty(noProperty) { connect(this, SIGNAL(textChanged(const QString&)), this, SLOT(slotTextChanged(const QString&))); } @@ -452,17 +537,16 @@ ExpressionLineEdit::ExpressionLineEdit(QWidget *parent) void ExpressionLineEdit::setDocumentObject(const App::DocumentObject * currentDocObj) { if (completer) { - delete completer; - completer = 0; + completer->setDocumentObject(currentDocObj); + return; } - if (currentDocObj != 0) { - completer = new ExpressionCompleter(currentDocObj, this); + completer = new ExpressionCompleter(currentDocObj, this, noProperty); completer->setWidget(this); completer->setCaseSensitivity(Qt::CaseInsensitive); connect(completer, SIGNAL(activated(QString)), this, SLOT(slotCompleteText(QString))); connect(completer, SIGNAL(highlighted(QString)), this, SLOT(slotCompleteText(QString))); - connect(this, SIGNAL(textChanged2(QString)), completer, SLOT(slotUpdate(QString))); + connect(this, SIGNAL(textChanged2(QString,int)), completer, SLOT(slotUpdate(QString,int))); } } @@ -480,19 +564,22 @@ void ExpressionLineEdit::hideCompleter() void ExpressionLineEdit::slotTextChanged(const QString & text) { if (!block) { - Q_EMIT textChanged2(text.left(cursorPosition())); + Q_EMIT textChanged2(text,cursorPosition()); } } void ExpressionLineEdit::slotCompleteText(const QString & completionPrefix) { - int start = completer->getPrefixStart(); + int start,end; + completer->getPrefixRange(start,end); QString before(text().left(start)); - QString after(text().mid(cursorPosition())); + QString after(text().mid(end)); Base::FlagToggler flag(block,false); - setText(before + completionPrefix + after); - setCursorPosition(QString(before + completionPrefix).length()); + before += completionPrefix; + setText(before + after); + setCursorPosition(before.length()); + completer->updatePrefixEnd(before.length()); } void ExpressionLineEdit::keyPressEvent(QKeyEvent *e) { @@ -514,8 +601,8 @@ ExpressionTextEdit::ExpressionTextEdit(QWidget *parent) void ExpressionTextEdit::setDocumentObject(const App::DocumentObject * currentDocObj) { if (completer) { - delete completer; - completer = 0; + completer->setDocumentObject(currentDocObj); + return; } if (currentDocObj != 0) { @@ -524,7 +611,7 @@ void ExpressionTextEdit::setDocumentObject(const App::DocumentObject * currentDo completer->setCaseSensitivity(Qt::CaseInsensitive); connect(completer, SIGNAL(activated(QString)), this, SLOT(slotCompleteText(QString))); connect(completer, SIGNAL(highlighted(QString)), this, SLOT(slotCompleteText(QString))); - connect(this, SIGNAL(textChanged2(QString)), completer, SLOT(slotUpdate(QString))); + connect(this, SIGNAL(textChanged2(QString,int)), completer, SLOT(slotUpdate(QString,int))); } } @@ -543,21 +630,22 @@ void ExpressionTextEdit::slotTextChanged() { if (!block) { QTextCursor cursor = textCursor(); - Q_EMIT textChanged2(cursor.block().text().left(cursor.positionInBlock())); + Q_EMIT textChanged2(cursor.block().text(),cursor.positionInBlock()); } } void ExpressionTextEdit::slotCompleteText(const QString & completionPrefix) { QTextCursor cursor = textCursor(); - int start = completer->getPrefixStart(); + int start,end; + completer->getPrefixRange(start,end); int pos = cursor.positionInBlock(); - if(pos>=start) { - Base::FlagToggler flag(block,false); - if(pos>start) - cursor.movePosition(QTextCursor::PreviousCharacter,QTextCursor::KeepAnchor,pos-start); - cursor.insertText(completionPrefix); - } + if(pos flag(block,false); + cursor.insertText(completionPrefix); + completer->updatePrefixEnd(cursor.positionInBlock()); } void ExpressionTextEdit::keyPressEvent(QKeyEvent *e) { diff --git a/src/Gui/ExpressionCompleter.h b/src/Gui/ExpressionCompleter.h index 77ed9ecbe2..7370f16d7b 100644 --- a/src/Gui/ExpressionCompleter.h +++ b/src/Gui/ExpressionCompleter.h @@ -28,32 +28,45 @@ class GuiExport ExpressionCompleter : public QCompleter { Q_OBJECT public: - ExpressionCompleter(const App::DocumentObject * currentDocObj, QObject *parent = 0); + ExpressionCompleter(const App::DocumentObject * currentDocObj, + QObject *parent = 0, bool noProperty = false); - int getPrefixStart() const { return prefixStart; } + void getPrefixRange(int &start, int &end) const { + start = prefixStart; + end = prefixEnd; + } + + void updatePrefixEnd(int end) { + prefixEnd = end; + } + + void setDocumentObject(const App::DocumentObject*); public Q_SLOTS: - void slotUpdate(const QString &prefix); + void slotUpdate(const QString &prefix, int pos); private: void init(); virtual QString pathFromIndex ( const QModelIndex & index ) const; virtual QStringList splitPath ( const QString & path ) const; - int prefixStart; + int prefixStart = 0; + int prefixEnd = 0; + App::DocumentObjectT currentObj; + bool noProperty; }; class GuiExport ExpressionLineEdit : public QLineEdit { Q_OBJECT public: - ExpressionLineEdit(QWidget *parent = 0); + ExpressionLineEdit(QWidget *parent = 0, bool noProperty=false); void setDocumentObject(const App::DocumentObject *currentDocObj); bool completerActive() const; void hideCompleter(); Q_SIGNALS: - void textChanged2(QString text); + void textChanged2(QString text, int pos); public Q_SLOTS: void slotTextChanged(const QString & text); void slotCompleteText(const QString & completionPrefix); @@ -62,6 +75,7 @@ protected: private: ExpressionCompleter * completer; bool block; + bool noProperty; }; class GuiExport ExpressionTextEdit : public QPlainTextEdit { @@ -74,7 +88,7 @@ public: protected: void keyPressEvent(QKeyEvent * event); Q_SIGNALS: - void textChanged2(QString text); + void textChanged2(QString text, int pos); public Q_SLOTS: void slotTextChanged(); void slotCompleteText(const QString & completionPrefix);