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.
657 lines
20 KiB
C++
657 lines
20 KiB
C++
#include "PreCompiled.h"
|
|
|
|
#ifndef _PreComp_
|
|
#include <QStandardItem>
|
|
#include <QStandardItemModel>
|
|
#include <QLineEdit>
|
|
#include <QAbstractItemView>
|
|
#include <QTextBlock>
|
|
#endif
|
|
|
|
#include <boost/algorithm/string/predicate.hpp>
|
|
|
|
#include <Base/Tools.h>
|
|
#include <Base/Console.h>
|
|
#include <App/Application.h>
|
|
#include <App/Document.h>
|
|
#include <App/DocumentObject.h>
|
|
#include <App/DocumentObserver.h>
|
|
#include <App/ObjectIdentifier.h>
|
|
#include "ExpressionCompleter.h"
|
|
#include <App/Expression.h>
|
|
#include <App/PropertyLinks.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, 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();
|
|
if(!noProperty)
|
|
inList = obj->getInListEx(true);
|
|
} else {
|
|
currentDoc.clear();
|
|
currentObj.clear();
|
|
inList.clear();
|
|
}
|
|
endResetModel();
|
|
|
|
}
|
|
|
|
// 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<App::Property*> props;
|
|
App::Document *doc = 0;
|
|
App::DocumentObject *obj = 0;
|
|
App::Property *prop = 0;
|
|
if(idx>=0 && idx<docSize)
|
|
doc = docs[idx/2];
|
|
else {
|
|
doc = App::GetApplication().getDocument(currentDoc.c_str());
|
|
if(!doc)
|
|
return;
|
|
idx -= docSize;
|
|
if(info.d.doc<0)
|
|
row = idx;
|
|
const auto &objs = doc->getObjects();
|
|
objSize = (int)objs.size()*2;
|
|
if(idx>=0 && idx<objSize) {
|
|
obj = objs[idx/2];
|
|
if(inList.count(obj))
|
|
return;
|
|
} else if (!noProperty) {
|
|
auto cobj = doc->getObject(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 && !noProperty)
|
|
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 && !noProperty)
|
|
res += QLatin1Char('.');
|
|
v->setValue(res);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
if(noProperty)
|
|
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) {
|
|
QString res;
|
|
if(sep)
|
|
res = QLatin1String(".");
|
|
res += QString::fromLatin1(prop->getName());
|
|
*v = res;
|
|
}
|
|
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<App::DocumentObject*> inList;
|
|
std::string currentDoc;
|
|
std::string currentObj;
|
|
bool noProperty;
|
|
};
|
|
|
|
/**
|
|
* @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)
|
|
: QCompleter(parent), currentObj(currentDocObj), noProperty(noProperty)
|
|
{
|
|
setCaseSensitivity(Qt::CaseInsensitive);
|
|
}
|
|
|
|
void ExpressionCompleter::init() {
|
|
if(model())
|
|
return;
|
|
|
|
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<ExpressionCompleterModel*>(m)->setDocumentObject(obj);
|
|
}
|
|
|
|
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<std::string> sl = p.getStringList();
|
|
std::vector<std::string>::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, int pos)
|
|
{
|
|
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;
|
|
|
|
std::string expression = Base::Tools::toStdString(prefix.mid(start));
|
|
|
|
// Tokenize prefix
|
|
std::vector<boost::tuple<int, int, std::string> > tokens = ExpressionParser::tokenize(expression);
|
|
|
|
// No tokens
|
|
if (tokens.size() == 0) {
|
|
if (popup())
|
|
popup()->setVisible(false);
|
|
return;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
int trim = 0;
|
|
if(prefixEnd > pos)
|
|
trim = prefixEnd - pos;
|
|
|
|
// Extract last tokens that can be rebuilt to a variable
|
|
ssize_t i = static_cast<ssize_t>(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<ssize_t>(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<ssize_t>(tokens.size()))
|
|
prefixStart = prefixEnd;
|
|
else
|
|
prefixStart = start + get<1>(tokens[i]);
|
|
|
|
// Build prefix from tokens
|
|
while (i < static_cast<ssize_t>(tokens.size())) {
|
|
completionPrefix += get<2>(tokens[i]);
|
|
++i;
|
|
}
|
|
|
|
if(trim && trim<(int)completionPrefix.size() )
|
|
completionPrefix.resize(completionPrefix.size()-trim);
|
|
|
|
// Set completion prefix
|
|
setCompletionPrefix(Base::Tools::fromStdString(completionPrefix));
|
|
|
|
if (!completionPrefix.empty() && widget()->hasFocus())
|
|
complete();
|
|
else {
|
|
if (popup())
|
|
popup()->setVisible(false);
|
|
}
|
|
}
|
|
|
|
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&)));
|
|
}
|
|
|
|
void ExpressionLineEdit::setDocumentObject(const App::DocumentObject * currentDocObj)
|
|
{
|
|
if (completer) {
|
|
completer->setDocumentObject(currentDocObj);
|
|
return;
|
|
}
|
|
if (currentDocObj != 0) {
|
|
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,int)), completer, SLOT(slotUpdate(QString,int)));
|
|
}
|
|
}
|
|
|
|
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,cursorPosition());
|
|
}
|
|
}
|
|
|
|
void ExpressionLineEdit::slotCompleteText(const QString & completionPrefix)
|
|
{
|
|
int start,end;
|
|
completer->getPrefixRange(start,end);
|
|
QString before(text().left(start));
|
|
QString after(text().mid(end));
|
|
|
|
Base::FlagToggler<bool> flag(block,false);
|
|
before += completionPrefix;
|
|
setText(before + after);
|
|
setCursorPosition(before.length());
|
|
completer->updatePrefixEnd(before.length());
|
|
}
|
|
|
|
void ExpressionLineEdit::keyPressEvent(QKeyEvent *e) {
|
|
Base::FlagToggler<bool> 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) {
|
|
completer->setDocumentObject(currentDocObj);
|
|
return;
|
|
}
|
|
|
|
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,int)), completer, SLOT(slotUpdate(QString,int)));
|
|
}
|
|
}
|
|
|
|
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(),cursor.positionInBlock());
|
|
}
|
|
}
|
|
|
|
void ExpressionTextEdit::slotCompleteText(const QString & completionPrefix)
|
|
{
|
|
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<bool> flag(block,false);
|
|
cursor.insertText(completionPrefix);
|
|
completer->updatePrefixEnd(cursor.positionInBlock());
|
|
}
|
|
|
|
void ExpressionTextEdit::keyPressEvent(QKeyEvent *e) {
|
|
Base::FlagToggler<bool> flag(block,true);
|
|
QPlainTextEdit::keyPressEvent(e);
|
|
}
|
|
|
|
#include "moc_ExpressionCompleter.cpp"
|