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 ¤t, 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 ¤t, 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