/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of Qt Creator. ** ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ****************************************************************************/ #include "variablechooser.h" #include "fancylineedit.h" #include "headerviewstretcher.h" // IconButton #include "macroexpander.h" #include "treemodel.h" #include "qtcassert.h" #include "utilsicons.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Utils { namespace Internal { enum { UnexpandedTextRole = Qt::UserRole, ExpandedTextRole }; class VariableTreeView : public QTreeView { public: VariableTreeView(QWidget *parent, VariableChooserPrivate *target) : QTreeView(parent), m_target(target) { setAttribute(Qt::WA_MacSmallSize); setAttribute(Qt::WA_MacShowFocusRect, false); setIndentation(indentation() * 7/10); header()->hide(); new HeaderViewStretcher(header(), 0); } void contextMenuEvent(QContextMenuEvent *ev) override; void currentChanged(const QModelIndex ¤t, const QModelIndex &previous) override; private: VariableChooserPrivate *m_target; }; class VariableSortFilterProxyModel : public QSortFilterProxyModel { public: explicit VariableSortFilterProxyModel(QObject *parent) : QSortFilterProxyModel(parent) {} bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override { const QModelIndex index = sourceModel()->index(sourceRow, filterKeyColumn(), sourceParent); if (!index.isValid()) return false; const QRegularExpression regexp = filterRegularExpression(); if (regexp.pattern().isEmpty() || sourceModel()->rowCount(index) > 0) return true; const QString displayText = index.data(Qt::DisplayRole).toString(); return displayText.contains(regexp); } }; class VariableChooserPrivate : public QObject { public: VariableChooserPrivate(VariableChooser *parent); void createIconButton() { m_iconButton = new IconButton; m_iconButton->setIcon(Utils::Icons::REPLACE.icon()); m_iconButton->setToolTip(VariableChooser::tr("Insert Variable")); m_iconButton->hide(); connect(m_iconButton.data(), static_cast(&QAbstractButton::clicked), this, &VariableChooserPrivate::updatePositionAndShow); } void updateDescription(const QModelIndex &index); void updateCurrentEditor(QWidget *old, QWidget *widget); void handleItemActivated(const QModelIndex &index); void insertText(const QString &variable); void updatePositionAndShow(bool); void updateFilter(const QString &filterText); QWidget *currentWidget() const; int buttonMargin() const; void updateButtonGeometry(); public: VariableChooser *q; TreeModel<> m_model; QPointer m_lineEdit; QPointer m_textEdit; QPointer m_plainTextEdit; QPointer m_iconButton; Utils::FancyLineEdit *m_variableFilter; VariableTreeView *m_variableTree; QLabel *m_variableDescription; QSortFilterProxyModel *m_sortModel; QString m_defaultDescription; QByteArray m_currentVariableName; // Prevent recursive insertion of currently expanded item }; class VariableGroupItem : public TreeItem { public: VariableGroupItem() = default; QVariant data(int column, int role) const override { if (role == Qt::DisplayRole || role == Qt::EditRole) { if (column == 0) if (MacroExpander *expander = m_provider()) return expander->displayName(); } return QVariant(); } bool canFetchMore() const override { return !m_populated; } void fetchMore() override { if (MacroExpander *expander = m_provider()) populateGroup(expander); m_populated = true; } void populateGroup(MacroExpander *expander); public: VariableChooserPrivate *m_chooser = nullptr; // Not owned. bool m_populated = false; MacroExpanderProvider m_provider; }; class VariableItem : public TypedTreeItem { public: VariableItem() = default; Qt::ItemFlags flags(int) const override { if (m_variable == parent()->m_chooser->m_currentVariableName) return Qt::ItemIsSelectable; return Qt::ItemIsSelectable|Qt::ItemIsEnabled; } QVariant data(int column, int role) const override { if (role == Qt::DisplayRole || role == Qt::EditRole) { if (column == 0) return m_variable; } if (role == Qt::ToolTipRole) { QString description = m_expander->variableDescription(m_variable); const QString value = m_expander->value(m_variable).toHtmlEscaped(); if (!value.isEmpty()) description += QLatin1String("

") + VariableChooser::tr("Current Value: %1").arg(value); return description; } if (role == UnexpandedTextRole) return QString::fromUtf8("%{" + m_variable + '}'); if (role == ExpandedTextRole) return m_expander->expand(QString::fromUtf8("%{" + m_variable + '}')); return QVariant(); } public: MacroExpander *m_expander; QByteArray m_variable; }; void VariableTreeView::contextMenuEvent(QContextMenuEvent *ev) { const QModelIndex index = indexAt(ev->pos()); QString unexpandedText = index.data(UnexpandedTextRole).toString(); QString expandedText = index.data(ExpandedTextRole).toString(); QMenu menu; QAction *insertUnexpandedAction = nullptr; QAction *insertExpandedAction = nullptr; if (unexpandedText.isEmpty()) { insertUnexpandedAction = menu.addAction(VariableChooser::tr("Insert Unexpanded Value")); insertUnexpandedAction->setEnabled(false); } else { insertUnexpandedAction = menu.addAction(VariableChooser::tr("Insert \"%1\"").arg(unexpandedText)); } if (expandedText.isEmpty()) { insertExpandedAction = menu.addAction(VariableChooser::tr("Insert Expanded Value")); insertExpandedAction->setEnabled(false); } else { insertExpandedAction = menu.addAction(VariableChooser::tr("Insert \"%1\"").arg(expandedText)); } QAction *act = menu.exec(ev->globalPos()); if (act == insertUnexpandedAction) m_target->insertText(unexpandedText); else if (act == insertExpandedAction) m_target->insertText(expandedText); } void VariableTreeView::currentChanged(const QModelIndex ¤t, const QModelIndex &previous) { m_target->updateDescription(current); QTreeView::currentChanged(current, previous); } VariableChooserPrivate::VariableChooserPrivate(VariableChooser *parent) : q(parent), m_lineEdit(nullptr), m_textEdit(nullptr), m_plainTextEdit(nullptr), m_iconButton(nullptr), m_variableFilter(nullptr), m_variableTree(nullptr), m_variableDescription(nullptr) { m_defaultDescription = VariableChooser::tr("Select a variable to insert."); m_variableFilter = new Utils::FancyLineEdit(q); m_variableTree = new VariableTreeView(q, this); m_variableDescription = new QLabel(q); m_variableFilter->setFiltering(true); m_sortModel = new VariableSortFilterProxyModel(this); m_sortModel->setSourceModel(&m_model); m_sortModel->sort(0); m_sortModel->setFilterKeyColumn(0); m_sortModel->setFilterCaseSensitivity(Qt::CaseInsensitive); m_variableTree->setModel(m_sortModel); m_variableDescription->setText(m_defaultDescription); m_variableDescription->setMinimumSize(QSize(0, 60)); m_variableDescription->setAlignment(Qt::AlignLeft|Qt::AlignTop); m_variableDescription->setWordWrap(true); m_variableDescription->setAttribute(Qt::WA_MacSmallSize); m_variableDescription->setTextInteractionFlags(Qt::TextBrowserInteraction); auto verticalLayout = new QVBoxLayout(q); verticalLayout->setContentsMargins(3, 3, 3, 12); verticalLayout->addWidget(m_variableFilter); verticalLayout->addWidget(m_variableTree); verticalLayout->addWidget(m_variableDescription); connect(m_variableFilter, &QLineEdit::textChanged, this, &VariableChooserPrivate::updateFilter); connect(m_variableTree, &QTreeView::activated, this, &VariableChooserPrivate::handleItemActivated); connect(qobject_cast(qApp), &QApplication::focusChanged, this, &VariableChooserPrivate::updateCurrentEditor); updateCurrentEditor(nullptr, QApplication::focusWidget()); } void VariableGroupItem::populateGroup(MacroExpander *expander) { if (!expander) return; foreach (const QByteArray &variable, expander->visibleVariables()) { auto item = new VariableItem; item->m_variable = variable; item->m_expander = expander; appendChild(item); } foreach (const MacroExpanderProvider &subProvider, expander->subProviders()) { if (!subProvider) continue; if (expander->isAccumulating()) { populateGroup(subProvider()); } else { auto item = new VariableGroupItem; item->m_chooser = m_chooser; item->m_provider = subProvider; appendChild(item); } } } } // namespace Internal using namespace Internal; /*! \class Utils::VariableChooser \inheaderfile coreplugin/variablechooser.h \inmodule QtCreator \brief The VariableChooser class is used to add a tool window for selecting \QC variables to line edits, text edits or plain text edits. If you allow users to add \QC variables to strings that are specified in your UI, for example when users can provide a string through a text control, you should add a variable chooser to it. The variable chooser allows users to open a tool window that contains the list of all available variables together with a description. Double-clicking a variable inserts the corresponding string into the corresponding text control like a line edit. \image variablechooser.png "External Tools Preferences with Variable Chooser" The variable chooser monitors focus changes of all children of its parent widget. When a text control gets focus, the variable chooser checks if it has variable support set. If the control supports variables, a tool button which opens the variable chooser is shown in it while it has focus. Supported text controls are QLineEdit, QTextEdit and QPlainTextEdit. The variable chooser is deleted when its parent widget is deleted. Example: \code QWidget *myOptionsContainerWidget = new QWidget; new Utils::VariableChooser(myOptionsContainerWidget) QLineEdit *myLineEditOption = new QLineEdit(myOptionsContainerWidget); myOptionsContainerWidget->layout()->addWidget(myLineEditOption); Utils::VariableChooser::addVariableSupport(myLineEditOption); \endcode */ /*! * \internal * \variable VariableChooser::kVariableSupportProperty * Property name that is checked for deciding if a widget supports \QC variables. * Can be manually set with * \c{textcontrol->setProperty(VariableChooser::kVariableSupportProperty, true)} */ const char kVariableSupportProperty[] = "QtCreator.VariableSupport"; const char kVariableNameProperty[] = "QtCreator.VariableName"; /*! * Creates a variable chooser that tracks all children of \a parent for variable support. * Ownership is also transferred to \a parent. */ VariableChooser::VariableChooser(QWidget *parent) : QWidget(parent), d(new VariableChooserPrivate(this)) { setWindowTitle(tr("Variables")); setWindowFlags(Qt::Tool); setFocusPolicy(Qt::StrongFocus); setFocusProxy(d->m_variableTree); setGeometry(QRect(0, 0, 400, 500)); addMacroExpanderProvider([]() { return globalMacroExpander(); }); } /*! * \internal */ VariableChooser::~VariableChooser() { delete d->m_iconButton; delete d; } /*! Adds the macro expander provider \a provider. */ void VariableChooser::addMacroExpanderProvider(const MacroExpanderProvider &provider) { auto item = new VariableGroupItem; item->m_chooser = d; item->m_provider = provider; d->m_model.rootItem()->prependChild(item); } /*! * Marks the control \a textcontrol as supporting variables. * * If the control provides a variable to the macro expander itself, set * \a ownName to the variable name to prevent the user from choosing the * variable, which would lead to endless recursion. */ void VariableChooser::addSupportedWidget(QWidget *textcontrol, const QByteArray &ownName) { QTC_ASSERT(textcontrol, return); textcontrol->setProperty(kVariableSupportProperty, QVariant::fromValue(this)); textcontrol->setProperty(kVariableNameProperty, ownName); } void VariableChooser::addSupportForChildWidgets(QWidget *parent, MacroExpander *expander) { auto chooser = new VariableChooser(parent); chooser->addMacroExpanderProvider([expander] { return expander; }); foreach (QWidget *child, parent->findChildren()) { if (qobject_cast(child) || qobject_cast(child) || qobject_cast(child)) chooser->addSupportedWidget(child); } } /*! * \internal */ void VariableChooserPrivate::updateDescription(const QModelIndex &index) { if (m_variableDescription) m_variableDescription->setText(m_model.data(m_sortModel->mapToSource(index), Qt::ToolTipRole).toString()); } /*! * \internal */ int VariableChooserPrivate::buttonMargin() const { return 24; } void VariableChooserPrivate::updateButtonGeometry() { QWidget *current = currentWidget(); int margin = buttonMargin(); int rightPadding = 0; if (const auto scrollArea = qobject_cast(current)) { rightPadding = scrollArea->verticalScrollBar()->isVisible() ? scrollArea->verticalScrollBar()->width() : 0; } m_iconButton->setGeometry(current->rect().adjusted( current->width() - (margin + 4), 0, 0, -qMax(0, current->height() - (margin + 4))) .translated(-rightPadding, 0)); } void VariableChooserPrivate::updateCurrentEditor(QWidget *old, QWidget *widget) { Q_UNUSED(old) if (!widget) // we might loose focus, but then keep the previous state return; // prevent children of the chooser itself, and limit to children of chooser's parent bool handle = false; QWidget *parent = widget; while (parent) { if (parent == q) return; if (parent == q->parentWidget()) { handle = true; break; } parent = parent->parentWidget(); } if (!handle) return; QLineEdit *previousLineEdit = m_lineEdit; QWidget *previousWidget = currentWidget(); m_lineEdit = nullptr; m_textEdit = nullptr; m_plainTextEdit = nullptr; auto chooser = widget->property(kVariableSupportProperty).value(); m_currentVariableName = widget->property(kVariableNameProperty).toByteArray(); bool supportsVariables = chooser == q; if (auto lineEdit = qobject_cast(widget)) m_lineEdit = (supportsVariables ? lineEdit : nullptr); else if (auto textEdit = qobject_cast(widget)) m_textEdit = (supportsVariables ? textEdit : nullptr); else if (auto plainTextEdit = qobject_cast(widget)) m_plainTextEdit = (supportsVariables ? plainTextEdit : nullptr); QWidget *current = currentWidget(); if (current != previousWidget) { if (previousWidget) previousWidget->removeEventFilter(q); if (previousLineEdit) previousLineEdit->setTextMargins(0, 0, 0, 0); if (m_iconButton) { m_iconButton->hide(); m_iconButton->setParent(nullptr); } if (current) { current->installEventFilter(q); // escape key handling and geometry changes if (!m_iconButton) createIconButton(); int margin = buttonMargin(); if (m_lineEdit) m_lineEdit->setTextMargins(0, 0, margin, 0); m_iconButton->setParent(current); updateButtonGeometry(); m_iconButton->show(); } else { q->hide(); } } } /*! * \internal */ void VariableChooserPrivate::updatePositionAndShow(bool) { if (QWidget *w = q->parentWidget()) { QPoint parentCenter = w->mapToGlobal(w->geometry().center()); q->move(parentCenter.x() - q->width()/2, parentCenter.y() - q->height()/2); } q->show(); q->raise(); q->activateWindow(); m_variableTree->expandAll(); } void VariableChooserPrivate::updateFilter(const QString &filterText) { const QString pattern = QRegularExpression::escape(filterText); m_sortModel->setFilterRegularExpression( QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); m_variableTree->expandAll(); } /*! * \internal */ QWidget *VariableChooserPrivate::currentWidget() const { if (m_lineEdit) return m_lineEdit; if (m_textEdit) return m_textEdit; return m_plainTextEdit; } /*! * \internal */ void VariableChooserPrivate::handleItemActivated(const QModelIndex &index) { QString text = m_model.data(m_sortModel->mapToSource(index), UnexpandedTextRole).toString(); if (!text.isEmpty()) insertText(text); } /*! * \internal */ void VariableChooserPrivate::insertText(const QString &text) { if (m_lineEdit) { m_lineEdit->insert(text); m_lineEdit->activateWindow(); } else if (m_textEdit) { m_textEdit->insertPlainText(text); m_textEdit->activateWindow(); } else if (m_plainTextEdit) { m_plainTextEdit->insertPlainText(text); m_plainTextEdit->activateWindow(); } } /*! * \internal */ static bool handleEscapePressed(QKeyEvent *ke, QWidget *widget) { if (ke->key() == Qt::Key_Escape && !ke->modifiers()) { ke->accept(); QTimer::singleShot(0, widget, &QWidget::close); return true; } return false; } /*! * \internal */ bool VariableChooser::event(QEvent *ev) { if (ev->type() == QEvent::KeyPress || ev->type() == QEvent::ShortcutOverride) { auto ke = static_cast(ev); if (handleEscapePressed(ke, this)) return true; } return QWidget::event(ev); } /*! * \internal */ bool VariableChooser::eventFilter(QObject *obj, QEvent *event) { if (obj != d->currentWidget()) return false; if ((event->type() == QEvent::KeyPress || event->type() == QEvent::ShortcutOverride) && isVisible()) { auto ke = static_cast(event); return handleEscapePressed(ke, this); } else if (event->type() == QEvent::Resize || event->type() == QEvent::LayoutRequest) { d->updateButtonGeometry(); } else if (event->type() == QEvent::Hide) { close(); } return false; } } // namespace Internal