File indexing completed on 2024-03-24 04:00:38
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 QList<KTextEditor::Variable> &variables) 0140 { 0141 beginResetModel(); 0142 m_variables = variables; 0143 endResetModel(); 0144 } 0145 0146 private: 0147 QList<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 ¤t, 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 toolTip = KTextEditor::Editor::instance()->expandText(inputText, activeView); 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;