Files
create/src/Gui/ExpressionCompleter.cpp
Zheng, Lei 782913407a 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.
2019-08-30 14:49:14 +02:00

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"