File indexing completed on 2024-04-28 15:30:56

0001 /*
0002     SPDX-FileCopyrightText: 2019 Dominik Haumann <dhaumann@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "katevariableexpansionhelpers.h"
0008 
0009 #include "variable.h"
0010 
0011 #include <KLocalizedString>
0012 #include <KTextEditor/Application>
0013 #include <KTextEditor/Editor>
0014 #include <KTextEditor/MainWindow>
0015 
0016 #include <QAbstractItemModel>
0017 #include <QAction>
0018 #include <QCoreApplication>
0019 #include <QEvent>
0020 #include <QHelpEvent>
0021 #include <QLabel>
0022 #include <QLineEdit>
0023 #include <QListView>
0024 #include <QSortFilterProxyModel>
0025 #include <QStyleOptionToolButton>
0026 #include <QStylePainter>
0027 #include <QTextEdit>
0028 #include <QToolButton>
0029 #include <QToolTip>
0030 #include <QVBoxLayout>
0031 
0032 /**
0033  * Find closing bracket for @p str starting a position @p pos.
0034  */
0035 static int findClosing(QStringView str, int pos = 0)
0036 {
0037     const int len = str.size();
0038     int nesting = 0;
0039 
0040     while (pos < len) {
0041         const QChar c = str[pos];
0042         if (c == QLatin1Char('}')) {
0043             if (nesting == 0) {
0044                 return pos;
0045             }
0046             nesting--;
0047         } else if (c == QLatin1Char('{')) {
0048             nesting++;
0049         }
0050         ++pos;
0051     }
0052     return -1;
0053 }
0054 
0055 namespace KateMacroExpander
0056 {
0057 QString expandMacro(const QString &input, KTextEditor::View *view)
0058 {
0059     QString output = input;
0060     QString oldStr;
0061     do {
0062         oldStr = output;
0063         const int startIndex = output.indexOf(QLatin1String("%{"));
0064         if (startIndex < 0) {
0065             break;
0066         }
0067 
0068         const int endIndex = findClosing(output, startIndex + 2);
0069         if (endIndex <= startIndex) {
0070             break;
0071         }
0072 
0073         const int varLen = endIndex - (startIndex + 2);
0074         QString variable = output.mid(startIndex + 2, varLen);
0075         variable = expandMacro(variable, view);
0076         if (KTextEditor::Editor::instance()->expandVariable(variable, view, variable)) {
0077             output.replace(startIndex, endIndex - startIndex + 1, variable);
0078         }
0079     } while (output != oldStr); // str comparison guards against infinite loop
0080     return output;
0081 }
0082 
0083 }
0084 
0085 class VariableItemModel : public QAbstractItemModel
0086 {
0087 public:
0088     VariableItemModel(QObject *parent = nullptr)
0089         : QAbstractItemModel(parent)
0090     {
0091     }
0092 
0093     QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override
0094     {
0095         if (parent.isValid() || row < 0 || row >= m_variables.size()) {
0096             return {};
0097         }
0098 
0099         return createIndex(row, column);
0100     }
0101 
0102     QModelIndex parent(const QModelIndex &index) const override
0103     {
0104         Q_UNUSED(index)
0105         // flat list -> we never have parents
0106         return {};
0107     }
0108 
0109     int rowCount(const QModelIndex &parent = QModelIndex()) const override
0110     {
0111         return parent.isValid() ? 0 : m_variables.size();
0112     }
0113 
0114     int columnCount(const QModelIndex &parent = QModelIndex()) const override
0115     {
0116         Q_UNUSED(parent)
0117         return 3; // name | description | current value
0118     }
0119 
0120     QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
0121     {
0122         if (!index.isValid()) {
0123             return {};
0124         }
0125 
0126         const auto &var = m_variables[index.row()];
0127         switch (role) {
0128         case Qt::DisplayRole: {
0129             const QString suffix = var.isPrefixMatch() ? i18n("<value>") : QString();
0130             return QString(var.name() + suffix);
0131         }
0132         case Qt::ToolTipRole:
0133             return var.description();
0134         }
0135 
0136         return {};
0137     }
0138 
0139     void setVariables(const QVector<KTextEditor::Variable> &variables)
0140     {
0141         beginResetModel();
0142         m_variables = variables;
0143         endResetModel();
0144     }
0145 
0146 private:
0147     QVector<KTextEditor::Variable> m_variables;
0148 };
0149 
0150 class TextEditButton : public QToolButton
0151 {
0152 public:
0153     TextEditButton(QAction *showAction, QTextEdit *parent)
0154         : QToolButton(parent)
0155     {
0156         setAutoRaise(true);
0157         setDefaultAction(showAction);
0158         m_watched = parent->viewport();
0159         m_watched->installEventFilter(this);
0160         show();
0161         adjustPosition(m_watched->size());
0162     }
0163 
0164 protected:
0165     void paintEvent(QPaintEvent *) override
0166     {
0167         // reimplement to have same behavior as actions in QLineEdits
0168         QStylePainter p(this);
0169         QStyleOptionToolButton opt;
0170         initStyleOption(&opt);
0171         opt.state = opt.state & ~QStyle::State_Raised;
0172         opt.state = opt.state & ~QStyle::State_MouseOver;
0173         opt.state = opt.state & ~QStyle::State_Sunken;
0174         p.drawComplexControl(QStyle::CC_ToolButton, opt);
0175     }
0176 
0177 public:
0178     bool eventFilter(QObject *watched, QEvent *event) override
0179     {
0180         if (watched == m_watched) {
0181             switch (event->type()) {
0182             case QEvent::Resize: {
0183                 auto resizeEvent = static_cast<QResizeEvent *>(event);
0184                 adjustPosition(resizeEvent->size());
0185             }
0186             default:
0187                 break;
0188             }
0189         }
0190         return QToolButton::eventFilter(watched, event);
0191     }
0192 
0193 private:
0194     void adjustPosition(const QSize &parentSize)
0195     {
0196         QStyleOption sopt;
0197         sopt.initFrom(parentWidget());
0198         const int topMargin = 0; // style()->pixelMetric(QStyle::PM_LayoutTopMargin, &sopt, parentWidget());
0199         const int rightMargin = 0; // style()->pixelMetric(QStyle::PM_LayoutRightMargin, &sopt, parentWidget());
0200         if (isLeftToRight()) {
0201             move(parentSize.width() - width() - rightMargin, topMargin);
0202         } else {
0203             move(0, 0);
0204         }
0205     }
0206 
0207 private:
0208     QWidget *m_watched;
0209 };
0210 
0211 KateVariableExpansionDialog::KateVariableExpansionDialog(QWidget *parent)
0212     : QDialog(parent, Qt::Tool)
0213     , m_showAction(new QAction(QIcon::fromTheme(QStringLiteral("code-context")), i18n("Insert variable"), this))
0214     , m_variableModel(new VariableItemModel(this))
0215     , m_listView(new QListView(this))
0216 {
0217     setWindowTitle(i18n("Variables"));
0218 
0219     auto vbox = new QVBoxLayout(this);
0220     m_filterEdit = new QLineEdit(this);
0221     m_filterEdit->setPlaceholderText(i18n("Filter"));
0222     m_filterEdit->setFocus();
0223     m_filterEdit->installEventFilter(this);
0224     vbox->addWidget(m_filterEdit);
0225     vbox->addWidget(m_listView);
0226     m_listView->setUniformItemSizes(true);
0227 
0228     m_filterModel = new QSortFilterProxyModel(this);
0229     m_filterModel->setFilterRole(Qt::DisplayRole);
0230     m_filterModel->setSortRole(Qt::DisplayRole);
0231     m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
0232     m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive);
0233     m_filterModel->setFilterKeyColumn(0);
0234 
0235     m_filterModel->setSourceModel(m_variableModel);
0236     m_listView->setModel(m_filterModel);
0237 
0238     connect(m_filterEdit, &QLineEdit::textChanged, m_filterModel, &QSortFilterProxyModel::setFilterWildcard);
0239 
0240     auto lblDescription = new QLabel(i18n("Please select a variable."), this);
0241     auto lblCurrentValue = new QLabel(this);
0242 
0243     vbox->addWidget(lblDescription);
0244     vbox->addWidget(lblCurrentValue);
0245 
0246     // react to selection changes
0247     connect(m_listView->selectionModel(),
0248             &QItemSelectionModel::currentRowChanged,
0249             [this, lblDescription, lblCurrentValue](const QModelIndex &current, const QModelIndex &) {
0250                 if (current.isValid()) {
0251                     const auto &var = m_variables[m_filterModel->mapToSource(current).row()];
0252                     lblDescription->setText(var.description());
0253                     if (var.isPrefixMatch()) {
0254                         lblCurrentValue->setText(i18n("Current value: %1<value>", var.name()));
0255                     } else {
0256                         auto activeView = KTextEditor::Editor::instance()->application()->activeMainWindow()->activeView();
0257                         const auto value = var.evaluate(var.name(), activeView);
0258                         lblCurrentValue->setText(i18n("Current value: %1", value));
0259                     }
0260                 } else {
0261                     lblDescription->setText(i18n("Please select a variable."));
0262                     lblCurrentValue->clear();
0263                 }
0264             });
0265 
0266     // insert text on activation
0267     connect(m_listView, &QAbstractItemView::activated, [this](const QModelIndex &index) {
0268         if (index.isValid()) {
0269             const auto &var = m_variables[m_filterModel->mapToSource(index).row()];
0270 
0271             // not auto, don't fall for string builder, see bug 413474
0272             const QString name = QStringLiteral("%{") + var.name() + QLatin1Char('}');
0273             if (parentWidget() && parentWidget()->window()) {
0274                 auto currentWidget = parentWidget()->window()->focusWidget();
0275                 if (auto lineEdit = qobject_cast<QLineEdit *>(currentWidget)) {
0276                     lineEdit->insert(name);
0277                 } else if (auto textEdit = qobject_cast<QTextEdit *>(currentWidget)) {
0278                     textEdit->insertPlainText(name);
0279                 }
0280             }
0281         }
0282     });
0283 
0284     // show dialog whenever the action is clicked
0285     connect(m_showAction, &QAction::triggered, [this]() {
0286         show();
0287         activateWindow();
0288     });
0289 
0290     resize(400, 550);
0291 }
0292 
0293 KateVariableExpansionDialog::~KateVariableExpansionDialog()
0294 {
0295     for (auto it = m_textEditButtons.begin(); it != m_textEditButtons.end(); ++it) {
0296         if (it.value()) {
0297             delete it.value();
0298         }
0299     }
0300     m_textEditButtons.clear();
0301 }
0302 
0303 void KateVariableExpansionDialog::addVariable(const KTextEditor::Variable &variable)
0304 {
0305     Q_ASSERT(variable.isValid());
0306     m_variables.push_back(variable);
0307 
0308     m_variableModel->setVariables(m_variables);
0309 }
0310 
0311 int KateVariableExpansionDialog::isEmpty() const
0312 {
0313     return m_variables.isEmpty();
0314 }
0315 
0316 void KateVariableExpansionDialog::addWidget(QWidget *widget)
0317 {
0318     m_widgets.push_back(widget);
0319     widget->installEventFilter(this);
0320 
0321     connect(widget, &QObject::destroyed, this, &KateVariableExpansionDialog::onObjectDeleted);
0322 }
0323 
0324 void KateVariableExpansionDialog::onObjectDeleted(QObject *object)
0325 {
0326     m_widgets.removeAll(object);
0327     if (m_widgets.isEmpty()) {
0328         deleteLater();
0329     }
0330 }
0331 
0332 bool KateVariableExpansionDialog::eventFilter(QObject *watched, QEvent *event)
0333 {
0334     // filter line edit
0335     if (watched == m_filterEdit) {
0336         if (event->type() == QEvent::KeyPress) {
0337             QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
0338             const bool forward2list = (keyEvent->key() == Qt::Key_Up) || (keyEvent->key() == Qt::Key_Down) || (keyEvent->key() == Qt::Key_PageUp)
0339                 || (keyEvent->key() == Qt::Key_PageDown) || (keyEvent->key() == Qt::Key_Enter) || (keyEvent->key() == Qt::Key_Return);
0340             if (forward2list) {
0341                 QCoreApplication::sendEvent(m_listView, event);
0342                 return true;
0343             }
0344         }
0345         return QDialog::eventFilter(watched, event);
0346     }
0347 
0348     // tracked widgets (tooltips, adding/removing the showAction)
0349     switch (event->type()) {
0350     case QEvent::FocusIn: {
0351         if (auto lineEdit = qobject_cast<QLineEdit *>(watched)) {
0352             lineEdit->addAction(m_showAction, QLineEdit::TrailingPosition);
0353         } else if (auto textEdit = qobject_cast<QTextEdit *>(watched)) {
0354             if (!m_textEditButtons.contains(textEdit)) {
0355                 m_textEditButtons[textEdit] = new TextEditButton(m_showAction, textEdit);
0356             }
0357             m_textEditButtons[textEdit]->raise();
0358             m_textEditButtons[textEdit]->show();
0359         }
0360         break;
0361     }
0362     case QEvent::FocusOut: {
0363         if (auto lineEdit = qobject_cast<QLineEdit *>(watched)) {
0364             lineEdit->removeAction(m_showAction);
0365         } else if (auto textEdit = qobject_cast<QTextEdit *>(watched)) {
0366             if (m_textEditButtons.contains(textEdit)) {
0367                 delete m_textEditButtons[textEdit];
0368                 m_textEditButtons.remove(textEdit);
0369             }
0370         }
0371         break;
0372     }
0373     case QEvent::ToolTip: {
0374         QString inputText;
0375         if (auto lineEdit = qobject_cast<QLineEdit *>(watched)) {
0376             inputText = lineEdit->text();
0377         }
0378         QString toolTip;
0379         if (!inputText.isEmpty()) {
0380             auto activeView = KTextEditor::Editor::instance()->application()->activeMainWindow()->activeView();
0381             KTextEditor::Editor::instance()->expandText(inputText, activeView, toolTip);
0382         }
0383 
0384         if (!toolTip.isEmpty()) {
0385             auto helpEvent = static_cast<QHelpEvent *>(event);
0386             QToolTip::showText(helpEvent->globalPos(), toolTip, qobject_cast<QWidget *>(watched));
0387             event->accept();
0388             return true;
0389         }
0390         break;
0391     }
0392     default:
0393         break;
0394     }
0395 
0396     // auto-hide on focus change
0397     auto parentWindow = parentWidget()->window();
0398     const bool keepVisible = isActiveWindow() || m_widgets.contains(parentWindow->focusWidget());
0399     if (!keepVisible) {
0400         hide();
0401     }
0402 
0403     return QDialog::eventFilter(watched, event);
0404 }
0405 
0406 // kate: space-indent on; indent-width 4; replace-tabs on;