1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25 
26 #include "variablechooser.h"
27 
28 #include "fancylineedit.h"
29 #include "headerviewstretcher.h" // IconButton
30 #include "macroexpander.h"
31 #include "treemodel.h"
32 #include "qtcassert.h"
33 #include "utilsicons.h"
34 
35 #include <QApplication>
36 #include <QAbstractItemModel>
37 #include <QHeaderView>
38 #include <QLabel>
39 #include <QLineEdit>
40 #include <QListWidgetItem>
41 #include <QMenu>
42 #include <QPlainTextEdit>
43 #include <QPointer>
44 #include <QScrollBar>
45 #include <QSortFilterProxyModel>
46 #include <QTextEdit>
47 #include <QTimer>
48 #include <QTreeView>
49 #include <QVBoxLayout>
50 #include <QVector>
51 
52 namespace Utils {
53 namespace Internal {
54 
55 enum {
56     UnexpandedTextRole = Qt::UserRole,
57     ExpandedTextRole
58 };
59 
60 class VariableTreeView : public QTreeView
61 {
62 public:
VariableTreeView(QWidget * parent,VariableChooserPrivate * target)63     VariableTreeView(QWidget *parent, VariableChooserPrivate *target)
64         : QTreeView(parent), m_target(target)
65     {
66         setAttribute(Qt::WA_MacSmallSize);
67         setAttribute(Qt::WA_MacShowFocusRect, false);
68         setIndentation(indentation() * 7/10);
69         header()->hide();
70         new HeaderViewStretcher(header(), 0);
71     }
72 
73     void contextMenuEvent(QContextMenuEvent *ev) override;
74 
75     void currentChanged(const QModelIndex &current, const QModelIndex &previous) override;
76 
77 private:
78     VariableChooserPrivate *m_target;
79 };
80 
81 class VariableSortFilterProxyModel : public QSortFilterProxyModel
82 {
83 public:
VariableSortFilterProxyModel(QObject * parent)84     explicit VariableSortFilterProxyModel(QObject *parent) : QSortFilterProxyModel(parent) {}
filterAcceptsRow(int sourceRow,const QModelIndex & sourceParent) const85     bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
86     {
87         const QModelIndex index = sourceModel()->index(sourceRow, filterKeyColumn(), sourceParent);
88         if (!index.isValid())
89             return false;
90 
91         const QRegularExpression regexp = filterRegularExpression();
92         if (regexp.pattern().isEmpty() || sourceModel()->rowCount(index) > 0)
93             return true;
94 
95         const QString displayText = index.data(Qt::DisplayRole).toString();
96         return displayText.contains(regexp);
97     }
98 };
99 
100 class VariableChooserPrivate : public QObject
101 {
102 public:
103     VariableChooserPrivate(VariableChooser *parent);
104 
createIconButton()105     void createIconButton()
106     {
107         m_iconButton = new IconButton;
108         m_iconButton->setIcon(Utils::Icons::REPLACE.icon());
109         m_iconButton->setToolTip(VariableChooser::tr("Insert Variable"));
110         m_iconButton->hide();
111         connect(m_iconButton.data(), static_cast<void(QAbstractButton::*)(bool)>(&QAbstractButton::clicked),
112                 this, &VariableChooserPrivate::updatePositionAndShow);
113     }
114 
115     void updateDescription(const QModelIndex &index);
116     void updateCurrentEditor(QWidget *old, QWidget *widget);
117     void handleItemActivated(const QModelIndex &index);
118     void insertText(const QString &variable);
119     void updatePositionAndShow(bool);
120     void updateFilter(const QString &filterText);
121 
122     QWidget *currentWidget() const;
123 
124     int buttonMargin() const;
125     void updateButtonGeometry();
126 
127 public:
128     VariableChooser *q;
129     TreeModel<> m_model;
130 
131     QPointer<QLineEdit> m_lineEdit;
132     QPointer<QTextEdit> m_textEdit;
133     QPointer<QPlainTextEdit> m_plainTextEdit;
134     QPointer<IconButton> m_iconButton;
135 
136     Utils::FancyLineEdit *m_variableFilter;
137     VariableTreeView *m_variableTree;
138     QLabel *m_variableDescription;
139     QSortFilterProxyModel *m_sortModel;
140     QString m_defaultDescription;
141     QByteArray m_currentVariableName; // Prevent recursive insertion of currently expanded item
142 };
143 
144 class VariableGroupItem : public TreeItem
145 {
146 public:
147     VariableGroupItem() = default;
148 
data(int column,int role) const149     QVariant data(int column, int role) const override
150     {
151         if (role == Qt::DisplayRole || role == Qt::EditRole) {
152             if (column == 0)
153                 if (MacroExpander *expander = m_provider())
154                     return expander->displayName();
155         }
156 
157         return QVariant();
158     }
159 
canFetchMore() const160     bool canFetchMore() const override
161     {
162         return !m_populated;
163     }
164 
fetchMore()165     void fetchMore() override
166     {
167         if (MacroExpander *expander = m_provider())
168             populateGroup(expander);
169         m_populated = true;
170     }
171 
172     void populateGroup(MacroExpander *expander);
173 
174 public:
175     VariableChooserPrivate *m_chooser = nullptr; // Not owned.
176     bool m_populated = false;
177     MacroExpanderProvider m_provider;
178 };
179 
180 class VariableItem : public TypedTreeItem<TreeItem, VariableGroupItem>
181 {
182 public:
183     VariableItem() = default;
184 
flags(int) const185     Qt::ItemFlags flags(int) const override
186     {
187         if (m_variable == parent()->m_chooser->m_currentVariableName)
188             return Qt::ItemIsSelectable;
189         return Qt::ItemIsSelectable|Qt::ItemIsEnabled;
190     }
191 
data(int column,int role) const192     QVariant data(int column, int role) const override
193     {
194         if (role == Qt::DisplayRole || role == Qt::EditRole) {
195             if (column == 0)
196                 return m_variable;
197         }
198 
199         if (role == Qt::ToolTipRole) {
200             QString description = m_expander->variableDescription(m_variable);
201             const QString value = m_expander->value(m_variable).toHtmlEscaped();
202             if (!value.isEmpty())
203                 description += QLatin1String("<p>") + VariableChooser::tr("Current Value: %1").arg(value);
204             return description;
205         }
206 
207         if (role == UnexpandedTextRole)
208             return QString::fromUtf8("%{" + m_variable + '}');
209 
210         if (role == ExpandedTextRole)
211             return m_expander->expand(QString::fromUtf8("%{" + m_variable + '}'));
212 
213         return QVariant();
214     }
215 
216 public:
217     MacroExpander *m_expander;
218     QByteArray m_variable;
219 };
220 
contextMenuEvent(QContextMenuEvent * ev)221 void VariableTreeView::contextMenuEvent(QContextMenuEvent *ev)
222 {
223     const QModelIndex index = indexAt(ev->pos());
224 
225     QString unexpandedText = index.data(UnexpandedTextRole).toString();
226     QString expandedText = index.data(ExpandedTextRole).toString();
227 
228     QMenu menu;
229     QAction *insertUnexpandedAction = nullptr;
230     QAction *insertExpandedAction = nullptr;
231 
232     if (unexpandedText.isEmpty()) {
233         insertUnexpandedAction = menu.addAction(VariableChooser::tr("Insert Unexpanded Value"));
234         insertUnexpandedAction->setEnabled(false);
235     } else {
236         insertUnexpandedAction = menu.addAction(VariableChooser::tr("Insert \"%1\"").arg(unexpandedText));
237     }
238 
239     if (expandedText.isEmpty()) {
240         insertExpandedAction = menu.addAction(VariableChooser::tr("Insert Expanded Value"));
241         insertExpandedAction->setEnabled(false);
242     } else {
243         insertExpandedAction = menu.addAction(VariableChooser::tr("Insert \"%1\"").arg(expandedText));
244     }
245 
246 
247     QAction *act = menu.exec(ev->globalPos());
248 
249     if (act == insertUnexpandedAction)
250         m_target->insertText(unexpandedText);
251     else if (act == insertExpandedAction)
252         m_target->insertText(expandedText);
253 }
254 
currentChanged(const QModelIndex & current,const QModelIndex & previous)255 void VariableTreeView::currentChanged(const QModelIndex &current, const QModelIndex &previous)
256 {
257     m_target->updateDescription(current);
258     QTreeView::currentChanged(current, previous);
259 }
260 
VariableChooserPrivate(VariableChooser * parent)261 VariableChooserPrivate::VariableChooserPrivate(VariableChooser *parent)
262     : q(parent),
263       m_lineEdit(nullptr),
264       m_textEdit(nullptr),
265       m_plainTextEdit(nullptr),
266       m_iconButton(nullptr),
267       m_variableFilter(nullptr),
268       m_variableTree(nullptr),
269       m_variableDescription(nullptr)
270 {
271     m_defaultDescription = VariableChooser::tr("Select a variable to insert.");
272 
273     m_variableFilter = new Utils::FancyLineEdit(q);
274     m_variableTree = new VariableTreeView(q, this);
275     m_variableDescription = new QLabel(q);
276 
277     m_variableFilter->setFiltering(true);
278 
279     m_sortModel = new VariableSortFilterProxyModel(this);
280     m_sortModel->setSourceModel(&m_model);
281     m_sortModel->sort(0);
282     m_sortModel->setFilterKeyColumn(0);
283     m_sortModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
284     m_variableTree->setModel(m_sortModel);
285 
286     m_variableDescription->setText(m_defaultDescription);
287     m_variableDescription->setMinimumSize(QSize(0, 60));
288     m_variableDescription->setAlignment(Qt::AlignLeft|Qt::AlignTop);
289     m_variableDescription->setWordWrap(true);
290     m_variableDescription->setAttribute(Qt::WA_MacSmallSize);
291     m_variableDescription->setTextInteractionFlags(Qt::TextBrowserInteraction);
292 
293     auto verticalLayout = new QVBoxLayout(q);
294     verticalLayout->setContentsMargins(3, 3, 3, 12);
295     verticalLayout->addWidget(m_variableFilter);
296     verticalLayout->addWidget(m_variableTree);
297     verticalLayout->addWidget(m_variableDescription);
298 
299     connect(m_variableFilter, &QLineEdit::textChanged,
300             this, &VariableChooserPrivate::updateFilter);
301     connect(m_variableTree, &QTreeView::activated,
302             this, &VariableChooserPrivate::handleItemActivated);
303     connect(qobject_cast<QApplication *>(qApp), &QApplication::focusChanged,
304             this, &VariableChooserPrivate::updateCurrentEditor);
305     updateCurrentEditor(nullptr, QApplication::focusWidget());
306 }
307 
populateGroup(MacroExpander * expander)308 void VariableGroupItem::populateGroup(MacroExpander *expander)
309 {
310     if (!expander)
311         return;
312 
313     foreach (const QByteArray &variable, expander->visibleVariables()) {
314         auto item = new VariableItem;
315         item->m_variable = variable;
316         item->m_expander = expander;
317         appendChild(item);
318     }
319 
320     foreach (const MacroExpanderProvider &subProvider, expander->subProviders()) {
321         if (!subProvider)
322             continue;
323         if (expander->isAccumulating()) {
324             populateGroup(subProvider());
325         } else {
326             auto item = new VariableGroupItem;
327             item->m_chooser = m_chooser;
328             item->m_provider = subProvider;
329             appendChild(item);
330         }
331     }
332 }
333 
334 } // namespace Internal
335 
336 using namespace Internal;
337 
338 /*!
339     \class Utils::VariableChooser
340     \inheaderfile coreplugin/variablechooser.h
341     \inmodule QtCreator
342 
343     \brief The VariableChooser class is used to add a tool window for selecting \QC variables
344     to line edits, text edits or plain text edits.
345 
346     If you allow users to add \QC variables to strings that are specified in your UI, for example
347     when users can provide a string through a text control, you should add a variable chooser to it.
348     The variable chooser allows users to open a tool window that contains the list of
349     all available variables together with a description. Double-clicking a variable inserts the
350     corresponding string into the corresponding text control like a line edit.
351 
352     \image variablechooser.png "External Tools Preferences with Variable Chooser"
353 
354     The variable chooser monitors focus changes of all children of its parent widget.
355     When a text control gets focus, the variable chooser checks if it has variable support set.
356     If the control supports variables,
357     a tool button which opens the variable chooser is shown in it while it has focus.
358 
359     Supported text controls are QLineEdit, QTextEdit and QPlainTextEdit.
360 
361     The variable chooser is deleted when its parent widget is deleted.
362 
363     Example:
364     \code
365     QWidget *myOptionsContainerWidget = new QWidget;
366     new Utils::VariableChooser(myOptionsContainerWidget)
367     QLineEdit *myLineEditOption = new QLineEdit(myOptionsContainerWidget);
368     myOptionsContainerWidget->layout()->addWidget(myLineEditOption);
369     Utils::VariableChooser::addVariableSupport(myLineEditOption);
370     \endcode
371 */
372 
373 /*!
374  * \internal
375  * \variable VariableChooser::kVariableSupportProperty
376  * Property name that is checked for deciding if a widget supports \QC variables.
377  * Can be manually set with
378  * \c{textcontrol->setProperty(VariableChooser::kVariableSupportProperty, true)}
379  */
380 const char kVariableSupportProperty[] = "QtCreator.VariableSupport";
381 const char kVariableNameProperty[] = "QtCreator.VariableName";
382 
383 /*!
384  * Creates a variable chooser that tracks all children of \a parent for variable support.
385  * Ownership is also transferred to \a parent.
386  */
VariableChooser(QWidget * parent)387 VariableChooser::VariableChooser(QWidget *parent) :
388     QWidget(parent),
389     d(new VariableChooserPrivate(this))
390 {
391     setWindowTitle(tr("Variables"));
392     setWindowFlags(Qt::Tool);
393     setFocusPolicy(Qt::StrongFocus);
394     setFocusProxy(d->m_variableTree);
395     setGeometry(QRect(0, 0, 400, 500));
396     addMacroExpanderProvider([]() { return globalMacroExpander(); });
397 }
398 
399 /*!
400  * \internal
401  */
~VariableChooser()402 VariableChooser::~VariableChooser()
403 {
404     delete d->m_iconButton;
405     delete d;
406 }
407 
408 /*!
409     Adds the macro expander provider \a provider.
410 */
addMacroExpanderProvider(const MacroExpanderProvider & provider)411 void VariableChooser::addMacroExpanderProvider(const MacroExpanderProvider &provider)
412 {
413     auto item = new VariableGroupItem;
414     item->m_chooser = d;
415     item->m_provider = provider;
416     d->m_model.rootItem()->prependChild(item);
417 }
418 
419 /*!
420  * Marks the control \a textcontrol as supporting variables.
421  *
422  * If the control provides a variable to the macro expander itself, set
423  * \a ownName to the variable name to prevent the user from choosing the
424  * variable, which would lead to endless recursion.
425  */
addSupportedWidget(QWidget * textcontrol,const QByteArray & ownName)426 void VariableChooser::addSupportedWidget(QWidget *textcontrol, const QByteArray &ownName)
427 {
428     QTC_ASSERT(textcontrol, return);
429     textcontrol->setProperty(kVariableSupportProperty, QVariant::fromValue<QWidget *>(this));
430     textcontrol->setProperty(kVariableNameProperty, ownName);
431 }
432 
addSupportForChildWidgets(QWidget * parent,MacroExpander * expander)433 void VariableChooser::addSupportForChildWidgets(QWidget *parent, MacroExpander *expander)
434 {
435      auto chooser = new VariableChooser(parent);
436      chooser->addMacroExpanderProvider([expander] { return expander; });
437      foreach (QWidget *child, parent->findChildren<QWidget *>()) {
438          if (qobject_cast<QLineEdit *>(child)
439                  || qobject_cast<QTextEdit *>(child)
440                  || qobject_cast<QPlainTextEdit *>(child))
441              chooser->addSupportedWidget(child);
442      }
443 }
444 
445 /*!
446  * \internal
447  */
updateDescription(const QModelIndex & index)448 void VariableChooserPrivate::updateDescription(const QModelIndex &index)
449 {
450     if (m_variableDescription)
451         m_variableDescription->setText(m_model.data(m_sortModel->mapToSource(index),
452                                                     Qt::ToolTipRole).toString());
453 }
454 
455 /*!
456  * \internal
457  */
buttonMargin() const458 int VariableChooserPrivate::buttonMargin() const
459 {
460     return 24;
461 }
462 
updateButtonGeometry()463 void VariableChooserPrivate::updateButtonGeometry()
464 {
465     QWidget *current = currentWidget();
466     int margin = buttonMargin();
467     int rightPadding = 0;
468     if (const auto scrollArea = qobject_cast<const QAbstractScrollArea*>(current)) {
469         rightPadding = scrollArea->verticalScrollBar()->isVisible() ?
470                     scrollArea->verticalScrollBar()->width() : 0;
471     }
472     m_iconButton->setGeometry(current->rect().adjusted(
473                                   current->width() - (margin + 4), 0,
474                                   0, -qMax(0, current->height() - (margin + 4)))
475                               .translated(-rightPadding, 0));
476 }
477 
updateCurrentEditor(QWidget * old,QWidget * widget)478 void VariableChooserPrivate::updateCurrentEditor(QWidget *old, QWidget *widget)
479 {
480     Q_UNUSED(old)
481     if (!widget) // we might loose focus, but then keep the previous state
482         return;
483     // prevent children of the chooser itself, and limit to children of chooser's parent
484     bool handle = false;
485     QWidget *parent = widget;
486     while (parent) {
487         if (parent == q)
488             return;
489         if (parent == q->parentWidget()) {
490             handle = true;
491             break;
492         }
493         parent = parent->parentWidget();
494     }
495     if (!handle)
496         return;
497 
498     QLineEdit *previousLineEdit = m_lineEdit;
499     QWidget *previousWidget = currentWidget();
500     m_lineEdit = nullptr;
501     m_textEdit = nullptr;
502     m_plainTextEdit = nullptr;
503     auto chooser = widget->property(kVariableSupportProperty).value<QWidget *>();
504     m_currentVariableName = widget->property(kVariableNameProperty).toByteArray();
505     bool supportsVariables = chooser == q;
506     if (auto lineEdit = qobject_cast<QLineEdit *>(widget))
507         m_lineEdit = (supportsVariables ? lineEdit : nullptr);
508     else if (auto textEdit = qobject_cast<QTextEdit *>(widget))
509         m_textEdit = (supportsVariables ? textEdit : nullptr);
510     else if (auto plainTextEdit = qobject_cast<QPlainTextEdit *>(widget))
511         m_plainTextEdit = (supportsVariables ? plainTextEdit : nullptr);
512 
513     QWidget *current = currentWidget();
514     if (current != previousWidget) {
515         if (previousWidget)
516             previousWidget->removeEventFilter(q);
517         if (previousLineEdit)
518             previousLineEdit->setTextMargins(0, 0, 0, 0);
519         if (m_iconButton) {
520             m_iconButton->hide();
521             m_iconButton->setParent(nullptr);
522         }
523         if (current) {
524             current->installEventFilter(q); // escape key handling and geometry changes
525             if (!m_iconButton)
526                 createIconButton();
527             int margin = buttonMargin();
528             if (m_lineEdit)
529                 m_lineEdit->setTextMargins(0, 0, margin, 0);
530             m_iconButton->setParent(current);
531             updateButtonGeometry();
532             m_iconButton->show();
533         } else {
534             q->hide();
535         }
536     }
537 }
538 
539 
540 /*!
541  * \internal
542  */
updatePositionAndShow(bool)543 void VariableChooserPrivate::updatePositionAndShow(bool)
544 {
545     if (QWidget *w = q->parentWidget()) {
546         QPoint parentCenter = w->mapToGlobal(w->geometry().center());
547         q->move(parentCenter.x() - q->width()/2, parentCenter.y() - q->height()/2);
548     }
549     q->show();
550     q->raise();
551     q->activateWindow();
552     m_variableTree->expandAll();
553 }
554 
updateFilter(const QString & filterText)555 void VariableChooserPrivate::updateFilter(const QString &filterText)
556 {
557     const QString pattern = QRegularExpression::escape(filterText);
558     m_sortModel->setFilterRegularExpression(
559                 QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption));
560     m_variableTree->expandAll();
561 }
562 
563 /*!
564  * \internal
565  */
currentWidget() const566 QWidget *VariableChooserPrivate::currentWidget() const
567 {
568     if (m_lineEdit)
569         return m_lineEdit;
570     if (m_textEdit)
571         return m_textEdit;
572     return m_plainTextEdit;
573 }
574 
575 /*!
576  * \internal
577  */
handleItemActivated(const QModelIndex & index)578 void VariableChooserPrivate::handleItemActivated(const QModelIndex &index)
579 {
580     QString text = m_model.data(m_sortModel->mapToSource(index), UnexpandedTextRole).toString();
581     if (!text.isEmpty())
582         insertText(text);
583 }
584 
585 /*!
586  * \internal
587  */
insertText(const QString & text)588 void VariableChooserPrivate::insertText(const QString &text)
589 {
590     if (m_lineEdit) {
591         m_lineEdit->insert(text);
592         m_lineEdit->activateWindow();
593     } else if (m_textEdit) {
594         m_textEdit->insertPlainText(text);
595         m_textEdit->activateWindow();
596     } else if (m_plainTextEdit) {
597         m_plainTextEdit->insertPlainText(text);
598         m_plainTextEdit->activateWindow();
599     }
600 }
601 
602 /*!
603  * \internal
604  */
handleEscapePressed(QKeyEvent * ke,QWidget * widget)605 static bool handleEscapePressed(QKeyEvent *ke, QWidget *widget)
606 {
607     if (ke->key() == Qt::Key_Escape && !ke->modifiers()) {
608         ke->accept();
609         QTimer::singleShot(0, widget, &QWidget::close);
610         return true;
611     }
612     return false;
613 }
614 
615 /*!
616  * \internal
617  */
event(QEvent * ev)618 bool VariableChooser::event(QEvent *ev)
619 {
620     if (ev->type() == QEvent::KeyPress || ev->type() == QEvent::ShortcutOverride) {
621         auto ke = static_cast<QKeyEvent *>(ev);
622         if (handleEscapePressed(ke, this))
623             return true;
624     }
625     return QWidget::event(ev);
626 }
627 
628 /*!
629  * \internal
630  */
eventFilter(QObject * obj,QEvent * event)631 bool VariableChooser::eventFilter(QObject *obj, QEvent *event)
632 {
633     if (obj != d->currentWidget())
634         return false;
635     if ((event->type() == QEvent::KeyPress || event->type() == QEvent::ShortcutOverride) && isVisible()) {
636         auto ke = static_cast<QKeyEvent *>(event);
637         return handleEscapePressed(ke, this);
638     } else if (event->type() == QEvent::Resize || event->type() == QEvent::LayoutRequest) {
639         d->updateButtonGeometry();
640     } else if (event->type() == QEvent::Hide) {
641         close();
642     }
643     return false;
644 }
645 
646 } // namespace Internal
647