FreeCAD

Форк
0
/
CallTips.cpp 
783 строки · 30.4 Кб
1
/***************************************************************************
2
 *   Copyright (c) 2007 Werner Mayer <wmayer[at]users.sourceforge.net>     *
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 <QApplication>
27
# include <QKeyEvent>
28
# include <QLabel>
29
# include <QPlainTextEdit>
30
# include <QRegularExpression>
31
# include <QRegularExpressionMatch>
32
# include <QTextCursor>
33
# include <QToolTip>
34
#endif
35

36
#include <App/Property.h>
37
#include <App/PropertyContainer.h>
38
#include <App/PropertyContainerPy.h>
39
#include <App/Document.h>
40
#include <App/DocumentObject.h>
41
#include <App/DocumentPy.h>
42
#include <App/DocumentObjectPy.h>
43
#include <Base/Console.h>
44
#include <Base/Interpreter.h>
45
#include <Base/PyObjectBase.h>
46
#include <Gui/BitmapFactory.h>
47
#include <Gui/DocumentPy.h>
48

49
#include "CallTips.h"
50

51

52
Q_DECLARE_METATYPE( Gui::CallTip ) //< allows use of QVariant
53

54
namespace Gui
55
{
56

57
/**
58
 * template class Temporary.
59
 * Allows variable changes limited to a scope.
60
 */
61
template <typename TYPE>
62
class Temporary
63
{
64
public:
65
    Temporary( TYPE &var, const TYPE tmpVal )
66
      : _var(var), _saveVal(var)
67
    { var = tmpVal; }
68

69
    ~Temporary( )
70
    { _var = _saveVal; }
71

72
private:
73
    TYPE &_var;
74
    TYPE  _saveVal;
75
};
76

77
} /* namespace Gui */
78

79
using namespace Gui;
80

81
CallTipsList::CallTipsList(QPlainTextEdit* parent)
82
  :  QListWidget(parent), textEdit(parent), cursorPos(0), validObject(true), doCallCompletion(false)
83
{
84
    // make the user assume that the widget is active
85
    QPalette pal = parent->palette();
86
    pal.setColor(QPalette::Inactive, QPalette::Highlight, pal.color(QPalette::Active, QPalette::Highlight));
87
    pal.setColor(QPalette::Inactive, QPalette::HighlightedText, pal.color(QPalette::Active, QPalette::HighlightedText));
88
    parent->setPalette( pal );
89

90
    connect(this, &QListWidget::itemActivated, this, &CallTipsList::callTipItemActivated);
91

92
    hideKeys.append(Qt::Key_Space);
93
    hideKeys.append(Qt::Key_Exclam);
94
    hideKeys.append(Qt::Key_QuoteDbl);
95
    hideKeys.append(Qt::Key_NumberSign);
96
    hideKeys.append(Qt::Key_Dollar);
97
    hideKeys.append(Qt::Key_Percent);
98
    hideKeys.append(Qt::Key_Ampersand);
99
    hideKeys.append(Qt::Key_Apostrophe);
100
    hideKeys.append(Qt::Key_Asterisk);
101
    hideKeys.append(Qt::Key_Plus);
102
    hideKeys.append(Qt::Key_Comma);
103
    hideKeys.append(Qt::Key_Minus);
104
    hideKeys.append(Qt::Key_Period);
105
    hideKeys.append(Qt::Key_Slash);
106
    hideKeys.append(Qt::Key_Colon);
107
    hideKeys.append(Qt::Key_Semicolon);
108
    hideKeys.append(Qt::Key_Less);
109
    hideKeys.append(Qt::Key_Equal);
110
    hideKeys.append(Qt::Key_Greater);
111
    hideKeys.append(Qt::Key_Question);
112
    hideKeys.append(Qt::Key_At);
113
    hideKeys.append(Qt::Key_Backslash);
114

115
    compKeys.append(Qt::Key_ParenLeft);
116
    compKeys.append(Qt::Key_ParenRight);
117
    compKeys.append(Qt::Key_BracketLeft);
118
    compKeys.append(Qt::Key_BracketRight);
119
    compKeys.append(Qt::Key_BraceLeft);
120
    compKeys.append(Qt::Key_BraceRight);
121
}
122

123
CallTipsList::~CallTipsList() = default;
124

125
void CallTipsList::keyboardSearch(const QString& wordPrefix)
126
{
127
    // first search for the item that matches perfectly
128
    for (int i=0; i<count(); ++i) {
129
        QString text = item(i)->text();
130
        if (text.startsWith(wordPrefix)) {
131
            setCurrentRow(i);
132
            return;
133
        }
134
    }
135

136
    // now do a case insensitive comparison
137
    for (int i=0; i<count(); ++i) {
138
        QString text = item(i)->text();
139
        if (text.startsWith(wordPrefix, Qt::CaseInsensitive)) {
140
            setCurrentRow(i);
141
            return;
142
        }
143
    }
144

145
    if (currentItem())
146
        currentItem()->setSelected(false);
147
}
148

149
void CallTipsList::validateCursor()
150
{
151
    QTextCursor cursor = textEdit->textCursor();
152
    int currentPos = cursor.position();
153
    if (currentPos < this->cursorPos) {
154
        hide();
155
    }
156
    else {
157
        cursor.setPosition(this->cursorPos);
158
        cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
159
        QString word = cursor.selectedText();
160
        if (!word.isEmpty()) {
161
            // the following text might be an operator, brackets, ...
162
            const QChar underscore =  QLatin1Char('_');
163
            const QChar ch = word.at(0);
164
            if (!ch.isLetterOrNumber() && ch != underscore)
165
                word.clear();
166
        }
167
        if (currentPos > this->cursorPos+word.length()) {
168
            hide();
169
        }
170
        else if (!word.isEmpty()){
171
            // If the word is empty we should not allow to do a search,
172
            // otherwise we may select the next item which is not okay in this
173
            // context. This might happen if e.g. Shift is pressed.
174
            keyboardSearch(word);
175
        }
176
    }
177
}
178

179
QString CallTipsList::extractContext(const QString& line) const
180
{
181
    int len = line.size();
182
    int index = len-1;
183
    for (int i=0; i<len; i++) {
184
        int pos = len-1-i;
185
        const char ch = line.at(pos).toLatin1();
186
        if ((ch >= 48 && ch <= 57)  ||    // Numbers
187
            (ch >= 65 && ch <= 90)  ||    // Uppercase letters
188
            (ch >= 97 && ch <= 122) ||    // Lowercase letters
189
            (ch == '.') || (ch == '_') || // dot or underscore
190
            (ch == ' ') || (ch == '\t'))  // whitespace (between dot and text)
191
            index = pos;
192
        else
193
            break;
194
    }
195

196
    return line.mid(index);
197
}
198

199
QMap<QString, CallTip> CallTipsList::extractTips(const QString& context) const
200
{
201
    Base::PyGILStateLocker lock;
202
    QMap<QString, CallTip> tips;
203
    if (context.isEmpty())
204
        return tips;
205

206
    try {
207
        Py::Module module("__main__");
208
        Py::Dict dict = module.getDict();
209

210
        // this is used to filter out input of the form "1."
211
        QStringList items = context.split(QLatin1Char('.'));
212
        QString modname = items.front();
213
        items.pop_front();
214
        if (!dict.hasKey(std::string(modname.toLatin1())))
215
            return tips; // unknown object
216
        // Don't use hasattr & getattr because if a property is bound to a method this will be executed twice.
217
        PyObject* code = Py_CompileString(static_cast<const char*>(context.toLatin1()), "<CallTipsList>", Py_eval_input);
218
        if (!code) {
219
            PyErr_Clear();
220
            return tips;
221
        }
222

223
        PyObject* eval = nullptr;
224
        if (PyCode_Check(code)) {
225
            eval = PyEval_EvalCode(code, dict.ptr(), dict.ptr());
226
        }
227
        Py_DECREF(code);
228
        if (!eval) {
229
            PyErr_Clear();
230
            return tips;
231
        }
232
        Py::Object obj(eval, true);
233

234
        // Checks whether the type is a subclass of PyObjectBase because to get the doc string
235
        // of a member we must get it by its type instead of its instance otherwise we get the
236
        // wrong string, namely that of the type of the member.
237
        // Note: 3rd party libraries may use their own type object classes so that we cannot
238
        // reliably use Py::Type. To be on the safe side we should use Py::Object to assign
239
        // the used type object to.
240
        //Py::Object type = obj.type();
241
        Py::Object type(PyObject_Type(obj.ptr()), true);
242
        Py::Object inst = obj; // the object instance
243
        PyObject* typeobj = Base::getTypeAsObject(&Base::PyObjectBase::Type);
244
        PyObject* typedoc = Base::getTypeAsObject(&App::DocumentObjectPy::Type);
245
        PyObject* basetype = Base::getTypeAsObject(&PyBaseObject_Type);
246

247
        if (PyObject_IsSubclass(type.ptr(), typedoc) == 1) {
248
            // From the template Python object we don't query its type object because there we keep
249
            // a list of additional methods that we won't see otherwise. But to get the correct doc
250
            // strings we query the type's dict in the class itself.
251
            // To see if we have a template Python object we check for the existence of '__fc_template__'
252
            // See also: FeaturePythonPyT
253
            if (!obj.hasAttr("__fc_template__")) {
254
                obj = type;
255
            }
256
        }
257
        else if (PyObject_IsSubclass(type.ptr(), typeobj) == 1) {
258
            obj = type;
259
        }
260
        else if (PyObject_IsInstance(obj.ptr(), basetype) == 1) {
261
            // New style class which can be a module, type, list, tuple, int, float, ...
262
            // Make sure it's not a type object
263
            PyObject* typetype = Base::getTypeAsObject(&PyType_Type);
264
            if (PyObject_IsInstance(obj.ptr(), typetype) != 1) {
265
                // For wrapped objects with PySide2 use the object, not its type
266
                // as otherwise attributes added at runtime won't be listed (e.g. MainWindowPy)
267
                QString typestr(QLatin1String(Py_TYPE(obj.ptr())->tp_name));
268

269
                // this should be now a user-defined Python class
270
                // http://stackoverflow.com/questions/12233103/in-python-at-runtime-determine-if-an-object-is-a-class-old-and-new-type-instan
271
                if (!typestr.startsWith(QLatin1String("PySide")) && Py_TYPE(obj.ptr())->tp_flags & Py_TPFLAGS_HEAPTYPE) {
272
                    obj = type;
273
                }
274
            }
275
        }
276

277
        // If we have an instance of PyObjectBase then determine whether it's valid or not
278
        if (PyObject_IsInstance(inst.ptr(), typeobj) == 1) {
279
            auto baseobj = static_cast<Base::PyObjectBase*>(inst.ptr());
280
            validObject = baseobj->isValid();
281
        }
282
        else {
283
            // PyObject_IsInstance might set an exception
284
            PyErr_Clear();
285
        }
286

287
        Py::List list(obj.dir());
288

289
        // If we derive from PropertyContainerPy we can search for the properties in the
290
        // C++ twin class.
291
        PyObject* proptypeobj = Base::getTypeAsObject(&App::PropertyContainerPy::Type);
292
        if (PyObject_IsSubclass(type.ptr(), proptypeobj) == 1) {
293
            // These are the attributes of the instance itself which are NOT accessible by
294
            // its type object
295
            extractTipsFromProperties(inst, tips);
296
        }
297

298
        // If we derive from App::DocumentPy we have direct access to the objects by their internal
299
        // names. So, we add these names to the list, too.
300
        PyObject* appdoctypeobj = Base::getTypeAsObject(&App::DocumentPy::Type);
301
        if (PyObject_IsSubclass(type.ptr(), appdoctypeobj) == 1) {
302
            auto docpy = static_cast<App::DocumentPy*>(inst.ptr());
303
            auto document = docpy->getDocumentPtr();
304
            // Make sure that the C++ object is alive
305
            if (document) {
306
                std::vector<App::DocumentObject*> objects = document->getObjects();
307
                Py::List list;
308
                for (const auto & object : objects)
309
                    list.append(Py::String(object->getNameInDocument()));
310
                extractTipsFromObject(inst, list, tips);
311
            }
312
        }
313

314
        // If we derive from Gui::DocumentPy we have direct access to the objects by their internal
315
        // names. So, we add these names to the list, too.
316
        PyObject* guidoctypeobj = Base::getTypeAsObject(&Gui::DocumentPy::Type);
317
        if (PyObject_IsSubclass(type.ptr(), guidoctypeobj) == 1) {
318
            auto docpy = static_cast<Gui::DocumentPy*>(inst.ptr());
319
            if (docpy->getDocumentPtr()) {
320
                App::Document* document = docpy->getDocumentPtr()->getDocument();
321
                // Make sure that the C++ object is alive
322
                if (document) {
323
                    std::vector<App::DocumentObject*> objects = document->getObjects();
324
                    Py::List list;
325
                    for (const auto & object : objects)
326
                        list.append(Py::String(object->getNameInDocument()));
327
                    extractTipsFromObject(inst, list, tips);
328
                }
329
            }
330
        }
331

332
        // These are the attributes from the type object
333
        extractTipsFromObject(obj, list, tips);
334
    }
335
    catch (Py::Exception& e) {
336
        // Just clear the Python exception
337
        e.clear();
338
    }
339

340
    return tips;
341
}
342

343
bool shibokenMayCrash(void)
344
{
345
    // Shiboken 6.4.8 to 6.7.3 crash if we try to read their object
346
    // attributes without a current stack frame.
347
    // FreeCAD issue: https://github.com/FreeCAD/FreeCAD/issues/14101
348
    // Qt issue: https://bugreports.qt.io/browse/PYSIDE-2796
349

350
    Py::Module shiboken("shiboken6");
351
    Py::Tuple version(shiboken.getAttr("__version_info__"));
352
    int major = Py::Long(version.getItem(0));
353
    int minor = Py::Long(version.getItem(1));
354
    int patch = Py::Long(version.getItem(2));
355
    bool brokenVersion = (major == 6 && minor >= 4 && minor < 8);
356
    bool fixedVersion = (major == 6 && minor == 7 && patch > 2);
357
    return brokenVersion && !fixedVersion;
358
}
359

360
Py::Object CallTipsList::getAttrWorkaround(Py::Object& obj, Py::String& name) const
361
{
362
    QString typestr(QLatin1String(Py_TYPE(obj.ptr())->tp_name));
363
    bool hasWorkingGetAttr =
364
        !(typestr == QLatin1String("Shiboken.ObjectType") && shibokenMayCrash());
365

366
    if (hasWorkingGetAttr) {
367
        return obj.getAttr(name.as_string());
368
    }
369

370
    Py::Dict globals;
371
    Py::Dict locals;
372
    locals.setItem("obj", obj);
373
    locals.setItem("attr", name);
374

375
    Py::Object bouncer(Py_CompileString("getattr(obj, attr)", "<CallTipsList>", Py_eval_input));
376
    Py::Object attr(PyEval_EvalCode(bouncer.ptr(), globals.ptr(), locals.ptr()));
377

378
    return attr;
379
}
380

381
void CallTipsList::extractTipsFromObject(Py::Object& obj, Py::List& list, QMap<QString, CallTip>& tips) const
382
{
383
    for (Py::List::iterator it = list.begin(); it != list.end(); ++it) {
384
        try {
385
            Py::String attrname(*it);
386
            std::string name(attrname.as_string());
387

388
            Py::Object attr = getAttrWorkaround(obj, attrname);
389
            if (!attr.ptr()) {
390
                Base::Console().Log("Python attribute '%s' returns null!\n", name.c_str());
391
                continue;
392
            }
393

394
            CallTip tip;
395
            QString str = QString::fromLatin1(name.c_str());
396
            tip.name = str;
397

398
            if (attr.isCallable()) {
399
                PyObject* basetype = Base::getTypeAsObject(&PyBaseObject_Type);
400
                if (PyObject_IsSubclass(attr.ptr(), basetype) == 1) {
401
                    tip.type = CallTip::Class;
402
                }
403
                else {
404
                    PyErr_Clear(); // PyObject_IsSubclass might set an exception
405
                    tip.type = CallTip::Method;
406
                }
407
            }
408
            else if (PyModule_Check(attr.ptr())) {
409
                tip.type = CallTip::Module;
410
            }
411
            else {
412
                tip.type = CallTip::Member;
413
            }
414

415
            if (str == QLatin1String("__doc__") && attr.isString()) {
416
                Py::Object help = attr;
417
                if (help.isString()) {
418
                    Py::String doc(help);
419
                    QString longdoc = QString::fromUtf8(doc.as_string().c_str());
420
                    int pos = longdoc.indexOf(QLatin1Char('\n'));
421
                    pos = qMin(pos, 70);
422
                    if (pos < 0)
423
                        pos = qMin(longdoc.length(), 70);
424
                    tip.description = stripWhiteSpace(longdoc);
425
                    tip.parameter = longdoc.left(pos);
426
                }
427
            }
428
            else if (attr.hasAttr("__doc__")) {
429
                Py::Object help = attr.getAttr("__doc__");
430
                if (help.isString()) {
431
                    Py::String doc(help);
432
                    QString longdoc = QString::fromUtf8(doc.as_string().c_str());
433
                    int pos = longdoc.indexOf(QLatin1Char('\n'));
434
                    pos = qMin(pos, 70);
435
                    if (pos < 0)
436
                        pos = qMin(longdoc.length(), 70);
437
                    tip.description = stripWhiteSpace(longdoc);
438
                    tip.parameter = longdoc.left(pos);
439
                }
440
            }
441

442
            // Do not override existing items
443
            QMap<QString, CallTip>::iterator pos = tips.find(str);
444
            if (pos == tips.end())
445
                tips[str] = tip;
446
        }
447
        catch (Py::Exception& e) {
448
            // Just clear the Python exception
449
            e.clear();
450
        }
451
    }
452
}
453

454
void CallTipsList::extractTipsFromProperties(Py::Object& obj, QMap<QString, CallTip>& tips) const
455
{
456
    auto cont = static_cast<App::PropertyContainerPy*>(obj.ptr());
457
    App::PropertyContainer* container = cont->getPropertyContainerPtr();
458
    // Make sure that the C++ object is alive
459
    if (!container)
460
        return;
461
    std::map<std::string,App::Property*> Map;
462
    container->getPropertyMap(Map);
463

464
    for (const auto & It : Map) {
465
        CallTip tip;
466
        QString str = QString::fromLatin1(It.first.c_str());
467
        tip.name = str;
468
        tip.type = CallTip::Property;
469
        QString longdoc = QString::fromUtf8(container->getPropertyDocumentation(It.second));
470
        // a point, mesh or shape property
471
        if (It.second->isDerivedFrom(Base::Type::fromName("App::PropertyComplexGeoData"))) {
472
            Py::Object data(It.second->getPyObject(), true);
473
            if (data.hasAttr("__doc__")) {
474
                Py::Object help = data.getAttr("__doc__");
475
                if (help.isString()) {
476
                    Py::String doc(help);
477
                    longdoc = QString::fromUtf8(doc.as_string().c_str());
478
                }
479
            }
480
        }
481
        if (!longdoc.isEmpty()) {
482
            int pos = longdoc.indexOf(QLatin1Char('\n'));
483
            pos = qMin(pos, 70);
484
            if (pos < 0)
485
                pos = qMin(longdoc.length(), 70);
486
            tip.description = stripWhiteSpace(longdoc);
487
            tip.parameter = longdoc.left(pos);
488
        }
489
        tips[str] = tip;
490
    }
491
}
492

493
void CallTipsList::showTips(const QString& line)
494
{
495
    // search only once
496
    static QPixmap type_module_icon = BitmapFactory().pixmap("ClassBrowser/type_module.svg");
497
    static QPixmap type_class_icon = BitmapFactory().pixmap("ClassBrowser/type_class.svg");
498
    static QPixmap method_icon = BitmapFactory().pixmap("ClassBrowser/method.svg");
499
    static QPixmap member_icon = BitmapFactory().pixmap("ClassBrowser/member.svg");
500
    static QPixmap property_icon = BitmapFactory().pixmap("ClassBrowser/property.svg");
501

502
    // object is in error state
503
    static QPixmap forbidden_icon(Gui::BitmapFactory().pixmapFromSvg("forbidden", property_icon.size() / 4));
504
    static QPixmap forbidden_type_module_icon = BitmapFactory().merge(type_module_icon,forbidden_icon,BitmapFactoryInst::BottomLeft);
505
    static QPixmap forbidden_type_class_icon = BitmapFactory().merge(type_class_icon,forbidden_icon,BitmapFactoryInst::BottomLeft);
506
    static QPixmap forbidden_method_icon = BitmapFactory().merge(method_icon,forbidden_icon,BitmapFactoryInst::BottomLeft);
507
    static QPixmap forbidden_member_icon = BitmapFactory().merge(member_icon,forbidden_icon,BitmapFactoryInst::BottomLeft);
508
    static QPixmap forbidden_property_icon = BitmapFactory().merge(property_icon,forbidden_icon,BitmapFactoryInst::BottomLeft);
509

510
    this->validObject = true;
511
    QString context = extractContext(line);
512
    context = context.simplified();
513
    QMap<QString, CallTip> tips = extractTips(context);
514
    clear();
515
    for (QMap<QString, CallTip>::Iterator it = tips.begin(); it != tips.end(); ++it) {
516
        addItem(it.key());
517
        QListWidgetItem *item = this->item(this->count()-1);
518
        item->setData(Qt::ToolTipRole, QVariant(it.value().description));
519
        item->setData(Qt::UserRole, QVariant::fromValue( it.value() )); //< store full CallTip data
520
        switch (it.value().type)
521
        {
522
        case CallTip::Module:
523
            {
524
                item->setIcon((this->validObject ? type_module_icon : forbidden_type_module_icon));
525
            }   break;
526
        case CallTip::Class:
527
            {
528
                item->setIcon((this->validObject ? type_class_icon : forbidden_type_class_icon));
529
            }   break;
530
        case CallTip::Method:
531
            {
532
                item->setIcon((this->validObject ? method_icon : forbidden_method_icon));
533
            }   break;
534
        case CallTip::Member:
535
            {
536
                item->setIcon((this->validObject ? member_icon : forbidden_member_icon));
537
            }   break;
538
        case CallTip::Property:
539
            {
540
                item->setIcon((this->validObject ? property_icon : forbidden_property_icon));
541
            }   break;
542
        default:
543
            break;
544
        }
545
    }
546

547
    if (count()==0)
548
        return; // nothing found
549

550
    // get the minimum width and height of the box
551
    int h = 0;
552
    int w = 0;
553
    for (int i = 0; i < count(); ++i) {
554
        QRect r = visualItemRect(item(i));
555
        w = qMax(w, r.width());
556
        h += r.height();
557
    }
558

559
    // Add an offset
560
    w += 2*frameWidth();
561
    h += 2*frameWidth();
562

563
    // get the start position of the word prefix
564
    QTextCursor cursor = textEdit->textCursor();
565
    this->cursorPos = cursor.position();
566
    QRect rect = textEdit->cursorRect(cursor);
567
    int posX = rect.x();
568
    int posY = rect.y();
569
    int boxH = h;
570

571
    // Decide whether to show downstairs or upstairs
572
    if (posY > textEdit->viewport()->height()/2) {
573
        h = qMin(qMin(h,posY), 250);
574
        if (h < boxH)
575
            w += textEdit->style()->pixelMetric(QStyle::PM_ScrollBarExtent);
576
        setGeometry(posX,posY-h, w, h);
577
    }
578
    else {
579
        h = qMin(qMin(h,textEdit->viewport()->height()-fontMetrics().height()-posY), 250);
580
        if (h < boxH)
581
            w += textEdit->style()->pixelMetric(QStyle::PM_ScrollBarExtent);
582
        setGeometry(posX, posY+fontMetrics().height(), w, h);
583
    }
584

585
    setCurrentRow(0);
586
    show();
587
}
588

589
void CallTipsList::showEvent(QShowEvent* e)
590
{
591
    QListWidget::showEvent(e);
592
    // install this object to filter timer events for the tooltip label
593
    qApp->installEventFilter(this);
594
}
595

596
void CallTipsList::hideEvent(QHideEvent* e)
597
{
598
    QListWidget::hideEvent(e);
599
    qApp->removeEventFilter(this);
600
}
601

602
/**
603
 * Get all incoming events of the text edit and redirect some of them, like key up and
604
 * down, mouse press events, ... to the widget itself.
605
 */
606
bool CallTipsList::eventFilter(QObject * watched, QEvent * event)
607
{
608
    // This is a trick to avoid to hide the tooltip window after the defined time span
609
    // of 10 seconds. We just filter out all timer events to keep the label visible.
610
    if (watched->inherits("QLabel")) {
611
        auto label = qobject_cast<QLabel*>(watched);
612
        // Ignore the timer events to prevent from being closed
613
        if (label->windowFlags() & Qt::ToolTip && event->type() == QEvent::Timer)
614
            return true;
615
    }
616
    if (isVisible() && watched == textEdit->viewport()) {
617
        if (event->type() == QEvent::MouseButtonPress)
618
            hide();
619
    }
620
    else if (isVisible() && watched == textEdit) {
621
        if (event->type() == QEvent::KeyPress) {
622
            auto ke = static_cast<QKeyEvent*>(event);
623
            if (ke->key() == Qt::Key_Up || ke->key() == Qt::Key_Down) {
624
                keyPressEvent(ke);
625
                return true;
626
            }
627
            else if (ke->key() == Qt::Key_PageUp || ke->key() == Qt::Key_PageDown) {
628
                keyPressEvent(ke);
629
                return true;
630
            }
631
            else if (ke->key() == Qt::Key_Escape) {
632
                hide();
633
                return true;
634
            }
635
            else if ((ke->key() == Qt::Key_Minus) && (ke->modifiers() & Qt::ShiftModifier)) {
636
                // do nothing here, but this check is needed to ignore underscore
637
                // which in Qt 4.8 gives Key_Minus instead of Key_Underscore
638
            }
639
            else if (this->hideKeys.indexOf(ke->key()) > -1) {
640
                Q_EMIT itemActivated(currentItem());
641
                return false;
642
            }
643
            else if (ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter) {
644
                Q_EMIT itemActivated(currentItem());
645
                return true;
646
            }
647
            else if (ke->key() == Qt::Key_Tab) {
648
                // enable call completion for activating items
649
                Temporary<bool> tmp( this->doCallCompletion, true ); //< previous state restored on scope exit
650
                Q_EMIT itemActivated( currentItem() );
651
                return true;
652
            }
653
            else if (this->compKeys.indexOf(ke->key()) > -1) {
654
                Q_EMIT itemActivated(currentItem());
655
                return false;
656
            }
657
            else if (ke->key() == Qt::Key_Shift || ke->key() == Qt::Key_Control ||
658
                     ke->key() == Qt::Key_Meta || ke->key() == Qt::Key_Alt ||
659
                     ke->key() == Qt::Key_AltGr) {
660
                // filter these meta keys to avoid to call keyboardSearch()
661
                return true;
662
            }
663
        }
664
        else if (event->type() == QEvent::KeyRelease) {
665
            auto ke = static_cast<QKeyEvent*>(event);
666
            if (ke->key() == Qt::Key_Up || ke->key() == Qt::Key_Down ||
667
                ke->key() == Qt::Key_PageUp || ke->key() == Qt::Key_PageDown) {
668
                QList<QListWidgetItem *> items = selectedItems();
669
                if (!items.isEmpty()) {
670
                    QPoint p(width(), 0);
671
                    QString text = items.front()->toolTip();
672
                    if (!text.isEmpty()){
673
                        QToolTip::showText(mapToGlobal(p), text);
674
                    } else {
675
                        QToolTip::showText(p, QString());
676
                    }
677
                }
678
                return true;
679
            }
680
        }
681
        else if (event->type() == QEvent::FocusOut) {
682
            if (!hasFocus())
683
                hide();
684
        }
685
    }
686

687
    return QListWidget::eventFilter(watched, event);
688
}
689

690
void CallTipsList::callTipItemActivated(QListWidgetItem *item)
691
{
692
    hide();
693
    if (!item->isSelected())
694
        return;
695

696
    QString text = item->text();
697
    QTextCursor cursor = textEdit->textCursor();
698
    cursor.setPosition(this->cursorPos);
699
    cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
700
    QString sel = cursor.selectedText();
701
    if (!sel.isEmpty()) {
702
        // in case the cursor moved too far on the right side
703
        const QChar underscore =  QLatin1Char('_');
704
        const QChar ch = sel.at(sel.size()-1);
705
        if (!ch.isLetterOrNumber() && ch != underscore)
706
            cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor);
707
    }
708
    cursor.insertText( text );
709

710
    // get CallTip from item's UserRole-data
711
    auto callTip = item->data(Qt::UserRole).value<CallTip>();
712

713
    // if call completion enabled and we've something callable (method or class constructor) ...
714
    if (this->doCallCompletion
715
     && (callTip.type == CallTip::Method || callTip.type == CallTip::Class))
716
    {
717
      cursor.insertText( QLatin1String("()") ); //< just append parenthesis to identifier even inserted.
718

719
      /**
720
       * Try to find out if call needs arguments.
721
       * For this we search the description for appropriate hints ...
722
       */
723
      QRegularExpression argumentMatcher( QRegularExpression::escape( callTip.name ) + QLatin1String(R"(\s*\(\s*\w+.*\))") );
724
      argumentMatcher.setPatternOptions( QRegularExpression::InvertedGreedinessOption ); //< set regex non-greedy!
725
      if (argumentMatcher.match( callTip.description ).hasMatch())
726
      {
727
        // if arguments are needed, we just move the cursor one left, to between the parentheses.
728
        cursor.movePosition( QTextCursor::Left, QTextCursor::MoveAnchor, 1 );
729
        textEdit->setTextCursor( cursor );
730
      }
731
    }
732
    textEdit->ensureCursorVisible();
733

734
    QRect rect = textEdit->cursorRect(cursor);
735
    int posX = rect.x();
736
    int posY = rect.y();
737

738
    QPoint p(posX, posY);
739
    p = textEdit->mapToGlobal(p);
740
    QToolTip::showText( p, callTip.parameter );
741
}
742

743
QString CallTipsList::stripWhiteSpace(const QString& str) const
744
{
745
    QString stripped = str;
746
    QStringList lines = str.split(QLatin1String("\n"));
747
    int minspace=INT_MAX;
748
    int line=0;
749
    for (QStringList::iterator it = lines.begin(); it != lines.end(); ++it, ++line) {
750
        if (it->size() > 0 && line > 0) {
751
            int space = 0;
752
            for (int i=0; i<it->size(); i++) {
753
                if ((*it)[i] == QLatin1Char('\t'))
754
                    space++;
755
                else
756
                    break;
757
            }
758

759
            if (it->size() > space)
760
                minspace = std::min<int>(minspace, space);
761
        }
762
    }
763

764
    // remove all leading tabs from each line
765
    if (minspace > 0 && minspace < INT_MAX) {
766
        int line=0;
767
        QStringList strippedlines;
768
        for (QStringList::iterator it = lines.begin(); it != lines.end(); ++it, ++line) {
769
            if (line == 0 && !it->isEmpty()) {
770
                strippedlines << *it;
771
            }
772
            else if (it->size() > 0 && line > 0) {
773
                strippedlines << it->mid(minspace);
774
            }
775
        }
776

777
        stripped = strippedlines.join(QLatin1String("\n"));
778
    }
779

780
    return stripped;
781
}
782

783
#include "moc_CallTips.cpp"
784

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

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

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

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