/*************************************************************************** * Copyright (c) 2015 Eivind Kvedalen * * * * This file is part of the FreeCAD CAx development system. * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Library General Public * * License as published by the Free Software Foundation; either * * version 2 of the License, or (at your option) any later version. * * * * This library is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU Library General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this library; see the file COPYING.LIB. If not, * * write to the Free Software Foundation, Inc., 59 Temple Place, * * Suite 330, Boston, MA 02111-1307, USA * * * ***************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ExpressionCompleter.h" FC_LOG_LEVEL_INIT("Completer", true, true) Q_DECLARE_METATYPE(App::ObjectIdentifier) using namespace App; using namespace Gui; class ExpressionCompleterModel: public QAbstractItemModel { public: ExpressionCompleterModel(QObject* parent, bool noProperty) : QAbstractItemModel(parent) , noProperty(noProperty) {} void setNoProperty(bool enabled) { noProperty = enabled; } void setDocumentObject(const App::DocumentObject* obj, bool checkInList) { beginResetModel(); if (obj) { currentDoc = obj->getDocument()->getName(); currentObj = obj->getNameInDocument(); if (!noProperty && checkInList) { inList = obj->getInListEx(true); } } else { currentDoc.clear(); currentObj.clear(); inList.clear(); } endResetModel(); } // clang-format off // This ExpressionCompleter model works without any physical items. // Everything item related is stored inside QModelIndex.InternalPointer/InternalId(), // using the following Info structure. // // The Info contains two indices, one for document and the other for object. // For 32-bit system, the index is 16bit which limits the size to 64K. For // 64-bit system, the index is 32bit. // // The "virtual" items are organized as a tree. The root items are special, // which consists of three types in the following order, // // * Document, even index contains item using document's name, while // odd index with quoted document label. // * Objects of the current document, even index with object's internal // name, and odd index with quoted object label. // * Properties of the current object. // // Document item contains object item as child, and object item contains // property item. // // The QModelIndex of a root item has both the doc field and obj field set // to -1, and uses the row as the item index. We can figure out the type of // the item solely based on this row index. // // QModelIndex of a non-root object item has doc field as the document // index, and obj field set to -1. // // QModelIndex of a non-root property item has doc field as the document // index, and obj field as the object index. // // An item is uniquely identified by the pair (row, father_link) in the QModelIndex // // The completion tree structure created takes into account the current document and object // // It is done as such, in order to have contextual completion (prop -> object -> files): // * root (-1,-1) // | // |----- documents // |----- current documents' objects [externally set] // |----- current objects' props [externally set] // // This complicates the decoding schema for the root, where the childcount will be // doc.size() + current_doc.Objects.size() + current_obj.Props.size(). // // This is reflected in the complexity of the DATA function. // // Example encoding of a QMODEL Index // // ROOT (row -1, [-1,-1,-1,0]), info represented as [-1,-1,-1,0] // |-- doc 1 (non contextual) - (row 0, [-1,-1,-1,0]) = encode as parent => [0,-1,-1,0] // |-- doc 2 (non contextual) - (row 1, [-1,-1,-1,0]) = encode as parent => [1,-1,-1,0] // | |- doc 2.obj1 - (row 0, [1,-1,-1,0]) = encode as parent => [1, 0,-1,0] // | |- doc 2.obj2 - (row 1, [1,-1,-1,0]) = encode as parent => [1, 1,-1,0] // | |- doc 2.obj3 - (row 2, [1,-1,-1,0]) = encode as parent => [1, 2,-1,0] // | |- doc 2.obj3.prop1 - (row 0, [1, 2,-1,0]) = encode as parent => [1, 2, 0,0] // | |- doc 2.obj3.prop2 - (row 1, [1, 2,-1,0]) = encode as parent => [1, 2, 1,0] // | |- doc 2.obj3.prop3 - (row 2, [1, 2,-1,0]) = encode as parent => [1, 2, 2,0] // | |- doc 2.obj3.prop3.path0 - (row 0, [1, 2, 2,0]) = encode as parent => INVALID, LEAF ITEM // | |- doc 2.obj3.prop3.path1 - (row 1, [1, 2, 2,0]) = encode as parent => INVALID, LEAF ITEM // | // | // |-- doc 3 (non contextual) - (row 2, [-1,-1,-1,0]) = encode as parent => [2,-1,-1,0] // | // |-- obj1 (current doc - contextual) - (row 3, [-1,-1,-1,0]) = encode as parent => [3,-1,-1,1] // |-- obj2 (current doc - contextual) - (row 4, [-1,-1,-1,0]) = encode as parent => [4,-1,-1,1] // | |- obj2.prop1 (contextual) - (row 0, [4,-1,-1,1]) = encode as parent => [4,-1,0,1] // | |- obj2.prop2 (contextual) - (row 1, [4,-1,-1,1]) = encode as parent => [4,-1,1,1] // | | - obj2.prop2.path1 (contextual) - (row 0, [4,-1,0 ,1]) = encode as parent => INVALID, LEAF ITEM // | | - obj2.prop2.path2 (contextual) - (row 1, [4,-1,1 ,1]) = encode as parent => INVALID, LEAF ITEM // | // |-- prop1 (current obj - contextual) - (row 5, [-1,-1,-1,0]) = encode as parent => [5,-1,-1,1] // |-- prop2 (current obj - contextual) - (row 6, [-1,-1,-1,0]) = encode as parent => [6,-1,-1,1] // |-- prop2.path1 (contextual) - (row 0, [ 6,-1,-1,0]) = encode as parent => INVALID, LEAF ITEM // |-- prop2.path2 (contextual) - (row 1, [ 6,-1,-1,0]) = encode as parent => INVALID, LEAF ITEM // // clang-format on struct Info { qint32 doc; qint32 obj; qint32 prop; quint32 contextualHierarchy : 1; static const Info root; }; static const quint64 k_numBitsProp = 16ULL; // 0 .. 15 static const quint64 k_numBitsObj = 24ULL; // 16.. 39 static const quint64 k_numBitsContextualHierarchy = 1; // 40 static const quint64 k_numBitsDocuments = 23ULL; // 41.. 63 static const quint64 k_offsetProp = 0; static const quint64 k_offsetObj = k_offsetProp + k_numBitsProp; static const quint64 k_offsetContextualHierarchy = k_offsetObj + k_numBitsObj; static const quint64 k_offsetDocuments = k_offsetContextualHierarchy + k_numBitsContextualHierarchy; static const quint64 k_maskProp = ((1ULL << k_numBitsProp) - 1); static const quint64 k_maskObj = ((1ULL << k_numBitsObj) - 1); static const quint64 k_maskContextualHierarchy = ((1ULL << k_numBitsContextualHierarchy) - 1); static const quint64 k_maskDocuments = ((1ULL << k_numBitsDocuments) - 1); union InfoPtrEncoding { quint64 d_enc; struct { quint8 doc; quint8 prop; quint16 obj : 15; quint16 contextualHierarchy : 1; } d32; void* ptr; InfoPtrEncoding(const Info& info) : d_enc(0) { if (sizeof(void*) < sizeof(InfoPtrEncoding)) { d32.doc = (quint8)(info.doc + 1); d32.obj = (quint16)(info.obj + 1); d32.prop = (quint8)(info.prop + 1); d32.contextualHierarchy = info.contextualHierarchy; } else { d_enc = ((quint64(info.doc + 1) & k_maskDocuments) << k_offsetDocuments) | ((quint64(info.contextualHierarchy) & k_maskContextualHierarchy) << k_offsetContextualHierarchy) | ((quint64(info.obj + 1) & k_maskObj) << k_offsetObj) | ((quint64(info.prop + 1) & k_maskProp) << k_offsetProp); } } InfoPtrEncoding(void* pointer) : d_enc(0) { this->ptr = pointer; } Info DecodeInfo() { Info info; if (sizeof(void*) < sizeof(InfoPtrEncoding)) { info.doc = qint32(d32.doc) - 1; info.obj = qint32(d32.obj) - 1; info.prop = qint32(d32.prop) - 1; info.contextualHierarchy = d32.contextualHierarchy; } else { info.doc = ((d_enc >> k_offsetDocuments) & k_maskDocuments) - 1; info.contextualHierarchy = ((d_enc >> k_offsetContextualHierarchy) & k_maskContextualHierarchy); info.obj = ((d_enc >> k_offsetObj) & k_maskObj) - 1; info.prop = ((d_enc >> k_offsetProp) & k_maskProp) - 1; } return info; } }; static void* infoId(const Info& info) { InfoPtrEncoding ptrEnc(info); return ptrEnc.ptr; } static Info getInfo(const QModelIndex& index) { InfoPtrEncoding enc(index.internalPointer()); return enc.DecodeInfo(); } QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override { if (role != Qt::EditRole && role != Qt::DisplayRole && role != Qt::UserRole) { return {}; } QVariant variant; Info info = getInfo(index); _data(info, index.row(), &variant, nullptr, role == Qt::UserRole); FC_TRACE( info.doc << "," << info.obj << "," << info.prop << "," << info.contextualHierarchy << "," << index.row() << ": " << variant.toString().toUtf8().constData() ); return variant; } static std::vector retrieveSubPaths(const App::Property* prop) { std::vector result; if (prop) { prop->getPaths(result); // need to filter out irrelevant paths (len 1, aka just this object identifier) auto res = std::remove_if( result.begin(), result.end(), [](const App::ObjectIdentifier& path) -> bool { return path.getComponents().empty(); } ); result.erase(res, result.end()); } return result; } // Store named object property list in cache for performance purposes, // to avoid building it again for each requested index std::vector>& getCachedPropertyNamedList( DocumentObject* obj ) const { if (!this->namedPropsCache.contains(obj)) { this->namedPropsCache[obj]; obj->getPropertyNamedList(this->namedPropsCache[obj]); } return this->namedPropsCache[obj]; } // The completion tree structure created takes into account the current document and object // // It is done as such: // * root (-1,-1) // | // |----- documents // |----- current documents' objects [externally set] // |----- current objects' props [externally set] // // This complicates the decoding schema for the root, where the childcount will be // doc.size() + current_doc.Objects.size() + current_obj.Props.size(). // // this function is called in two modes: // - obtain the count of a node identified by Info,row => count != nullptr, v==nullptr // - get the text of an item. This text will contain separators but NO full path void _data(const Info& info, int row, QVariant* v, int* count, bool sep = false) const { int idx; // identify the document index. For any children of the root, it is given by traversing // the flat list and identified by [row] idx = info.doc < 0 ? row : info.doc; const auto& docs = App::GetApplication().getDocuments(); int docSize = (int)docs.size() * 2; int objSize = 0; int propSize = 0; App::Document* doc = nullptr; App::DocumentObject* obj = nullptr; const char* propName = nullptr; App::Property* prop = nullptr; // check if the document is uniquely identified: either the correct index in info.doc // OR if, the node is a descendant of the root, its row lands within 0...docsize if (idx >= 0 && idx < docSize) { doc = docs[idx / 2]; } else { // if we're looking at the ROOT, or the row identifies one of the other ROOT elements // |----- current documents' objects, rows: docs.size ... docs.size + // objs.size // |----- current objects' props, rows: docs.size + objs.size ... docs.size + // objs.size+ props.size // // We need to process the ROOT so we get the correct count for its children doc = App::GetApplication().getDocument(currentDoc.c_str()); if (!doc) { // no current, there are no additional objects return; } // move to the current documents' objects' range idx -= docSize; if (info.doc < 0) { row = idx; } const auto& objs = doc->getObjects(); objSize = (int)objs.size() * 2; // if this is a valid object, we found our object and break. // if not, this may be the root or one of current object's properties if (idx >= 0 && idx < objSize) { obj = objs[idx / 2]; // if they are in the ignore list skip if (inList.contains(obj)) { return; } } else if (!noProperty) { // need to check the current object's props range, or we're parsing the ROOT auto cobj = doc->getObject(currentObj.c_str()); if (cobj) { // move to the props range of the current object idx -= objSize; if (info.doc < 0) { row = idx; } // get the properties auto& props = this->getCachedPropertyNamedList(cobj); propSize = (int)props.size(); // if this is an invalid index, bail out // if it's the ROOT break! if (idx >= propSize) { return; } if (idx >= 0) { obj = cobj; // we only set the active object if we're not processing the // root. propName = props[idx].first; prop = props[idx].second; } } } } // the item is the ROOT or a CHILD of the root if (info.doc < 0) { // and we're asking for a count, compute it if (count) { // note that if we're dealing with a valid DOC node (row>0, ROOT_info) // objSize and propSize will be zero because of the early exit above *count = docSize + objSize + propSize; } if (idx >= 0 && v) { // we're asking for this child's data, and IT's NOT THE ROOT QString res; // we resolved the property if (propName) { res = QString::fromLatin1(propName); // resolve the property if (sep && !noProperty && !retrieveSubPaths(prop).empty()) { res += QLatin1Char('.'); } } else if (obj) { // the object has been resolved, use the saved idx to figure out quotation or // not. if (idx & 1) { res = QString::fromUtf8(quote(obj->Label.getStrValue()).c_str()); } else { res = QString::fromLatin1(obj->getNameInDocument()); } if (sep && !noProperty) { res += QLatin1Char('.'); } } else { // the document has been resolved, use the saved idx to figure out quotation or // not. if (idx & 1) { res = QString::fromUtf8(quote(doc->Label.getStrValue()).c_str()); } else { res = QString::fromUtf8(doc->getName()); } if (sep) { res += QLatin1Char('#'); } } v->setValue(res); } // done processing the ROOT or any child items return; } // object not resolved if (!obj) { // are we pointing to an object item, or our father (info) is an object idx = info.obj < 0 ? row : info.obj; const auto& objs = doc->getObjects(); objSize = (int)objs.size() * 2; // if invalid index, or in the ignore list bail out if (idx < 0 || idx >= objSize || inList.contains(obj)) { return; } obj = objs[idx / 2]; if (info.obj < 0) { // if this is AN actual Object item and not a root if (count) { *count = objSize; // set the correct count if requested } if (v) { // resolve the name QString res; if (idx & 1) { res = QString::fromUtf8(quote(obj->Label.getStrValue()).c_str()); } else { res = QString::fromLatin1(obj->getNameInDocument()); } if (sep && !noProperty) { res += QLatin1Char('.'); } v->setValue(res); } return; } } if (noProperty) { return; } if (!propName) { idx = info.prop < 0 ? row : info.prop; auto& props = this->getCachedPropertyNamedList(obj); propSize = (int)props.size(); // return if the property is invalid if (idx < 0 || idx >= propSize) { return; } propName = props[idx].first; prop = props[idx].second; // if this is a root object item if (info.prop < 0) { // set the property size count if (count) { *count = propSize; } if (v) { QString res = QString::fromLatin1(propName); // check to see if we have accessible paths from this prop name? if (sep && !retrieveSubPaths(prop).empty()) { res += QLatin1Char('.'); } *v = res; } return; } } // resolve paths if (prop) { // idx identifies the path idx = row; std::vector paths = retrieveSubPaths(prop); if (count) { *count = paths.size(); } // check to see if this is a valid path if (idx < 0 || idx >= static_cast(paths.size())) { return; } if (v) { auto str = paths[idx].getSubPathStr(); if (str.size() && (str[0] == '.' || str[0] == '#')) { // skip the "." *v = QString::fromLatin1(str.c_str() + 1); } else { *v = QString::fromLatin1(str.c_str()); } } } } QModelIndex parent(const QModelIndex& index) const override { if (!index.isValid()) { return {}; } Info parentInfo = getInfo(index); Info grandParentInfo = parentInfo; if (parentInfo.contextualHierarchy) { // for contextual hierarchy we have this: // ROOT -> Object in Current Doc -> Prop In Object -> PropPath // ROOT -> prop in Current Object -> prop Path if (parentInfo.prop >= 0) { grandParentInfo.prop = -1; return createIndex(parentInfo.prop, 0, infoId(grandParentInfo)); } // if the parent is the object or a prop attached to the root, we just need the below // line return createIndex(parentInfo.doc, 0, infoId(Info::root)); } else { if (parentInfo.prop >= 0) { grandParentInfo.prop = -1; return createIndex(parentInfo.prop, 0, infoId(grandParentInfo)); } if (parentInfo.obj >= 0) { grandParentInfo.obj = -1; return createIndex(parentInfo.obj, 0, infoId(grandParentInfo)); } if (parentInfo.doc >= 0) { grandParentInfo.doc = -1; return createIndex(parentInfo.doc, 0, infoId(grandParentInfo)); } } return {}; } // returns true if successful, false if 'element' identifies a Leaf bool modelIndexToParentInfo(QModelIndex element, Info& info) const { Info parentInfo; info = Info::root; if (element.isValid()) { parentInfo = getInfo(element); info = parentInfo; // Our wonderful element is a child of the root if (parentInfo.doc < 0) { // need special casing to properly identify this model's object const auto& docs = App::GetApplication().getDocuments(); auto docsSize = static_cast(docs.size() * 2); info.doc = element.row(); // if my element is a contextual descendant of root (current doc object list, // current object prop list) mark it as such if (element.row() >= docsSize) { info.contextualHierarchy = 1; } } else if (parentInfo.contextualHierarchy) { const auto& docs = App::GetApplication().getDocuments(); auto cdoc = App::GetApplication().getDocument(currentDoc.c_str()); if (cdoc) { int objsSize = static_cast(cdoc->getObjects().size() * 2); int idx = parentInfo.doc - static_cast(docs.size()); if (idx < objsSize) { // |-- Parent (OBJECT) - (row 4, [-1,-1,-1,0]) = encode as element => // [parent.row,-1,-1,1] // |- element (PROP) - (row 0, [parent.row,-1,-1,1]) = encode as // element => [parent.row,-1,parent.row,1] info.doc = parentInfo.doc; info.obj = -1; // object information is determined by the DOC index actually info.prop = element.row(); info.contextualHierarchy = 1; } else { // if my parent (parentInfo) is a prop, it means that our element is a prop // path and that is a leaf item (we don't split prop paths further) we can't // encode leaf items into an "Info" return false; } } else { // no contextual document return false; } } // regular hierarchy else if (parentInfo.obj <= 0) { info.obj = element.row(); } else if (parentInfo.prop <= 0) { info.prop = element.row(); } else { return false; } } return true; } QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const override { if (row < 0) { return {}; } Info myParentInfoEncoded = Info::root; // encode the parent's QModelIndex into an 'Info' structure bool parentCanHaveChildren = modelIndexToParentInfo(parent, myParentInfoEncoded); if (!parentCanHaveChildren) { return {}; } return createIndex(row, column, infoId(myParentInfoEncoded)); } // function returns how many children the QModelIndex parent has int rowCount(const QModelIndex& parent = QModelIndex()) const override { Info info; int row = 0; if (!parent.isValid()) { // we're getting the row count for the root // that is: document hierarchy _and_ contextual completion info = Info::root; row = -1; } else { // try to encode the parent's QModelIndex into an info structure // if the paren't can't have any children, return 0 if (!modelIndexToParentInfo(parent, info)) { return 0; } } int count = 0; _data(info, row, nullptr, &count); FC_TRACE( info.doc << "," << info.obj << "," << info.prop << "," << info.contextualHierarchy << "," << row << " row count " << count ); return count; } int columnCount(const QModelIndex&) const override { return 1; } private: mutable std::map>> namedPropsCache; std::set inList; std::string currentDoc; std::string currentObj; bool noProperty; }; const ExpressionCompleterModel::Info ExpressionCompleterModel::Info::root = {-1, -1, -1, 0}; /** * @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::DocumentObject* currentDocObj, QObject* parent, bool noProperty, bool checkInList ) : QCompleter(parent) , currentObj(currentDocObj) , noProperty(noProperty) , checkInList(checkInList) { setCaseSensitivity(Qt::CaseInsensitive); } void ExpressionCompleter::init() { if (model()) { return; } auto m = new ExpressionCompleterModel(this, noProperty); m->setDocumentObject(currentObj.getObject(), checkInList); setModel(m); } void ExpressionCompleter::setDocumentObject(const App::DocumentObject* obj, bool _checkInList) { if (!obj || !obj->isAttachedToDocument()) { currentObj = App::DocumentObjectT(); } else { currentObj = obj; } setCompletionPrefix(QString()); checkInList = _checkInList; auto m = model(); if (m) { static_cast(m)->setDocumentObject(obj, checkInList); } } void ExpressionCompleter::setNoProperty(bool enabled) { noProperty = enabled; auto m = model(); if (m) { static_cast(m)->setNoProperty(enabled); } } QString ExpressionCompleter::pathFromIndex(const QModelIndex& index) const { auto m = model(); if (!m || !index.isValid()) { return {}; } QString res; auto parent = index; do { res = m->data(parent, Qt::UserRole).toString() + res; parent = parent.parent(); } while (parent.isValid()); auto info = ExpressionCompleterModel::getInfo(index); FC_TRACE( "join path " << info.doc << "," << info.obj << "," << info.prop << "," << info.contextualHierarchy << "," << index.row() << ": " << res.toUtf8().constData() ); return res; } QStringList ExpressionCompleter::splitPath(const QString& input) const { QStringList resultList; std::string path = input.toUtf8().constData(); if (path.empty()) { return resultList; } int retry = 0; std::string lastElem; // used to recover in case of parse failure after ".". std::string trim; // used to delete ._self added for another recovery path while (true) { try { // this will not work for incomplete Tokens at the end // "Sketch." will fail to parse and complete. App::ObjectIdentifier ident = ObjectIdentifier::parse(currentObj.getObject(), path); std::vector stringList = ident.getStringList(); auto stringListIter = stringList.begin(); if (retry > 1 && !stringList.empty()) { stringList.pop_back(); } if (!stringList.empty()) { if (!trim.empty() && boost::ends_with(stringList.back(), trim)) { stringList.back().resize(stringList.back().size() - trim.size()); } while (stringListIter != stringList.end()) { resultList << QString::fromStdString(*stringListIter); ++stringListIter; } } if (lastElem.size()) { // if we finish in a trailing separator if (!lastElem.empty()) { // erase the separator lastElem.erase(lastElem.begin()); resultList << QString::fromStdString(lastElem); } else { // add empty string to allow completion after "." or "#" resultList << QString(); } } FC_TRACE( "split path " << path << " -> " << resultList.join(QLatin1String("/")).toUtf8().constData() ); return resultList; } catch (const Base::Exception& except) { FC_TRACE("split path " << path << " error: " << except.what()); if (retry == 0) { size_t lastElemStart = path.rfind('.'); if (lastElemStart == std::string::npos) { lastElemStart = path.rfind('#'); } if (lastElemStart != std::string::npos) { lastElem = path.substr(lastElemStart); path = path.substr(0, lastElemStart); } retry++; continue; } else if (retry == 1) { // restore path from retry 0 if (lastElem.size() > 1) { path = path + lastElem; lastElem = ""; } // else... we don't reset lastElem if it's a '.' or '#' to allow chaining // completions if (!path.empty()) { char last = path[path.size() - 1]; if (last != '#' && last != '.' && path.find('#') != std::string::npos) { path += "._self"; ++retry; continue; } } } else if (retry == 2) { if (path.size() >= 6) { path.resize(path.size() - 6); } if (!path.empty()) { char last = path[path.size() - 1]; if (last != '.' && last != '<' && path.find("#<<") != std::string::npos) { path += ">>._self"; ++retry; trim = ">>"; continue; } } } return QStringList() << input; } } } // Code below inspired by blog entry: // https://john.nachtimwald.com/2009/07/04/qcompleter-and-comma-separated-tags/ void ExpressionCompleter::slotUpdate(const QString& prefix, int pos) { FC_TRACE("SlotUpdate:" << prefix.toUtf8().constData()); init(); QString completionPrefix = tokenizer.perform(prefix, pos); if (completionPrefix.isEmpty()) { if (auto itemView = popup()) { itemView->setVisible(false); } return; } FC_TRACE("Completion Prefix:" << completionPrefix.toUtf8().constData()); // Set completion prefix setCompletionPrefix(completionPrefix); if (widget()->hasFocus()) { FC_TRACE("Complete on Prefix" << completionPrefix.toUtf8().constData()); complete(); FC_TRACE("Complete Done"); } else if (auto itemView = popup()) { itemView->setVisible(false); } Q_EMIT completerSlotUpdated(); } ExpressionValidator::ExpressionValidator(QObject* parent) : QValidator(parent) {} void ExpressionValidator::fixup(QString& input) const { if (input.startsWith(QLatin1String("="))) { input = input.mid(1); } } QValidator::State ExpressionValidator::validate(QString& input, int& pos) const { if (input.startsWith(QLatin1String("="))) { pos = 0; return QValidator::Invalid; } return QValidator::Acceptable; } ExpressionLineEdit::ExpressionLineEdit(QWidget* parent, bool noProperty, char checkPrefix, bool checkInList) : QLineEdit(parent) , completer(nullptr) , block(true) , noProperty(noProperty) , exactMatch(false) , checkInList(checkInList) { setPrefix(checkPrefix); connect(this, &QLineEdit::textEdited, this, &ExpressionLineEdit::slotTextChanged); } void ExpressionLineEdit::setPrefix(char prefix) { checkPrefix = prefix; setValidator(checkPrefix == '=' ? nullptr : new ExpressionValidator(this)); } void ExpressionLineEdit::setDocumentObject(const App::DocumentObject* currentDocObj, bool _checkInList) { checkInList = _checkInList; if (completer) { completer->setDocumentObject(currentDocObj, checkInList); return; } if (currentDocObj) { completer = new ExpressionCompleter(currentDocObj, this, noProperty, checkInList); completer->setWidget(this); completer->setCaseSensitivity(Qt::CaseInsensitive); if (!exactMatch) { completer->setFilterMode(Qt::MatchContains); } connect( completer, qOverload(&QCompleter::activated), this, &ExpressionLineEdit::slotCompleteTextSelected ); connect( completer, qOverload(&QCompleter::highlighted), this, &ExpressionLineEdit::slotCompleteTextHighlighted ); connect(this, &ExpressionLineEdit::textChanged2, completer, &ExpressionCompleter::slotUpdate); } } void ExpressionLineEdit::setNoProperty(bool enabled) { noProperty = enabled; if (completer) { completer->setNoProperty(enabled); } } void ExpressionLineEdit::setExactMatch(bool enabled) { exactMatch = enabled; if (completer) { completer->setFilterMode(exactMatch ? Qt::MatchStartsWith : Qt::MatchContains); } } ExpressionCompleter* ExpressionLineEdit::getCompleter(void) { return this->completer; } 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) { if (!text.size() || (checkPrefix && text[0] != QLatin1Char(checkPrefix))) { return; } Q_EMIT textChanged2(text, cursorPosition()); } } void ExpressionLineEdit::slotCompleteText(const QString& completionPrefix, ActivationMode mode) { int start, end; completer->getPrefixRange(start, end); QString before(text().left(start)); QString after(text().mid(end)); { Base::FlagToggler flag(block, false); before += completionPrefix; setText(before + after); setCursorPosition(before.length()); completer->updatePrefixEnd(before.length()); } // chain completions if we select an entry from the completer drop down // and that entry ends with '.' or '#' if (mode == ActivationMode::Activated) { std::string textToComplete = completionPrefix.toUtf8().constData(); if (textToComplete.size() && (*textToComplete.crbegin() == '.' || *textToComplete.crbegin() == '#')) { Base::FlagToggler flag(block, true); slotTextChanged(before + after); } } } void ExpressionLineEdit::slotCompleteTextHighlighted(const QString& completionPrefix) { slotCompleteText(completionPrefix, ActivationMode::Highlighted); } void ExpressionLineEdit::slotCompleteTextSelected(const QString& completionPrefix) { slotCompleteText(completionPrefix, ActivationMode::Activated); } void ExpressionLineEdit::keyPressEvent(QKeyEvent* e) { Base::FlagToggler flag(block, true); QLineEdit::keyPressEvent(e); } void ExpressionLineEdit::contextMenuEvent(QContextMenuEvent* event) { QMenu* menu = createStandardContextMenu(); if (completer) { menu->addSeparator(); QAction* match = menu->addAction(tr("Exact Match")); match->setCheckable(true); match->setChecked(completer->filterMode() == Qt::MatchStartsWith); QObject::connect(match, &QAction::toggled, this, &Gui::ExpressionLineEdit::setExactMatch); } menu->setAttribute(Qt::WA_DeleteOnClose); menu->popup(event->globalPos()); } /////////////////////////////////////////////////////////////////////// ExpressionTextEdit::ExpressionTextEdit(QWidget* parent) : QPlainTextEdit(parent) , completer(nullptr) , block(true) , exactMatch(false) { connect(this, &QPlainTextEdit::textChanged, this, &ExpressionTextEdit::slotTextChanged); } void ExpressionTextEdit::setExactMatch(bool enabled) { exactMatch = enabled; if (completer) { completer->setFilterMode(exactMatch ? Qt::MatchStartsWith : Qt::MatchContains); } } QSize ExpressionTextEdit::sizeHint() const { return QSize(200, 30); } void ExpressionTextEdit::setDocumentObject(const App::DocumentObject* currentDocObj) { if (completer) { completer->setDocumentObject(currentDocObj); return; } if (currentDocObj) { completer = new ExpressionCompleter(currentDocObj, this); if (!exactMatch) { completer->setFilterMode(Qt::MatchContains); } completer->setWidget(this); completer->setCaseSensitivity(Qt::CaseInsensitive); connect( completer, qOverload(&QCompleter::activated), this, &ExpressionTextEdit::slotCompleteTextSelected ); connect( completer, qOverload(&QCompleter::highlighted), this, &ExpressionTextEdit::slotCompleteTextHighlighted ); connect(this, &ExpressionTextEdit::textChanged2, completer, &ExpressionCompleter::slotUpdate); connect( completer, &ExpressionCompleter::completerSlotUpdated, this, &ExpressionTextEdit::adjustCompleterToCursor ); } } bool ExpressionTextEdit::completerActive() const { return completer && completer->popup() && completer->popup()->isVisible(); } void ExpressionTextEdit::hideCompleter() { if (completer && completer->popup()) { completer->popup()->setVisible(false); } } void ExpressionTextEdit::slotTextChanged() { if (!block) { QTextCursor cursor = textCursor(); completer->popup()->setVisible(false); // hide the completer to avoid flickering Q_EMIT textChanged2(cursor.block().text(), cursor.positionInBlock()); } } void ExpressionTextEdit::slotCompleteText(const QString& completionPrefix, ActivationMode mode) { QTextCursor cursor = textCursor(); int start, end; completer->getPrefixRange(start, end); int pos = cursor.positionInBlock(); if (pos < end) { cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, end - pos); } cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor, end - start); Base::FlagToggler flag(block, false); cursor.insertText(completionPrefix); completer->updatePrefixEnd(cursor.positionInBlock()); // chain completions only when activated (Enter/Click), not when highlighted (arrow keys) if (mode == ActivationMode::Activated) { std::string textToComplete = completionPrefix.toUtf8().constData(); if (!textToComplete.empty() && (*textToComplete.crbegin() == '.' || *textToComplete.crbegin() == '#')) { completer->slotUpdate(cursor.block().text(), cursor.positionInBlock()); } } } void ExpressionTextEdit::slotCompleteTextHighlighted(const QString& completionPrefix) { slotCompleteText(completionPrefix, ActivationMode::Highlighted); } void ExpressionTextEdit::slotCompleteTextSelected(const QString& completionPrefix) { slotCompleteText(completionPrefix, ActivationMode::Activated); } void ExpressionTextEdit::keyPressEvent(QKeyEvent* e) { Base::FlagToggler flag(block, true); // Shift+Enter - insert a new line if ((e->modifiers() & Qt::ShiftModifier) && (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return)) { this->setPlainText(this->toPlainText() + QLatin1Char('\n')); this->moveCursor(QTextCursor::End); if (completer) { completer->popup()->setVisible(false); } e->accept(); return; } // handling if completer is visible if (completer && completer->popup()->isVisible()) { switch (e->key()) { case Qt::Key_Enter: case Qt::Key_Return: case Qt::Key_Escape: case Qt::Key_Backtab: // default action e->ignore(); return; case Qt::Key_Tab: // if no completion is selected, take top one if (!completer->popup()->currentIndex().isValid()) { completer->popup()->setCurrentIndex(completer->popup()->model()->index(0, 0)); } completer->setCurrentRow(completer->popup()->currentIndex().row()); slotCompleteText(completer->currentCompletion(), ActivationMode::Highlighted); // refresh completion list completer->setCompletionPrefix(completer->currentCompletion()); adjustCompleterToCursor(); if (completer->completionCount() == 1) { completer->popup()->setVisible(false); } e->accept(); return; default: break; } } // Enter, Return or Tab - request default action if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return || e->key() == Qt::Key_Tab) { e->ignore(); return; } QPlainTextEdit::keyPressEvent(e); } void ExpressionTextEdit::contextMenuEvent(QContextMenuEvent* event) { QMenu* menu = createStandardContextMenu(); menu->addSeparator(); QAction* match = menu->addAction(tr("Exact Match")); if (completer) { match->setCheckable(true); match->setChecked(completer->filterMode() == Qt::MatchStartsWith); } else { match->setVisible(false); } QAction* action = menu->exec(event->globalPos()); if (completer) { if (action == match) { setExactMatch(match->isChecked()); } } delete menu; } void ExpressionTextEdit::adjustCompleterToCursor() { if (!completer || !completer->popup()) { return; } const int completionsCount = completer->completionModel()->rowCount(); if (!completionsCount) { return; } // get longest string width int maxCompletionWidth = 0; for (int i = 0; i < completionsCount; ++i) { const QModelIndex index = completer->completionModel()->index(i, 0); const QString element = completer->completionModel()->data(index).toString(); maxCompletionWidth = std::max( maxCompletionWidth, static_cast(element.size()) * completer->popup()->fontMetrics().averageCharWidth() ); } if (maxCompletionWidth == 0) { return; // no completions available } const QPoint cursorPos = cursorRect(textCursor()).topLeft(); int posX = cursorPos.x(); int posY = cursorPos.y(); constexpr double popupLengthRatio = 0.6; // popup shall not be longer than 0.6 of // TextEdit length const int widthLimit = static_cast(this->viewport()->width() * popupLengthRatio); completer->popup()->setMaximumWidth(widthLimit); maxCompletionWidth = std::min(maxCompletionWidth, widthLimit); QScreen* screen = QGuiApplication::primaryScreen(); // looking for screen on which popup appears const int cursorGlobalY = mapToGlobal(cursorPos).y(); for (QScreen* elem : QGuiApplication::screens()) { const int screenTopY = elem->geometry().top(); const int screenBottomY = elem->geometry().bottom(); if (cursorGlobalY >= screenTopY && cursorGlobalY < screenBottomY) { screen = elem; break; } } constexpr double marginToScreen = 0.05; // margin to screen as percent of screen // height; keep 5% margin to screen edge constexpr int rowsLimit = 20; // max. count of rows that shall be shown at once const int rowHeight = completer->popup()->fontMetrics().height(); int rowsToEdge = static_cast( (screen->geometry().bottom() * (1.0 - marginToScreen) - (mapToGlobal(cursorPos).y() + rowHeight)) / rowHeight ); const auto adjustHeight = [&rowHeight, &completionsCount](const int _rowsToEdge) -> int { return std::min({ rowHeight * rowsLimit, // up to 'rowsLimit' elements shall be shown at once _rowsToEdge * rowHeight, // or less limited to screen edge completionsCount * rowHeight + 5 // or all, if only there are only few; 5 is magic // number, somehow last entry is partly hovered }); }; int adjustedPopupHeight = adjustHeight(rowsToEdge); // vertical correction to cursor if (rowsToEdge < 4) { // display above cursor rowsToEdge = static_cast( mapToGlobal(cursorPos).y() - screen->geometry().height() * marginToScreen ); adjustedPopupHeight = adjustHeight(rowsToEdge); posY -= adjustedPopupHeight; } else { // display under cursor posY += rowHeight; } const QSize completerSize {maxCompletionWidth + 40, adjustedPopupHeight}; // 40 is margin for // scrollbar // horizontal correction to cursor if (posX + completerSize.width() > viewport()->width()) { posX = viewport()->width() - completerSize.width(); } completer->popup()->resize(completerSize); completer->popup()->move(mapToGlobal(QPoint {posX, posY})); completer->popup()->setVisible(true); } /////////////////////////////////////////////////////////////////////// ExpressionParameter* ExpressionParameter::instance() { static auto inst = new ExpressionParameter(); return inst; } bool ExpressionParameter::isCaseSensitive() const { auto handle = GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/Expression" ); return handle->GetBool("CompleterCaseSensitive", false); } bool ExpressionParameter::isExactMatch() const { auto handle = GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/Expression" ); return handle->GetBool("CompleterMatchExact", false); } #include "moc_ExpressionCompleter.cpp"