diff --git a/src/Gui/CMakeLists.txt b/src/Gui/CMakeLists.txt index f559e5c36..9e27c1cc4 100644 --- a/src/Gui/CMakeLists.txt +++ b/src/Gui/CMakeLists.txt @@ -204,6 +204,7 @@ set(Gui_MOC_HDRS DockWindowManager.h DocumentRecovery.h EditorView.h + ExpressionCompleter.h FileDialog.h Flag.h GraphicsViewZoom.h @@ -967,6 +968,7 @@ SET(FreeCADGui_CPP_SRCS DocumentModel.cpp DocumentPyImp.cpp GraphicsViewZoom.cpp + ExpressionCompleter.cpp GuiApplicationNativeEventAware.cpp GuiConsole.cpp Macro.cpp @@ -986,6 +988,7 @@ SET(FreeCADGui_SRCS BitmapFactory.h Document.h DocumentModel.h + ExpressionCompleter.h FreeCADGuiInit.py GraphicsViewZoom.h GuiApplicationNativeEventAware.h diff --git a/src/Gui/ExpressionCompleter.cpp b/src/Gui/ExpressionCompleter.cpp new file mode 100644 index 000000000..66eeb71ff --- /dev/null +++ b/src/Gui/ExpressionCompleter.cpp @@ -0,0 +1,320 @@ +#include "PreCompiled.h" + +#ifndef _PreComp_ +#include +#include +#include +#include +#endif + +#include +#include +#include +#include +#include +#include "ExpressionCompleter.h" + +Q_DECLARE_METATYPE(App::ObjectIdentifier); + +using namespace App; +using namespace Gui; + +/** + * @brief Construct an ExpressionCompleter object. + * @param currentDoc Current document to generate the model from. + * @param currentDocObj Current document object to generate model from. + * @param parent Parent object owning the completer. + */ + +ExpressionCompleter::ExpressionCompleter(const App::Document * currentDoc, const App::DocumentObject * currentDocObj, QObject *parent) + : QCompleter(parent) +{ + QStandardItemModel* model = new QStandardItemModel(this); + + std::vector docs = App::GetApplication().getDocuments(); + std::vector::const_iterator di = docs.begin(); + + std::vector deps = currentDocObj->getInList(); + std::set forbidden; + + for (std::vector::const_iterator it = deps.begin(); it != deps.end(); ++it) + forbidden.insert(*it); + + /* Create tree with full path to all objects */ + while (di != docs.end()) { + QStandardItem* docItem = new QStandardItem(QString::fromAscii((*di)->getName())); + + docItem->setData(QString::fromAscii((*di)->getName()) + QString::fromAscii("#"), Qt::UserRole); + createModelForDocument(*di, docItem, forbidden); + + model->appendRow(docItem); + + ++di; + } + + /* Create branch with current document object */ + + if (currentDocObj) { + createModelForDocument(currentDocObj->getDocument(), model->invisibleRootItem(), forbidden); + createModelForDocumentObject(currentDocObj, model->invisibleRootItem()); + } + else { + if (currentDoc) + createModelForDocument(currentDoc, model->invisibleRootItem(), forbidden); + } + + setModel(model); + + setCaseSensitivity(Qt::CaseInsensitive); +} + +/** + * @brief Create model node given document, the parent and forbidden node. + * @param doc Document + * @param parent Parent item + * @param forbidden Forbidden document objects; typically those that will create a loop in the DAG if used. + */ + +void ExpressionCompleter::createModelForDocument(const App::Document * doc, QStandardItem * parent, + const std::set & forbidden) { + std::vector docObjs = doc->getObjects(); + std::vector::const_iterator doi = docObjs.begin(); + + while (doi != docObjs.end()) { + std::set::const_iterator it = forbidden.find(*doi); + + // Skip? + if (it != forbidden.end()) { + ++doi; + continue; + } + + QStandardItem* docObjItem = new QStandardItem(QString::fromAscii((*doi)->getNameInDocument())); + + docObjItem->setData(QString::fromAscii((*doi)->getNameInDocument()) + QString::fromAscii("."), Qt::UserRole); + createModelForDocumentObject(*doi, docObjItem); + parent->appendRow(docObjItem); + + if (strcmp((*doi)->getNameInDocument(), (*doi)->Label.getValue()) != 0) { + std::string label = (*doi)->Label.getValue(); + + if (!ExpressionParser::isTokenAnIndentifier(label)) + label = quote(label); + + docObjItem = new QStandardItem(QString::fromUtf8(label.c_str())); + + docObjItem->setData( QString::fromUtf8(label.c_str()) + QString::fromAscii("."), Qt::UserRole); + createModelForDocumentObject(*doi, docObjItem); + parent->appendRow(docObjItem); + } + + ++doi; + } +} + +/** + * @brief Create model nodes for document object + * @param docObj Document object + * @param parent Parent item + */ + +void ExpressionCompleter::createModelForDocumentObject(const DocumentObject * docObj, QStandardItem * parent) +{ + std::vector props; + docObj->getPropertyList(props); + + std::vector::const_iterator pi = props.begin(); + while (pi != props.end()) { + createModelForPaths(*pi, parent); + ++pi; + } +} + +/** + * @brief Create nodes for a property. + * @param prop + * @param docObjItem + */ + +void ExpressionCompleter::createModelForPaths(const App::Property * prop, QStandardItem *docObjItem) +{ + std::vector paths; + std::vector::const_iterator ppi; + + prop->getPaths(paths); + + for (ppi = paths.begin(); ppi != paths.end(); ++ppi) { + QStandardItem* pathItem = new QStandardItem(Base::Tools::fromStdString(ppi->toString())); + + QVariant value; + + value.setValue(*ppi); + pathItem->setData(value, Qt::UserRole); + + docObjItem->appendRow(pathItem); + } +} + +QString ExpressionCompleter::pathFromIndex ( const QModelIndex & index ) const +{ + QStandardItemModel * m = static_cast(model()); + + if (m->data(index, Qt::UserRole).canConvert()) { + App::ObjectIdentifier p = m->data(index, Qt::UserRole).value(); + QString pStr = Base::Tools::fromStdString(p.toString()); + + QString parentStr; + QModelIndex parent = index.parent(); + while (parent.isValid()) { + QString thisParentStr = m->data(parent, Qt::UserRole).toString(); + + parentStr = thisParentStr + parentStr; + + parent = parent.parent(); + } + + return parentStr + pStr; + } + else if (m->data(index, Qt::UserRole).canConvert()) { + QModelIndex parent = index; + QString parentStr; + + while (parent.isValid()) { + QString thisParentStr = m->data(parent, Qt::UserRole).toString(); + + parentStr = thisParentStr + parentStr; + + parent = parent.parent(); + } + + return parentStr; + } + else + return QString(); +} + +QStringList ExpressionCompleter::splitPath ( const QString & path ) const +{ + try { + App::ObjectIdentifier p = ObjectIdentifier::parse(0, path.toUtf8().constData()); + QStringList l; + + if (p.getProperty()) { + for (int i = 0; i < p.numComponents(); ++i) + l << Base::Tools::fromStdString(p.getPropertyComponent(i).toString()); + return l; + } + else { + std::vector sl = p.getStringList(); + std::vector::const_iterator sli = sl.begin(); + + while (sli != sl.end()) { + l << Base::Tools::fromStdString(*sli); + ++sli; + } + + return l; + } + } + catch (const Base::Exception &) { + return QStringList() << path; + } +} + +// Code below inspired by blog entry: +// https://john.nachtimwald.com/2009/07/04/qcompleter-and-comma-separated-tags/ + +void ExpressionCompleter::slotUpdate(const QString & prefix) +{ + using namespace boost::tuples; + std::vector > tokens = ExpressionParser::tokenize(Base::Tools::toStdString(prefix)); + std::string completionPrefix; + + if (tokens.size() == 0 || (prefix.size() > 0 && prefix[prefix.size() - 1] == QChar(32))) { + if (popup()) + popup()->setVisible(false); + return; + } + + std::size_t i = tokens.size(); + + do { + --i; + if (!(get<0>(tokens[i]) == ExpressionParser::IDENTIFIER || + get<0>(tokens[i]) == ExpressionParser::STRING || + get<0>(tokens[i]) == '.')) + break; + } while (i > 0); + + prefixStart = get<1>(tokens[i]); + while (i < tokens.size()) { + completionPrefix += get<2>(tokens[i]); + ++i; + } + + setCompletionPrefix(Base::Tools::fromStdString(completionPrefix)); + + if (!completionPrefix.empty() && widget()->hasFocus()) + complete(); + else { + if (popup()) + popup()->setVisible(false); + } +} + +ExpressionLineEdit::ExpressionLineEdit(QWidget *parent) + : QLineEdit(parent) + , completer(0) + , block(false) +{ + connect(this, SIGNAL(textChanged(const QString&)), this, SLOT(slotTextChanged(const QString&))); +} + +void ExpressionLineEdit::setDocumentObject(const App::DocumentObject * currentDocObj) +{ + if (completer) { + delete completer; + completer = 0; + } + + if (currentDocObj != 0) { + completer = new ExpressionCompleter(currentDocObj->getDocument(), currentDocObj, this); + 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))); + } +} + +bool ExpressionLineEdit::completerActive() const +{ + return completer && completer->popup() && completer->popup()->isVisible(); +} + +void ExpressionLineEdit::hideCompleter() +{ + if (completer && completer->popup()) + completer->popup()->setVisible(false); +} + +void ExpressionLineEdit::slotTextChanged(const QString & text) +{ + if (!block) { + Q_EMIT textChanged2(text.left(cursorPosition())); + } +} + +void ExpressionLineEdit::slotCompleteText(const QString & completionPrefix) +{ + int start = completer->getPrefixStart(); + QString before(text().left(start)); + QString after(text().mid(cursorPosition())); + + block = true; + setText(before + completionPrefix + after); + setCursorPosition(QString(before + completionPrefix).length()); + block = false; +} + +#include "moc_ExpressionCompleter.cpp" diff --git a/src/Gui/ExpressionCompleter.h b/src/Gui/ExpressionCompleter.h new file mode 100644 index 000000000..e5911d137 --- /dev/null +++ b/src/Gui/ExpressionCompleter.h @@ -0,0 +1,66 @@ +#ifndef EXPRESSIONCOMPLETER_H +#define EXPRESSIONCOMPLETER_H + +#include +#include +#include +#include + +class QStandardItem; + +namespace App { +class Document; +class DocumentObject; +class Property; +class ObjectIdentifier; +} + +namespace Gui { + +/** + * @brief The ExpressionCompleter class extends the QCompleter class to provide a completer model of documentobject names and properties. + */ + +class GuiExport ExpressionCompleter : public QCompleter +{ + Q_OBJECT +public: + ExpressionCompleter(const App::Document * currentDoc, const App::DocumentObject * currentDocObj, QObject *parent = 0); + + int getPrefixStart() const { return prefixStart; } + +public Q_SLOTS: + void slotUpdate(const QString &prefix); + +private: + void createModelForDocument(const App::Document * doc, QStandardItem * parent, const std::set &forbidden); + void createModelForDocumentObject(const App::DocumentObject * docObj, QStandardItem * parent); + void createModelForPaths(const App::Property * prop, QStandardItem *docObjItem); + + virtual QString pathFromIndex ( const QModelIndex & index ) const; + virtual QStringList splitPath ( const QString & path ) const; + + int prefixStart; + +}; + +class GuiExport ExpressionLineEdit : public QLineEdit { + Q_OBJECT +public: + ExpressionLineEdit(QWidget *parent = 0); + void setDocumentObject(const App::DocumentObject *currentDocObj); + bool completerActive() const; + void hideCompleter(); +Q_SIGNALS: + void textChanged2(QString text); +public Q_SLOTS: + void slotTextChanged(const QString & text); + void slotCompleteText(const QString & completionPrefix); +private: + ExpressionCompleter * completer; + bool block; +}; + +} + +#endif // EXPRESSIONCOMPLETER_H