"Professional CMake" book suggest the following: "Targets should build successfully with or without compiler support for precompiled headers. It should be considered an optimization, not a requirement. In particular, do not explicitly include a precompile header (e.g. stdafx.h) in the source code, let CMake force-include an automatically generated precompile header on the compiler command line instead. This is more portable across the major compilers and is likely to be easier to maintain. It will also avoid warnings being generated from certain code checking tools like iwyu (include what you use)." Therefore, removed the "#include <PreCompiled.h>" from sources, also there is no need for the "#ifdef _PreComp_" anymore
1311 lines
46 KiB
C++
1311 lines
46 KiB
C++
/***************************************************************************
|
|
* Copyright (c) 2015 Eivind Kvedalen <eivind@kvedalen.name> *
|
|
* *
|
|
* 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 <boost/algorithm/string/predicate.hpp>
|
|
#include <QAbstractItemView>
|
|
#include <QContextMenuEvent>
|
|
#include <QLineEdit>
|
|
#include <QMenu>
|
|
#include <QTextBlock>
|
|
|
|
|
|
#include <App/Application.h>
|
|
#include <App/Document.h>
|
|
#include <App/DocumentObject.h>
|
|
#include <App/ExpressionParser.h>
|
|
#include <App/ObjectIdentifier.h>
|
|
#include <Base/Tools.h>
|
|
#include <CXX/Extensions.hxx>
|
|
|
|
#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<App::ObjectIdentifier> retrieveSubPaths(const App::Property* prop)
|
|
{
|
|
std::vector<App::ObjectIdentifier> 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;
|
|
}
|
|
|
|
// 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;
|
|
std::vector<std::pair<const char*, App::Property*>> props;
|
|
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
|
|
cobj->getPropertyNamedList(props);
|
|
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::fromLatin1(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;
|
|
obj->getPropertyNamedList(props);
|
|
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<App::ObjectIdentifier> paths = retrieveSubPaths(prop);
|
|
|
|
if (count) {
|
|
*count = paths.size();
|
|
}
|
|
|
|
// check to see if this is a valid path
|
|
if (idx < 0 || idx >= static_cast<int>(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());
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
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<int>(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<int>(cdoc->getObjects().size() * 2);
|
|
int idx = parentInfo.doc - static_cast<int>(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:
|
|
std::set<App::DocumentObject*> 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<ExpressionCompleterModel*>(m)->setDocumentObject(obj, checkInList);
|
|
}
|
|
}
|
|
|
|
void ExpressionCompleter::setNoProperty(bool enabled)
|
|
{
|
|
noProperty = enabled;
|
|
auto m = model();
|
|
if (m) {
|
|
static_cast<ExpressionCompleterModel*>(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<std::string> 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<const QString&>(&QCompleter::activated),
|
|
this,
|
|
&ExpressionLineEdit::slotCompleteTextSelected);
|
|
connect(completer,
|
|
qOverload<const QString&>(&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, bool isActivated)
|
|
{
|
|
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());
|
|
}
|
|
|
|
// chain completions if we select an entry from the completer drop down
|
|
// and that entry ends with '.' or '#'
|
|
if (isActivated) {
|
|
std::string textToComplete = completionPrefix.toUtf8().constData();
|
|
if (textToComplete.size()
|
|
&& (*textToComplete.crbegin() == '.' || *textToComplete.crbegin() == '#')) {
|
|
Base::FlagToggler<bool> flag(block, true);
|
|
slotTextChanged(before + after);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ExpressionLineEdit::slotCompleteTextHighlighted(const QString& completionPrefix)
|
|
{
|
|
slotCompleteText(completionPrefix, false);
|
|
}
|
|
|
|
void ExpressionLineEdit::slotCompleteTextSelected(const QString& completionPrefix)
|
|
{
|
|
slotCompleteText(completionPrefix, true);
|
|
}
|
|
|
|
|
|
void ExpressionLineEdit::keyPressEvent(QKeyEvent* e)
|
|
{
|
|
Base::FlagToggler<bool> 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<const QString&>(&QCompleter::activated),
|
|
this,
|
|
&ExpressionTextEdit::slotCompleteText);
|
|
connect(completer,
|
|
qOverload<const QString&>(&QCompleter::highlighted),
|
|
this,
|
|
&ExpressionTextEdit::slotCompleteText);
|
|
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)
|
|
{
|
|
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);
|
|
|
|
// 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));
|
|
}
|
|
// insert completion
|
|
completer->setCurrentRow(completer->popup()->currentIndex().row());
|
|
slotCompleteText(completer->currentCompletion());
|
|
|
|
// refresh completion list
|
|
completer->setCompletionPrefix(completer->currentCompletion());
|
|
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;
|
|
}
|
|
|
|
// get longest string width
|
|
int maxCompletionWidth = 0;
|
|
for (int i = 0; i < completer->completionModel()->rowCount(); ++i) {
|
|
const QModelIndex index = completer->completionModel()->index(i, 0);
|
|
const QString element = completer->completionModel()->data(index).toString();
|
|
maxCompletionWidth = std::max(maxCompletionWidth, static_cast<int>(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();
|
|
|
|
completer->popup()->setMaximumWidth(this->viewport()->width() * 0.6);
|
|
completer->popup()->setMaximumHeight(this->viewport()->height() * 0.6);
|
|
|
|
const QSize completerSize { maxCompletionWidth + 40, completer->popup()->size().height() }; // 40 is margin for scrollbar
|
|
completer->popup()->resize(completerSize);
|
|
|
|
// vertical correction
|
|
if (posY + completerSize.height() > viewport()->height()) {
|
|
posY -= completerSize.height();
|
|
} else {
|
|
posY += fontMetrics().height();
|
|
}
|
|
|
|
// horizontal correction
|
|
if (posX + completerSize.width() > viewport()->width()) {
|
|
posX = viewport()->width() - completerSize.width();
|
|
}
|
|
|
|
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"
|