#include "PreCompiled.h" #ifndef _PreComp_ #include #include #include #include #include #endif #include #include #include #include #include #include #include #include #include "ExpressionCompleter.h" #include #include 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, const App::DocumentObject *obj) :QAbstractItemModel(parent) { if(obj) { currentDoc = obj->getDocument()->getName(); currentObj = obj->getNameInDocument(); inList = obj->getInListEx(true); } } // This ExpressionCompleter model works without any pysical 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. union Info { struct { qint32 doc; qint32 obj; }d; struct { qint16 doc; qint16 obj; }d32; void *ptr; }; static void *infoId(const Info &info) { if(sizeof(void*) >= sizeof(info)) return info.ptr; Info info32; info32.d32.doc = (qint16)info.d.doc; info32.d32.obj = (qint16)info.d.obj; return info32.ptr; }; static Info getInfo(const QModelIndex &index) { Info info; info.ptr = index.internalPointer(); if(sizeof(void*) >= sizeof(Info)) return info; Info res; res.d.doc = info.d32.doc; res.d.obj = info.d32.obj; return res; } QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const { if(role!=Qt::EditRole && role!=Qt::DisplayRole && role!=Qt::UserRole) return QVariant(); QVariant v; Info info = getInfo(index); _data(info,index.row(),&v,0,role==Qt::UserRole); FC_TRACE(info.d.doc << "," << info.d.obj << "," << index.row() << ": " << v.toString().toUtf8().constData()); return v; } void _data(const Info &info, int row, QVariant *v, int *count, bool sep=false) const { int idx; idx = info.d.doc<0?row:info.d.doc; const auto &docs = App::GetApplication().getDocuments(); int docSize = (int)docs.size()*2; int objSize = 0; int propSize = 0; std::vector props; App::Document *doc = 0; App::DocumentObject *obj = 0; App::Property *prop = 0; if(idx>=0 && idxgetObjects(); objSize = (int)objs.size()*2; if(idx>=0 && idxgetObject(currentObj.c_str()); if(cobj) { idx -= objSize; if(info.d.doc<0) row = idx; cobj->getPropertyList(props); propSize = (int)props.size(); if(idx >= propSize) return; if(idx>=0) { obj = cobj; prop = props[idx]; } } } } if(info.d.doc<0) { if(count) *count = docSize + objSize + propSize; if(idx>=0 && v) { QString res; if(prop) res = QString::fromLatin1(prop->getName()); else if(obj) { if(idx & 1) res = QString::fromUtf8(quote(obj->Label.getStrValue()).c_str()); else res = QString::fromLatin1(obj->getNameInDocument()); if(sep) res += QLatin1Char('.'); }else { if(idx & 1) res = QString::fromUtf8(quote(doc->Label.getStrValue()).c_str()); else res = QString::fromLatin1(doc->getName()); if(sep) res += QLatin1Char('#'); } v->setValue(res); } return; } if(!obj) { idx = info.d.obj<0?row:info.d.obj; const auto &objs = doc->getObjects(); objSize = (int)objs.size()*2; if(idx<0 || idx>=objSize || inList.count(obj)) return; obj = objs[idx/2]; if(info.d.obj<0) { if(count) *count = objSize; if(v) { QString res; if(idx&1) res = QString::fromUtf8(quote(obj->Label.getStrValue()).c_str()); else res = QString::fromLatin1(obj->getNameInDocument()); if(sep) res += QLatin1Char('.'); v->setValue(res); } return; } } if(!prop) { idx = row; obj->getPropertyList(props); propSize = (int)props.size(); if(idx<0 || idx>=propSize) return; prop = props[idx]; if(count) *count = propSize; } if(v) *v = QString::fromLatin1(prop->getName()); return; } QModelIndex parent(const QModelIndex & index) const { if(!index.isValid()) return QModelIndex(); Info info; Info parentInfo; info = parentInfo = getInfo(index); if(info.d.obj>=0) { parentInfo.d.obj = -1; return createIndex(info.d.obj,0,infoId(parentInfo)); } if(info.d.doc>=0) { parentInfo.d.doc = -1; return createIndex(info.d.doc,0,infoId(parentInfo)); } return QModelIndex(); } QModelIndex index(int row, int column, const QModelIndex & parent = QModelIndex()) const { if(row<0) return QModelIndex(); Info info; if(!parent.isValid()) { info.d.doc = -1; info.d.obj = -1; }else{ info = getInfo(parent); if(info.d.doc<=0) info.d.doc = parent.row(); else if(info.d.obj<=0) info.d.obj = parent.row(); else return QModelIndex(); } return createIndex(row,column,infoId(info)); } int rowCount(const QModelIndex & parent = QModelIndex()) const { Info info; int row = 0; if(!parent.isValid()) { info.d.doc = -1; info.d.obj = -1; row = -1; }else{ info = getInfo(parent); if(info.d.doc<0) info.d.doc = parent.row(); else if(info.d.obj<0) info.d.obj = parent.row(); else return 0; } int count = 0; _data(info,row,0,&count); FC_TRACE(info.d.doc << "," << info.d.obj << "," << row << " row count " << count); return count; } int columnCount(const QModelIndex &) const { return 1; } private: std::set inList; std::string currentDoc; std::string currentObj; }; /** * @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) : QCompleter(parent), prefixStart(0), currentObj(currentDocObj) { setCaseSensitivity(Qt::CaseInsensitive); } void ExpressionCompleter::init() { if(model()) return; setModel(new ExpressionCompleterModel(this,currentObj.getObject())); } QString ExpressionCompleter::pathFromIndex ( const QModelIndex & index ) const { auto m = model(); if(!m || !index.isValid()) return QString(); 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.d.doc << "," << info.d.obj << "," << index.row() << ": " << res.toUtf8().constData()); return res; } QStringList ExpressionCompleter::splitPath ( const QString & input ) const { QStringList l; std::string path = input.toUtf8().constData(); if(path.empty()) return l; int retry = 0; std::string trim; while(1) { try { App::ObjectIdentifier p = ObjectIdentifier::parse( currentObj.getObject(), path); std::vector sl = p.getStringList(); std::vector::const_iterator sli = sl.begin(); if(retry && sl.size()) sl.pop_back(); if(trim.size() && boost::ends_with(sl.back(),trim)) sl.back().resize(sl.back().size()-trim.size()); while (sli != sl.end()) { l << Base::Tools::fromStdString(*sli); ++sli; } FC_TRACE("split path " << path << " -> " << l.join(QLatin1String("/")).toUtf8().constData()); return l; } catch (const Base::Exception &e) { FC_TRACE("split path " << path << " error: " << e.what()); if(!retry) { char last = path[path.size()-1]; if(last!='#' && last!='.' && path.find('#')!=std::string::npos) { path += "._self"; ++retry; continue; } }else if(retry==1) { path.resize(path.size()-6); 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) { init(); using namespace boost::tuples; std::string completionPrefix; // 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))); // No tokens, or last char is a space? if (tokens.size() == 0 || (prefix.size() > 0 && prefix[prefix.size() - 1] == QChar(32))) { 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]) != '.') break; --i; } ++i; // Set prefix start for use when replacing later if (i == static_cast(tokens.size())) prefixStart = prefix.size(); else prefixStart = (prefix.at(0) == QChar::fromLatin1('=') ? 1 : 0) + get<1>(tokens[i]); // Build prefix from tokens while (i < static_cast(tokens.size())) { completionPrefix += get<2>(tokens[i]); ++i; } // Set completion prefix 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(true) { 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, 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())); Base::FlagToggler flag(block,false); setText(before + completionPrefix + after); setCursorPosition(QString(before + completionPrefix).length()); } void ExpressionLineEdit::keyPressEvent(QKeyEvent *e) { Base::FlagToggler flag(block,true); QLineEdit::keyPressEvent(e); } /////////////////////////////////////////////////////////////////////// ExpressionTextEdit::ExpressionTextEdit(QWidget *parent) : QPlainTextEdit(parent) , completer(0) , block(true) { connect(this, SIGNAL(textChanged()), this, SLOT(slotTextChanged())); } void ExpressionTextEdit::setDocumentObject(const App::DocumentObject * currentDocObj) { if (completer) { delete completer; completer = 0; } if (currentDocObj != 0) { completer = new ExpressionCompleter(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 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(); Q_EMIT textChanged2(cursor.block().text().left(cursor.positionInBlock())); } } void ExpressionTextEdit::slotCompleteText(const QString & completionPrefix) { QTextCursor cursor = textCursor(); int start = completer->getPrefixStart(); 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); } } void ExpressionTextEdit::keyPressEvent(QKeyEvent *e) { Base::FlagToggler flag(block,true); QPlainTextEdit::keyPressEvent(e); } #include "moc_ExpressionCompleter.cpp"