FreeCAD

Форк
0
/
ExpressionCompleter.cpp 
1103 строки · 40.8 Кб
1
/***************************************************************************
2
 *   Copyright (c) 2015 Eivind Kvedalen <eivind@kvedalen.name>             *
3
 *                                                                         *
4
 *   This file is part of the FreeCAD CAx development system.              *
5
 *                                                                         *
6
 *   This library is free software; you can redistribute it and/or         *
7
 *   modify it under the terms of the GNU Library General Public           *
8
 *   License as published by the Free Software Foundation; either          *
9
 *   version 2 of the License, or (at your option) any later version.      *
10
 *                                                                         *
11
 *   This library  is distributed in the hope that it will be useful,      *
12
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
13
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
14
 *   GNU Library General Public License for more details.                  *
15
 *                                                                         *
16
 *   You should have received a copy of the GNU Library General Public     *
17
 *   License along with this library; see the file COPYING.LIB. If not,    *
18
 *   write to the Free Software Foundation, Inc., 59 Temple Place,         *
19
 *   Suite 330, Boston, MA  02111-1307, USA                                *
20
 *                                                                         *
21
 ***************************************************************************/
22

23
#include "PreCompiled.h"
24

25
#ifndef _PreComp_
26
# include <boost/algorithm/string/predicate.hpp>
27
# include <QAbstractItemView>
28
# include <QContextMenuEvent>
29
# include <QLineEdit>
30
# include <QMenu>
31
# include <QTextBlock>
32
#endif
33

34
#include <App/Application.h>
35
#include <App/Document.h>
36
#include <App/DocumentObject.h>
37
#include <App/ExpressionParser.h>
38
#include <App/ObjectIdentifier.h>
39
#include <Base/Tools.h>
40
#include <CXX/Extensions.hxx>
41

42
#include "ExpressionCompleter.h"
43

44

45
FC_LOG_LEVEL_INIT("Completer", true, true)
46

47
Q_DECLARE_METATYPE(App::ObjectIdentifier)
48

49
using namespace App;
50
using namespace Gui;
51

52
class ExpressionCompleterModel: public QAbstractItemModel
53
{
54
public:
55
    ExpressionCompleterModel(QObject* parent, bool noProperty)
56
        :QAbstractItemModel(parent), noProperty(noProperty)
57
    {
58
    }
59

60
    void setNoProperty(bool enabled) {
61
        noProperty = enabled;
62
    }
63

64
    void setDocumentObject(const App::DocumentObject* obj, bool checkInList)
65
    {
66
        beginResetModel();
67
        if (obj) {
68
            currentDoc = obj->getDocument()->getName();
69
            currentObj = obj->getNameInDocument();
70
            if (!noProperty && checkInList) {
71
                inList = obj->getInListEx(true);
72
            }
73
        }
74
        else {
75
            currentDoc.clear();
76
            currentObj.clear();
77
            inList.clear();
78
        }
79
        endResetModel();
80
    }
81

82
    // This ExpressionCompleter model works without any physical items.
83
    // Everything item related is stored inside QModelIndex.InternalPointer/InternalId(),
84
    // using the following Info structure.
85
    //
86
    // The Info contains two indices, one for document and the other for object.
87
    // For 32-bit system, the index is 16bit which limits the size to 64K. For
88
    // 64-bit system, the index is 32bit.
89
    //
90
    // The "virtual" items are organized as a tree. The root items are special,
91
    // which consists of three types in the following order,
92
    //
93
    // * Document, even index contains item using document's name, while
94
    //   odd index with quoted document label.
95
    // * Objects of the current document, even index with object's internal
96
    //   name, and odd index with quoted object label.
97
    // * Properties of the current object.
98
    //
99
    // Document item contains object item as child, and object item contains
100
    // property item.
101
    //
102
    // The QModelIndex of a root item has both the doc field and obj field set
103
    // to -1, and uses the row as the item index. We can figure out the type of
104
    // the item solely based on this row index.
105
    //
106
    // QModelIndex of a non-root object item has doc field as the document
107
    // index, and obj field set to -1.
108
    //
109
    // QModelIndex of a non-root property item has doc field as the document
110
    // index, and obj field as the object index.
111
    //
112
    // An item is uniquely identified by the pair (row, father_link) in the QModelIndex
113
    //
114
    // The completion tree structure created takes into account the current document and object
115
    //
116
    // It is done as such, in order to have contextual completion (prop -> object -> files):
117
    // * root (-1,-1)
118
    // |
119
    // |----- documents
120
    // |----- current documents' objects [externally set]
121
    // |----- current objects' props  [externally set]
122
    //
123
    // This complicates the decoding schema for the root, where the childcount will be
124
    // doc.size() + current_doc.Objects.size() + current_obj.Props.size().
125
    //
126
    // This is reflected in the complexity of the DATA function.
127
    //
128
    // Example encoding of a QMODEL Index
129
    //
130
    //    ROOT (row -1, [-1,-1,-1,0]), info represented as [-1,-1,-1,0]
131
    //    |-- doc 1 (non contextual)                 - (row 0, [-1,-1,-1,0]) = encode as parent => [0,-1,-1,0]
132
    //    |-- doc 2 (non contextual)                 - (row 1, [-1,-1,-1,0]) = encode as parent => [1,-1,-1,0]
133
    //    |   |- doc 2.obj1                          - (row 0, [1,-1,-1,0])  = encode as parent => [1, 0,-1,0]
134
    //    |   |- doc 2.obj2                          - (row 1, [1,-1,-1,0])  = encode as parent => [1, 1,-1,0]
135
    //    |   |- doc 2.obj3                          - (row 2, [1,-1,-1,0])  = encode as parent => [1, 2,-1,0]
136
    //    |      |- doc 2.obj3.prop1                 - (row 0, [1, 2,-1,0])  = encode as parent => [1, 2, 0,0]
137
    //    |      |- doc 2.obj3.prop2                 - (row 1, [1, 2,-1,0])  = encode as parent => [1, 2, 1,0]
138
    //    |      |- doc 2.obj3.prop3                 - (row 2, [1, 2,-1,0])  = encode as parent => [1, 2, 2,0]
139
    //    |         |- doc 2.obj3.prop3.path0        - (row 0, [1, 2, 2,0])  = encode as parent => INVALID, LEAF ITEM
140
    //    |         |- doc 2.obj3.prop3.path1        - (row 1, [1, 2, 2,0])  = encode as parent => INVALID, LEAF ITEM
141
    //    |
142
    //    |
143
    //    |-- doc 3 (non contextual)                 - (row 2, [-1,-1,-1,0]) = encode as parent => [2,-1,-1,0]
144
    //    |
145
    //    |-- obj1 (current doc - contextual)        - (row 3, [-1,-1,-1,0]) = encode as parent => [3,-1,-1,1]
146
    //    |-- obj2 (current doc - contextual)        - (row 4, [-1,-1,-1,0]) = encode as parent => [4,-1,-1,1]
147
    //    |   |- obj2.prop1 (contextual)             - (row 0, [4,-1,-1,1])  = encode as parent => [4,-1,0,1]
148
    //    |   |- obj2.prop2 (contextual)             - (row 1, [4,-1,-1,1])  = encode as parent => [4,-1,1,1]
149
    //    |      | - obj2.prop2.path1 (contextual)   - (row 0, [4,-1,0 ,1])  = encode as parent => INVALID, LEAF ITEM
150
    //    |      | - obj2.prop2.path2 (contextual)   - (row 1, [4,-1,1 ,1])  = encode as parent => INVALID, LEAF ITEM
151
    //    |
152
    //    |-- prop1 (current obj - contextual)       - (row 5, [-1,-1,-1,0]) = encode as parent => [5,-1,-1,1]
153
    //    |-- prop2 (current obj - contextual)       - (row 6, [-1,-1,-1,0]) = encode as parent => [6,-1,-1,1]
154
    //        |-- prop2.path1 (contextual)           - (row 0, [ 6,-1,-1,0]) = encode as parent => INVALID, LEAF ITEM
155
    //        |-- prop2.path2 (contextual)           - (row 1, [ 6,-1,-1,0]) = encode as parent => INVALID, LEAF ITEM
156
    //
157

158
    struct Info
159
    {
160
        qint32 doc;
161
        qint32 obj;
162
        qint32 prop;
163
        quint32 contextualHierarchy : 1;
164

165
        static const Info root;
166
    };
167

168
    static const quint64 k_numBitsProp                  = 16ULL;    // 0 .. 15
169
    static const quint64 k_numBitsObj                   = 24ULL;    // 16.. 39
170
    static const quint64 k_numBitsContextualHierarchy   = 1;        // 40
171
    static const quint64 k_numBitsDocuments             = 23ULL;    // 41.. 63
172

173
    static const quint64 k_offsetProp                   = 0;
174
    static const quint64 k_offsetObj                    = k_offsetProp + k_numBitsProp;
175
    static const quint64 k_offsetContextualHierarchy    = k_offsetObj + k_numBitsObj;
176
    static const quint64 k_offsetDocuments              = k_offsetContextualHierarchy + k_numBitsContextualHierarchy;
177

178
    static const quint64 k_maskProp                     = ((1ULL << k_numBitsProp) - 1);
179
    static const quint64 k_maskObj                      = ((1ULL << k_numBitsObj) - 1);
180
    static const quint64 k_maskContextualHierarchy      = ((1ULL << k_numBitsContextualHierarchy) - 1);
181
    static const quint64 k_maskDocuments                = ((1ULL << k_numBitsDocuments) - 1);
182

183
    union InfoPtrEncoding
184
    {
185
        quint64 d_enc;
186
        struct
187
        {
188
            quint8 doc;
189
            quint8 prop;
190
            quint16 obj : 15;
191
            quint16 contextualHierarchy : 1;
192
        } d32;
193
        void* ptr;
194

195
        InfoPtrEncoding(const Info& info)
196
            : d_enc(0)
197
        {
198
            if (sizeof(void*) < sizeof(InfoPtrEncoding)) {
199
                d32.doc = (quint8)(info.doc + 1);
200
                d32.obj = (quint16)(info.obj + 1);
201
                d32.prop = (quint8)(info.prop + 1);
202
                d32.contextualHierarchy = info.contextualHierarchy;
203
            }
204
            else {
205
                d_enc = ((quint64(info.doc + 1) & k_maskDocuments) << k_offsetDocuments)
206
                    | ((quint64(info.contextualHierarchy) & k_maskContextualHierarchy)
207
                       << k_offsetContextualHierarchy)
208
                    | ((quint64(info.obj + 1) & k_maskObj) << k_offsetObj)
209
                    | ((quint64(info.prop + 1) & k_maskProp) << k_offsetProp);
210
            }
211
        }
212
        InfoPtrEncoding(void* pointer)
213
            : d_enc(0)
214
        {
215
            this->ptr = pointer;
216
        }
217

218
        Info DecodeInfo()
219
        {
220
            Info info;
221
            if (sizeof(void*) < sizeof(InfoPtrEncoding)) {
222
                info.doc = qint32(d32.doc) - 1;
223
                info.obj = qint32(d32.obj) - 1;
224
                info.prop = qint32(d32.prop) - 1;
225
                info.contextualHierarchy = d32.contextualHierarchy;
226
            }
227
            else {
228
                info.doc = ((d_enc >> k_offsetDocuments) & k_maskDocuments) - 1;
229
                info.contextualHierarchy =
230
                    ((d_enc >> k_offsetContextualHierarchy) & k_maskContextualHierarchy);
231
                info.obj = ((d_enc >> k_offsetObj) & k_maskObj) - 1;
232
                info.prop = ((d_enc >> k_offsetProp) & k_maskProp) - 1;
233
            }
234
            return info;
235
        }
236
    };
237

238
    static void* infoId(const Info& info)
239
    {
240
        InfoPtrEncoding ptrEnc(info);
241
        return ptrEnc.ptr;
242
    }
243

244
    static Info getInfo(const QModelIndex& index)
245
    {
246
        InfoPtrEncoding enc(index.internalPointer());
247
        return enc.DecodeInfo();
248
    }
249

250
    QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override
251
    {
252
        if (role != Qt::EditRole && role != Qt::DisplayRole && role != Qt::UserRole)
253
            return {};
254
        QVariant variant;
255
        Info info = getInfo(index);
256
        _data(info, index.row(), &variant, nullptr, role == Qt::UserRole);
257
        FC_TRACE(info.doc << "," << info.obj << "," << info.prop << "," << info.contextualHierarchy
258
                          << "," << index.row() << ": " << variant.toString().toUtf8().constData());
259
        return variant;
260
    }
261

262
    static std::vector<App::ObjectIdentifier> retrieveSubPaths(const App::Property* prop)
263
    {
264
        std::vector<App::ObjectIdentifier> result;
265
        if (prop) {
266
            prop->getPaths(result);
267
            // need to filter out irrelevant paths (len 1, aka just this object identifier)
268
            auto res = std::remove_if(
269
                result.begin(), result.end(), [](const App::ObjectIdentifier& path) -> bool {
270
                    return path.getComponents().empty();
271
                });
272
            result.erase(res, result.end());
273
        }
274
        return result;
275
    }
276

277
    // The completion tree structure created takes into account the current document and object
278
    //
279
    // It is done as such:
280
    // * root (-1,-1)
281
    // |
282
    // |----- documents
283
    // |----- current documents' objects [externally set]
284
    // |----- current objects' props  [externally set]
285
    //
286
    // This complicates the decoding schema for the root, where the childcount will be
287
    // doc.size() + current_doc.Objects.size() + current_obj.Props.size().
288
    //
289
    // this function is called in two modes:
290
    // - obtain the count of a node identified by Info,row => count != nullptr, v==nullptr
291
    // - get the text of an item. This text will contain separators but NO full path
292
    void _data(const Info& info, int row, QVariant* v, int* count, bool sep = false) const
293
    {
294
        int idx;
295
        // identify the document index. For any children of the root, it is given by traversing
296
        // the flat list and identified by [row]
297
        idx = info.doc < 0 ? row : info.doc;
298
        const auto &docs = App::GetApplication().getDocuments();
299
        int docSize = (int)docs.size() * 2;
300
        int objSize = 0;
301
        int propSize = 0;
302
        std::vector<std::pair<const char*, App::Property*> > props;
303
        App::Document* doc = nullptr;
304
        App::DocumentObject* obj = nullptr;
305
        const char* propName = nullptr;
306
        App::Property* prop = nullptr;
307
        // check if the document is uniquely identified: either the correct index in info.doc
308
        // OR if, the node is a descendant of the root, its row lands within 0...docsize
309
        if (idx >= 0 && idx < docSize) {
310
            doc = docs[idx / 2];
311
        }
312
        else {
313
            // if we're looking at the ROOT, or the row identifies one of the other ROOT elements
314
            // |----- current documents' objects, rows: docs.size             ... docs.size + objs.size
315
            // |----- current objects' props,     rows: docs.size + objs.size ... docs.size + objs.size+  props.size
316
            //
317
            // We need to process the ROOT so we get the correct count for its children
318
            doc = App::GetApplication().getDocument(currentDoc.c_str());
319
            if (!doc)// no current, there are no additional objects
320
                return;
321

322
            // move to the current documents' objects' range
323
            idx -= docSize;
324
            if (info.doc < 0)
325
                row = idx;
326

327
            const auto &objs = doc->getObjects();
328
            objSize = (int)objs.size() * 2;
329
            // if this is a valid object, we found our object and break.
330
            // if not, this may be the root or one of current object's properties
331
            if (idx >= 0 && idx < objSize) {
332
                obj = objs[idx / 2];
333
                // if they are in the ignore list skip
334
                if (inList.count(obj))
335
                    return;
336
            }
337
            else if (!noProperty) {
338
                // need to check the current object's props range, or we're parsing the ROOT
339
                auto cobj = doc->getObject(currentObj.c_str());
340
                if (cobj) {
341
                    // move to the props range of the current object
342
                    idx -= objSize;
343
                    if (info.doc < 0)
344
                        row = idx;
345
                    // get the properties
346
                    cobj->getPropertyNamedList(props);
347
                    propSize = (int)props.size();
348

349
                    // if this is an invalid index, bail out
350
                    // if it's the ROOT break!
351
                    if (idx >= propSize)
352
                        return;
353
                    if (idx >= 0) {
354
                        obj = cobj; // we only set the active object if we're not processing the root.
355
                        propName = props[idx].first;
356
                        prop = props[idx].second;
357
                    }
358
                }
359
            }
360
        }
361
        // the item is the ROOT or a CHILD of the root
362
        if (info.doc < 0) {
363
            // and we're asking for a count, compute it
364
            if (count) {
365
                // note that if we're dealing with a valid DOC node (row>0, ROOT_info)
366
                // objSize and propSize will be zero because of the early exit above
367
                *count = docSize + objSize + propSize;
368
            }
369
            if (idx >= 0 && v) {
370
                // we're asking for this child's data, and IT's NOT THE ROOT
371
                QString res;
372
                // we resolved the property
373
                if (propName) {
374
                    res = QString::fromLatin1(propName);
375
                    // resolve the property
376
                    if (sep && !noProperty && !retrieveSubPaths(prop).empty())
377
                        res += QLatin1Char('.');
378
                }
379
                else if (obj) {
380
                    // the object has been resolved, use the saved idx to figure out quotation or not.
381
                    if (idx & 1)
382
                        res = QString::fromUtf8(quote(obj->Label.getStrValue()).c_str());
383
                    else
384
                        res = QString::fromLatin1(obj->getNameInDocument());
385
                    if (sep && !noProperty)
386
                        res += QLatin1Char('.');
387
                }
388
                else {
389
                    // the document has been resolved, use the saved idx to figure out quotation or not.
390
                    if (idx & 1)
391
                        res = QString::fromUtf8(quote(doc->Label.getStrValue()).c_str());
392
                    else
393
                        res = QString::fromLatin1(doc->getName());
394
                    if (sep)
395
                        res += QLatin1Char('#');
396
                }
397
                v->setValue(res);
398
            }
399
            // done processing the ROOT or any child items
400
            return;
401
        }
402

403
        // object not resolved
404
        if (!obj) {
405
            // are we pointing to an object item, or our father (info) is an object
406
            idx = info.obj < 0 ? row : info.obj;
407
            const auto &objs = doc->getObjects();
408
            objSize = (int)objs.size() * 2;
409
            // if invalid index, or in the ignore list bail out
410
            if (idx < 0 || idx >= objSize || inList.count(obj))
411
                return;
412
            obj = objs[idx / 2];
413

414
            if (info.obj < 0) {
415
                // if this is AN actual Object item and not a root
416
                if (count)
417
                    *count = objSize; // set the correct count if requested
418
                if (v) {
419
                    // resolve the name
420
                    QString res;
421
                    if (idx & 1)
422
                        res = QString::fromUtf8(quote(obj->Label.getStrValue()).c_str());
423
                    else
424
                        res = QString::fromLatin1(obj->getNameInDocument());
425
                    if (sep && !noProperty)
426
                        res += QLatin1Char('.');
427
                    v->setValue(res);
428
                }
429
                return;
430
            }
431
        }
432

433
        if (noProperty)
434
            return;
435
        if (!propName) {
436
            idx = info.prop < 0 ? row : info.prop;
437
            obj->getPropertyNamedList(props);
438
            propSize = (int)props.size();
439
            // return if the property is invalid
440
            if (idx < 0 || idx >= propSize) {
441
                return;
442
            }
443
            propName = props[idx].first;
444
            prop = props[idx].second;
445
            // if this is a root object item
446
            if (info.prop < 0) {
447
                // set the property size count
448
                if (count) {
449
                    *count = propSize;
450
                }
451
                if (v) {
452
                    QString res = QString::fromLatin1(propName);
453

454
                    // check to see if we have accessible paths from this prop name?
455
                    if (sep && !retrieveSubPaths(prop).empty())
456
                        res += QLatin1Char('.');
457
                    *v = res;
458
                }
459
                return;
460
            }
461
        }
462

463
        // resolve paths
464
        if (prop) {
465
            // idx identifies the path
466
            idx = row;
467
            std::vector<App::ObjectIdentifier> paths = retrieveSubPaths(prop);
468

469
            if (count) {
470
                *count = paths.size();
471
            }
472

473
            // check to see if this is a valid path
474
            if (idx < 0 || idx >= static_cast<int>(paths.size())) {
475
                return;
476
            }
477

478
            if (v) {
479
                auto str = paths[idx].getSubPathStr();
480
                if (str.size() && (str[0] == '.' || str[0] == '#')) {
481
                    // skip the "."
482
                    *v = QString::fromLatin1(str.c_str() + 1);
483
                }
484
                else {
485
                    *v = QString::fromLatin1(str.c_str());
486
                }
487
            }
488
        }
489
        return;
490
    }
491

492
    QModelIndex parent(const QModelIndex & index) const override {
493
        if (!index.isValid())
494
            return {};
495

496
        Info parentInfo = getInfo(index);
497
        Info grandParentInfo = parentInfo;
498

499
        if (parentInfo.contextualHierarchy) {
500
            // for contextual hierarchy we have this:
501
            // ROOT -> Object in Current Doc -> Prop In Object -> PropPath
502
            // ROOT -> prop in Current Object -> prop Path
503

504
            if (parentInfo.prop >= 0) {
505
                grandParentInfo.prop = -1;
506
                return createIndex(parentInfo.prop, 0, infoId(grandParentInfo));
507
            }
508
            // if the parent is the object or a prop attached to the root, we just need the below line
509
            return createIndex(parentInfo.doc, 0, infoId(Info::root));
510
        }
511
        else {
512
            if (parentInfo.prop >= 0) {
513
                grandParentInfo.prop = -1;
514
                return createIndex(parentInfo.prop, 0, infoId(grandParentInfo));
515
            }
516
            if (parentInfo.obj >= 0) {
517
                grandParentInfo.obj = -1;
518
                return createIndex(parentInfo.obj, 0, infoId(grandParentInfo));
519
            }
520
            if (parentInfo.doc >= 0) {
521
                grandParentInfo.doc = -1;
522
                return createIndex(parentInfo.doc, 0, infoId(grandParentInfo));
523
            }
524
        }
525

526

527
        return {};
528
    }
529

530
    // returns true if successful, false if 'element' identifies a Leaf
531
    bool modelIndexToParentInfo(QModelIndex element, Info & info) const
532
    {
533
        Info parentInfo;
534
        info = Info::root;
535

536
        if (element.isValid()) {
537
            parentInfo = getInfo(element);
538
            info = parentInfo;
539

540
            // Our wonderful element is a child of the root
541
            if (parentInfo.doc < 0) {
542
                // need special casing to properly identify this model's object
543
                const auto& docs = App::GetApplication().getDocuments();
544
                auto docsSize = static_cast<int>(docs.size() * 2);
545

546
                info.doc = element.row();
547

548
                // if my element is a contextual descendant of root (current doc object list, current object prop list)
549
                // mark it as such
550
                if (element.row() >= docsSize) {
551
                    info.contextualHierarchy = 1;
552
                }
553
            }
554
            else if (parentInfo.contextualHierarchy) {
555
                const auto& docs = App::GetApplication().getDocuments();
556
                auto cdoc = App::GetApplication().getDocument(currentDoc.c_str());
557

558
                if (cdoc) {
559
                    int objsSize = static_cast<int>(cdoc->getObjects().size() * 2);
560
                    int idx = parentInfo.doc - static_cast<int>(docs.size());
561
                    if (idx < objsSize) {
562
                        //  |-- Parent (OBJECT)   - (row 4, [-1,-1,-1,0]) = encode as element => [parent.row,-1,-1,1]
563
                        //      |- element (PROP) - (row 0, [parent.row,-1,-1,1]) = encode as element => [parent.row,-1,parent.row,1]
564

565
                        info.doc = parentInfo.doc;
566
                        info.obj = -1;// object information is determined by the DOC index actually
567
                        info.prop = element.row();
568
                        info.contextualHierarchy = 1;
569
                    }
570
                    else {
571
                        // if my parent (parentInfo) is a prop, it means that our element is a prop path
572
                        // and that is a leaf item (we don't split prop paths further)
573
                        // we can't encode leaf items into an "Info"
574
                        return false;
575
                    }
576
                }
577
                else {
578
                    // no contextual document
579
                    return false;
580
                }
581

582
            }
583
            // regular hierarchy
584
            else if (parentInfo.obj <= 0) {
585
                info.obj = element.row();
586
            }
587
            else if (parentInfo.prop <= 0) {
588
                info.prop = element.row();
589
            }
590
            else {
591
                return false;
592
            }
593
        }
594
        return true;
595
    }
596

597
    QModelIndex index(int row, int column,
598
        const QModelIndex & parent = QModelIndex()) const override {
599
        if (row < 0)
600
            return {};
601
        Info myParentInfoEncoded = Info::root;
602

603
        // encode the parent's QModelIndex into an 'Info' structure
604
        bool parentCanHaveChildren = modelIndexToParentInfo(parent, myParentInfoEncoded);
605
        if (!parentCanHaveChildren) {
606
            return {};
607
        }
608
        return createIndex(row, column, infoId(myParentInfoEncoded));
609
    }
610

611
    // function returns how many children the QModelIndex parent has
612
    int rowCount(const QModelIndex& parent = QModelIndex()) const override
613
    {
614
        Info info;
615
        int row = 0;
616
        if (!parent.isValid()) {
617
            // we're getting the row count for the root
618
            // that is: document hierarchy _and_ contextual completion
619
            info = Info::root;
620
            row = -1;
621
        }
622
        else {
623
            // try to encode the parent's QModelIndex into an info structure
624
            // if the paren't can't have any children, return 0
625
            if (!modelIndexToParentInfo(parent, info)) {
626
                return 0;
627
            }
628
        }
629
        int count = 0;
630
        _data(info, row, nullptr, &count);
631
        FC_TRACE(info.doc << "," << info.obj << "," << info.prop << "," << info.contextualHierarchy
632
                          << "," << row << " row count " << count);
633
        return count;
634
    }
635

636
    int columnCount(const QModelIndex&) const override
637
    {
638
        return 1;
639
    }
640

641
private:
642
    std::set<App::DocumentObject*> inList;
643
    std::string currentDoc;
644
    std::string currentObj;
645
    bool noProperty;
646
};
647

648
const ExpressionCompleterModel::Info ExpressionCompleterModel::Info::root = {-1, -1, -1, 0};
649

650
/**
651
 * @brief Construct an ExpressionCompleter object.
652
 * @param currentDoc Current document to generate the model from.
653
 * @param currentDocObj Current document object to generate model from.
654
 * @param parent Parent object owning the completer.
655
 */
656

657
ExpressionCompleter::ExpressionCompleter(const App::DocumentObject* currentDocObj,
658
    QObject* parent, bool noProperty, bool checkInList)
659
    : QCompleter(parent), currentObj(currentDocObj)
660
    , noProperty(noProperty), checkInList(checkInList)
661
{
662
    setCaseSensitivity(Qt::CaseInsensitive);
663
}
664

665
void ExpressionCompleter::init() {
666
    if (model())
667
        return;
668

669
    auto m = new ExpressionCompleterModel(this,noProperty);
670
    m->setDocumentObject(currentObj.getObject(),checkInList);
671
    setModel(m);
672
}
673

674
void ExpressionCompleter::setDocumentObject(const App::DocumentObject* obj, bool _checkInList)
675
{
676
    if (!obj || !obj->isAttachedToDocument())
677
        currentObj = App::DocumentObjectT();
678
    else
679
        currentObj = obj;
680
    setCompletionPrefix(QString());
681
    checkInList = _checkInList;
682
    auto m = model();
683
    if (m)
684
        static_cast<ExpressionCompleterModel*>(m)->setDocumentObject(obj, checkInList);
685
}
686

687
void ExpressionCompleter::setNoProperty(bool enabled) {
688
    noProperty = enabled;
689
    auto m = model();
690
    if (m)
691
        static_cast<ExpressionCompleterModel*>(m)->setNoProperty(enabled);
692
}
693

694
QString ExpressionCompleter::pathFromIndex(const QModelIndex& index) const
695
{
696
    auto m = model();
697
    if (!m || !index.isValid())
698
        return {};
699

700
    QString res;
701
    auto parent = index;
702
    do {
703
        res = m->data(parent, Qt::UserRole).toString() + res;
704
        parent = parent.parent();
705
    } while (parent.isValid());
706

707
    auto info = ExpressionCompleterModel::getInfo(index);
708
    FC_TRACE("join path " << info.doc << "," << info.obj << "," << info.prop << ","
709
                          << info.contextualHierarchy << "," << index.row()
710
                          << ": " << res.toUtf8().constData());
711
    return res;
712
}
713

714
QStringList ExpressionCompleter::splitPath(const QString& input) const
715
{
716
    QStringList resultList;
717
    std::string path = input.toUtf8().constData();
718
    if (path.empty())
719
        return resultList;
720

721
    int retry = 0;
722
    std::string lastElem;  // used to recover in case of parse failure after ".".
723
    std::string trim;      // used to delete ._self added for another recovery path
724
    while (true) {
725
        try {
726
            // this will not work for incomplete Tokens at the end
727
            // "Sketch." will fail to parse and complete.
728

729
            App::ObjectIdentifier ident = ObjectIdentifier::parse(
730
                    currentObj.getObject(), path);
731

732
            std::vector<std::string> stringList = ident.getStringList();
733
            auto stringListIter = stringList.begin();
734
            if (retry > 1 && !stringList.empty())
735
                stringList.pop_back();
736

737
            if (!stringList.empty()) {
738
                if (!trim.empty() && boost::ends_with(stringList.back(), trim))
739
                    stringList.back().resize(stringList.back().size() - trim.size());
740
                while (stringListIter != stringList.end()) {
741
                    resultList << Base::Tools::fromStdString(*stringListIter);
742
                    ++stringListIter;
743
                }
744
            }
745
            if (lastElem.size()) {
746
                // if we finish in a trailing separator
747
                if (!lastElem.empty()) {
748
                    // erase the separator
749
                    lastElem.erase(lastElem.begin());
750
                    resultList << Base::Tools::fromStdString(lastElem);
751
                } else {
752
                    // add empty string to allow completion after "." or "#"
753
                    resultList << QString();
754
                }
755
            }
756
            FC_TRACE("split path " << path << " -> "
757
                                   << resultList.join(QLatin1String("/")).toUtf8().constData());
758
            return resultList;
759
        }
760
        catch (const Base::Exception& except) {
761
            FC_TRACE("split path " << path << " error: " << except.what());
762
            if (retry == 0) {
763
                size_t lastElemStart = path.rfind('.');
764

765
                if (lastElemStart == std::string::npos) {
766
                    lastElemStart = path.rfind('#');
767
                }
768
                if (lastElemStart != std::string::npos) {
769
                    lastElem = path.substr(lastElemStart);
770
                    path = path.substr(0, lastElemStart);
771
                }
772
                retry++;
773
                continue;
774
            }
775
            else if (retry == 1) {
776
                // restore path from retry 0
777
                if (lastElem.size() > 1) {
778
                    path = path + lastElem;
779
                    lastElem = "";
780
                }
781
                // else... we don't reset lastElem if it's a '.' or '#' to allow chaining completions
782
                if (!path.empty()) {
783
                    char last = path[path.size() - 1];
784
                    if (last != '#' && last != '.' && path.find('#') != std::string::npos) {
785
                        path += "._self";
786
                        ++retry;
787
                        continue;
788
                    }
789
                }
790
            }
791
            else if (retry == 2) {
792
                if (path.size() >= 6) {
793
                    path.resize(path.size() - 6);
794
                }
795
                if (!path.empty()) {
796
                    char last = path[path.size() - 1];
797
                    if (last != '.' && last != '<' && path.find("#<<") != std::string::npos) {
798
                        path += ">>._self";
799
                        ++retry;
800
                        trim = ">>";
801
                        continue;
802
                    }
803
                }
804
            }
805
            return QStringList() << input;
806
        }
807
    }
808
}
809

810
// Code below inspired by blog entry:
811
// https://john.nachtimwald.com/2009/07/04/qcompleter-and-comma-separated-tags/
812

813
void ExpressionCompleter::slotUpdate(const QString & prefix, int pos)
814
{
815
    FC_TRACE("SlotUpdate:" << prefix.toUtf8().constData());
816

817
    init();
818

819
    QString completionPrefix = tokenizer.perform(prefix, pos);
820
    if (completionPrefix.isEmpty()) {
821
        if (auto itemView = popup())
822
            itemView->setVisible(false);
823
        return;
824
    }
825

826
    FC_TRACE("Completion Prefix:" << completionPrefix.toUtf8().constData());
827
    // Set completion prefix
828
    setCompletionPrefix(completionPrefix);
829

830
    if (widget()->hasFocus()) {
831
        FC_TRACE("Complete on Prefix" << completionPrefix.toUtf8().constData());
832
        complete();
833
        FC_TRACE("Complete Done");
834

835
    }
836
    else if (auto itemView = popup()) {
837
        itemView->setVisible(false);
838
    }
839
}
840

841
ExpressionLineEdit::ExpressionLineEdit(QWidget* parent, bool noProperty,
842
    char checkPrefix, bool checkInList)
843
    : QLineEdit(parent)
844
    , completer(nullptr)
845
    , block(true)
846
    , noProperty(noProperty)
847
    , exactMatch(false)
848
    , checkInList(checkInList)
849
    , checkPrefix(checkPrefix)
850
{
851
    connect(this, &QLineEdit::textEdited, this, &ExpressionLineEdit::slotTextChanged);
852
}
853

854
void ExpressionLineEdit::setPrefix(char prefix) {
855
    checkPrefix = prefix;
856
}
857

858
void ExpressionLineEdit::setDocumentObject(const App::DocumentObject* currentDocObj,
859
    bool _checkInList)
860
{
861
    checkInList = _checkInList;
862
    if (completer) {
863
        completer->setDocumentObject(currentDocObj, checkInList);
864
        return;
865
    }
866
    if (currentDocObj) {
867
        completer = new ExpressionCompleter(currentDocObj, this, noProperty, checkInList);
868
        completer->setWidget(this);
869
        completer->setCaseSensitivity(Qt::CaseInsensitive);
870
        if (!exactMatch)
871
            completer->setFilterMode(Qt::MatchContains);
872
        connect(completer, qOverload<const QString&>(&QCompleter::activated),
873
                this, &ExpressionLineEdit::slotCompleteTextSelected);
874
        connect(completer,
875
                qOverload<const QString&>(&QCompleter::highlighted),
876
                this, &ExpressionLineEdit::slotCompleteTextHighlighted);
877
        connect(this, &ExpressionLineEdit::textChanged2,
878
                completer, &ExpressionCompleter::slotUpdate);
879
    }
880
}
881

882
void ExpressionLineEdit::setNoProperty(bool enabled) {
883
    noProperty = enabled;
884
    if (completer)
885
        completer->setNoProperty(enabled);
886
}
887

888
void ExpressionLineEdit::setExactMatch(bool enabled) {
889
    exactMatch = enabled;
890
    if (completer)
891
        completer->setFilterMode(exactMatch ? Qt::MatchStartsWith : Qt::MatchContains);
892

893
}
894

895
bool ExpressionLineEdit::completerActive() const
896
{
897
    return completer && completer->popup() && completer->popup()->isVisible();
898
}
899

900
void ExpressionLineEdit::hideCompleter()
901
{
902
    if (completer && completer->popup())
903
        completer->popup()->setVisible(false);
904
}
905

906
void ExpressionLineEdit::slotTextChanged(const QString & text)
907
{
908
    if (!block) {
909
        if (!text.size() || (checkPrefix && text[0] != QLatin1Char(checkPrefix)))
910
            return;
911
        Q_EMIT textChanged2(text,cursorPosition());
912
    }
913
}
914

915
void ExpressionLineEdit::slotCompleteText(const QString & completionPrefix, bool isActivated)
916
{
917
    int start,end;
918
    completer->getPrefixRange(start,end);
919
    QString before(text().left(start));
920
    QString after(text().mid(end));
921

922
    {
923
        Base::FlagToggler<bool> flag(block, false);
924
        before += completionPrefix;
925
        setText(before + after);
926
        setCursorPosition(before.length());
927
        completer->updatePrefixEnd(before.length());
928
    }
929

930
    // chain completions if we select an entry from the completer drop down
931
    // and that entry ends with '.' or '#'
932
    if (isActivated) {
933
        std::string textToComplete = completionPrefix.toUtf8().constData();
934
        if (textToComplete.size()
935
            && (*textToComplete.crbegin() == '.' || *textToComplete.crbegin() == '#')) {
936
            Base::FlagToggler<bool> flag(block, true);
937
            slotTextChanged(before + after);
938
        }
939
    }
940
}
941

942
void ExpressionLineEdit::slotCompleteTextHighlighted(const QString& completionPrefix)
943
{
944
    slotCompleteText(completionPrefix, false);
945
}
946

947
void ExpressionLineEdit::slotCompleteTextSelected(const QString& completionPrefix)
948
{
949
    slotCompleteText(completionPrefix, true);
950
}
951

952

953
void ExpressionLineEdit::keyPressEvent(QKeyEvent* e)
954
{
955
    Base::FlagToggler<bool> flag(block,true);
956
    QLineEdit::keyPressEvent(e);
957
}
958

959
void ExpressionLineEdit::contextMenuEvent(QContextMenuEvent* event)
960
{
961
    QMenu* menu = createStandardContextMenu();
962

963
    if (completer) {
964
        menu->addSeparator();
965
        QAction *match = menu->addAction(tr("Exact match"));
966
        match->setCheckable(true);
967
        match->setChecked(completer->filterMode() == Qt::MatchStartsWith);
968
        QObject::connect(match, &QAction::toggled,
969
                         this, &Gui::ExpressionLineEdit::setExactMatch);
970
    }
971
    menu->setAttribute(Qt::WA_DeleteOnClose);
972

973
    menu->popup(event->globalPos());
974
}
975

976

977
///////////////////////////////////////////////////////////////////////
978

979
ExpressionTextEdit::ExpressionTextEdit(QWidget* parent)
980
    : QPlainTextEdit(parent)
981
    , completer(nullptr)
982
    , block(true)
983
    , exactMatch(false)
984
{
985
    connect(this, &QPlainTextEdit::textChanged, this, &ExpressionTextEdit::slotTextChanged);
986
}
987

988
void ExpressionTextEdit::setExactMatch(bool enabled)
989
{
990
    exactMatch = enabled;
991
    if (completer)
992
        completer->setFilterMode(exactMatch ? Qt::MatchStartsWith : Qt::MatchContains);
993
}
994

995
void ExpressionTextEdit::setDocumentObject(const App::DocumentObject* currentDocObj)
996
{
997
    if (completer) {
998
        completer->setDocumentObject(currentDocObj);
999
        return;
1000
    }
1001

1002
    if (currentDocObj) {
1003
        completer = new ExpressionCompleter(currentDocObj, this);
1004
        if (!exactMatch)
1005
            completer->setFilterMode(Qt::MatchContains);
1006
        completer->setWidget(this);
1007
        completer->setCaseSensitivity(Qt::CaseInsensitive);
1008
        connect(completer, qOverload<const QString&>(&QCompleter::activated), this,
1009
            &ExpressionTextEdit::slotCompleteText);
1010
        connect(completer, qOverload<const QString&>(&QCompleter::highlighted), this,
1011
            &ExpressionTextEdit::slotCompleteText);
1012
        connect(this, &ExpressionTextEdit::textChanged2, completer,
1013
            &ExpressionCompleter::slotUpdate);
1014
    }
1015
}
1016

1017
bool ExpressionTextEdit::completerActive() const
1018
{
1019
    return completer && completer->popup() && completer->popup()->isVisible();
1020
}
1021

1022
void ExpressionTextEdit::hideCompleter()
1023
{
1024
    if (completer && completer->popup())
1025
        completer->popup()->setVisible(false);
1026
}
1027

1028
void ExpressionTextEdit::slotTextChanged()
1029
{
1030
    if (!block) {
1031
        QTextCursor cursor = textCursor();
1032
        Q_EMIT textChanged2(cursor.block().text(),cursor.positionInBlock());
1033
    }
1034
}
1035

1036
void ExpressionTextEdit::slotCompleteText(const QString& completionPrefix)
1037
{
1038
    QTextCursor cursor = textCursor();
1039
    int start,end;
1040
    completer->getPrefixRange(start,end);
1041
    int pos = cursor.positionInBlock();
1042
    if (pos < end) {
1043
        cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, end - pos);
1044
    }
1045
    cursor.movePosition(QTextCursor::PreviousCharacter,QTextCursor::KeepAnchor,end-start);
1046
    Base::FlagToggler<bool> flag(block,false);
1047
    cursor.insertText(completionPrefix);
1048
    completer->updatePrefixEnd(cursor.positionInBlock());
1049
}
1050

1051
void ExpressionTextEdit::keyPressEvent(QKeyEvent* e)
1052
{
1053
    Base::FlagToggler<bool> flag(block,true);
1054
    QPlainTextEdit::keyPressEvent(e);
1055
}
1056

1057
void ExpressionTextEdit::contextMenuEvent(QContextMenuEvent* event)
1058
{
1059
    QMenu* menu = createStandardContextMenu();
1060
    menu->addSeparator();
1061
    QAction* match = menu->addAction(tr("Exact match"));
1062

1063
    if (completer) {
1064
        match->setCheckable(true);
1065
        match->setChecked(completer->filterMode() == Qt::MatchStartsWith);
1066
    }
1067
    else {
1068
        match->setVisible(false);
1069
    }
1070

1071
    QAction* action = menu->exec(event->globalPos());
1072

1073
    if (completer) {
1074
        if (action == match)
1075
            setExactMatch(match->isChecked());
1076
    }
1077

1078
    delete menu;
1079
}
1080

1081
///////////////////////////////////////////////////////////////////////
1082

1083
ExpressionParameter* ExpressionParameter::instance()
1084
{
1085
    static auto inst = new ExpressionParameter();
1086
    return inst;
1087
}
1088

1089
bool ExpressionParameter::isCaseSensitive() const
1090
{
1091
    auto handle = GetApplication().GetParameterGroupByPath(
1092
                "User parameter:BaseApp/Preferences/Expression");
1093
    return handle->GetBool("CompleterCaseSensitive", false);
1094
}
1095

1096
bool ExpressionParameter::isExactMatch() const
1097
{
1098
    auto handle = GetApplication().GetParameterGroupByPath(
1099
                "User parameter:BaseApp/Preferences/Expression");
1100
    return handle->GetBool("CompleterMatchExact", false);
1101
}
1102

1103
#include "moc_ExpressionCompleter.cpp"
1104

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.