1/*
2 SPDX-FileCopyrightText: 2019 Dominik Haumann <dhaumann@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "katevariableexpansionhelpers.h"
8
9#include "variable.h"
10
11#include <KLocalizedString>
12#include <KTextEditor/Application>
13#include <KTextEditor/Editor>
14#include <KTextEditor/MainWindow>
15
16#include <QAbstractItemModel>
17#include <QAction>
18#include <QCoreApplication>
19#include <QEvent>
20#include <QHelpEvent>
21#include <QLabel>
22#include <QLineEdit>
23#include <QListView>
24#include <QSortFilterProxyModel>
25#include <QStyleOptionToolButton>
26#include <QStylePainter>
27#include <QTextEdit>
28#include <QToolButton>
29#include <QToolTip>
30#include <QVBoxLayout>
31
32/**
33 * Find closing bracket for @p str starting a position @p pos.
34 */
35static int findClosing(QStringView str, int pos = 0)
36{
37 const int len = str.size();
38 int nesting = 0;
39
40 while (pos < len) {
41 const QChar c = str[pos];
42 if (c == QLatin1Char('}')) {
43 if (nesting == 0) {
44 return pos;
45 }
46 nesting--;
47 } else if (c == QLatin1Char('{')) {
48 nesting++;
49 }
50 ++pos;
51 }
52 return -1;
53}
54
55namespace KateMacroExpander
56{
57QString expandMacro(const QString &input, KTextEditor::View *view)
58{
59 QString output = input;
60 QString oldStr;
61 do {
62 oldStr = output;
63 const int startIndex = output.indexOf(s: QLatin1String("%{"));
64 if (startIndex < 0) {
65 break;
66 }
67
68 const int endIndex = findClosing(str: output, pos: startIndex + 2);
69 if (endIndex <= startIndex) {
70 break;
71 }
72
73 const int varLen = endIndex - (startIndex + 2);
74 QString variable = output.mid(position: startIndex + 2, n: varLen);
75 variable = expandMacro(input: variable, view);
76 if (KTextEditor::Editor::instance()->expandVariable(variable, view, output&: variable)) {
77 output.replace(i: startIndex, len: endIndex - startIndex + 1, after: variable);
78 }
79 } while (output != oldStr); // str comparison guards against infinite loop
80 return output;
81}
82
83}
84
85class VariableItemModel : public QAbstractItemModel
86{
87public:
88 VariableItemModel(QObject *parent = nullptr)
89 : QAbstractItemModel(parent)
90 {
91 }
92
93 QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override
94 {
95 if (parent.isValid() || row < 0 || row >= m_variables.size()) {
96 return {};
97 }
98
99 return createIndex(arow: row, acolumn: column);
100 }
101
102 QModelIndex parent(const QModelIndex &index) const override
103 {
104 Q_UNUSED(index)
105 // flat list -> we never have parents
106 return {};
107 }
108
109 int rowCount(const QModelIndex &parent = QModelIndex()) const override
110 {
111 return parent.isValid() ? 0 : m_variables.size();
112 }
113
114 int columnCount(const QModelIndex &parent = QModelIndex()) const override
115 {
116 Q_UNUSED(parent)
117 return 3; // name | description | current value
118 }
119
120 QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
121 {
122 if (!index.isValid()) {
123 return {};
124 }
125
126 const auto &var = m_variables[index.row()];
127 switch (role) {
128 case Qt::DisplayRole: {
129 const QString suffix = var.isPrefixMatch() ? i18n("<value>") : QString();
130 return QString(var.name() + suffix);
131 }
132 case Qt::ToolTipRole:
133 return var.description();
134 }
135
136 return {};
137 }
138
139 void setVariables(const QList<KTextEditor::Variable> &variables)
140 {
141 beginResetModel();
142 m_variables = variables;
143 endResetModel();
144 }
145
146private:
147 QList<KTextEditor::Variable> m_variables;
148};
149
150class TextEditButton : public QToolButton
151{
152public:
153 TextEditButton(QAction *showAction, QTextEdit *parent)
154 : QToolButton(parent)
155 {
156 setAutoRaise(true);
157 setDefaultAction(showAction);
158 m_watched = parent->viewport();
159 m_watched->installEventFilter(filterObj: this);
160 show();
161 adjustPosition(parentSize: m_watched->size());
162 }
163
164protected:
165 void paintEvent(QPaintEvent *) override
166 {
167 // reimplement to have same behavior as actions in QLineEdits
168 QStylePainter p(this);
169 QStyleOptionToolButton opt;
170 initStyleOption(option: &opt);
171 opt.state = opt.state & ~QStyle::State_Raised;
172 opt.state = opt.state & ~QStyle::State_MouseOver;
173 opt.state = opt.state & ~QStyle::State_Sunken;
174 p.drawComplexControl(cc: QStyle::CC_ToolButton, opt);
175 }
176
177public:
178 bool eventFilter(QObject *watched, QEvent *event) override
179 {
180 if (watched == m_watched) {
181 switch (event->type()) {
182 case QEvent::Resize: {
183 auto resizeEvent = static_cast<QResizeEvent *>(event);
184 adjustPosition(parentSize: resizeEvent->size());
185 }
186 default:
187 break;
188 }
189 }
190 return QToolButton::eventFilter(watched, event);
191 }
192
193private:
194 void adjustPosition(const QSize &parentSize)
195 {
196 QStyleOption sopt;
197 sopt.initFrom(w: parentWidget());
198 const int topMargin = 0; // style()->pixelMetric(QStyle::PM_LayoutTopMargin, &sopt, parentWidget());
199 const int rightMargin = 0; // style()->pixelMetric(QStyle::PM_LayoutRightMargin, &sopt, parentWidget());
200 if (isLeftToRight()) {
201 move(ax: parentSize.width() - width() - rightMargin, ay: topMargin);
202 } else {
203 move(ax: 0, ay: 0);
204 }
205 }
206
207private:
208 QWidget *m_watched;
209};
210
211KateVariableExpansionDialog::KateVariableExpansionDialog(QWidget *parent)
212 : QDialog(parent, Qt::Tool)
213 , m_showAction(new QAction(QIcon::fromTheme(QStringLiteral("code-context")), i18n("Insert variable"), this))
214 , m_variableModel(new VariableItemModel(this))
215 , m_listView(new QListView(this))
216{
217 setWindowTitle(i18n("Variables"));
218
219 auto vbox = new QVBoxLayout(this);
220 m_filterEdit = new QLineEdit(this);
221 m_filterEdit->setPlaceholderText(i18n("Filter"));
222 m_filterEdit->setFocus();
223 m_filterEdit->installEventFilter(filterObj: this);
224 vbox->addWidget(m_filterEdit);
225 vbox->addWidget(m_listView);
226 m_listView->setUniformItemSizes(true);
227
228 m_filterModel = new QSortFilterProxyModel(this);
229 m_filterModel->setFilterRole(Qt::DisplayRole);
230 m_filterModel->setSortRole(Qt::DisplayRole);
231 m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
232 m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive);
233 m_filterModel->setFilterKeyColumn(0);
234
235 m_filterModel->setSourceModel(m_variableModel);
236 m_listView->setModel(m_filterModel);
237
238 connect(sender: m_filterEdit, signal: &QLineEdit::textChanged, context: m_filterModel, slot: &QSortFilterProxyModel::setFilterWildcard);
239
240 auto lblDescription = new QLabel(i18n("Please select a variable."), this);
241 auto lblCurrentValue = new QLabel(this);
242
243 vbox->addWidget(lblDescription);
244 vbox->addWidget(lblCurrentValue);
245
246 // react to selection changes
247 connect(sender: m_listView->selectionModel(),
248 signal: &QItemSelectionModel::currentRowChanged,
249 slot: [this, lblDescription, lblCurrentValue](const QModelIndex &current, const QModelIndex &) {
250 if (current.isValid()) {
251 const auto &var = m_variables[m_filterModel->mapToSource(proxyIndex: current).row()];
252 lblDescription->setText(var.description());
253 if (var.isPrefixMatch()) {
254 lblCurrentValue->setText(i18n("Current value: %1<value>", var.name()));
255 } else {
256 auto activeView = KTextEditor::Editor::instance()->application()->activeMainWindow()->activeView();
257 const auto value = var.evaluate(prefix: var.name(), view: activeView);
258 lblCurrentValue->setText(i18n("Current value: %1", value));
259 }
260 } else {
261 lblDescription->setText(i18n("Please select a variable."));
262 lblCurrentValue->clear();
263 }
264 });
265
266 // insert text on activation
267 connect(sender: m_listView, signal: &QAbstractItemView::activated, slot: [this](const QModelIndex &index) {
268 if (index.isValid()) {
269 const auto &var = m_variables[m_filterModel->mapToSource(proxyIndex: index).row()];
270
271 // not auto, don't fall for string builder, see bug 413474
272 const QString name = QStringLiteral("%{") + var.name() + QLatin1Char('}');
273 if (parentWidget() && parentWidget()->window()) {
274 auto currentWidget = parentWidget()->window()->focusWidget();
275 if (auto lineEdit = qobject_cast<QLineEdit *>(object: currentWidget)) {
276 lineEdit->insert(name);
277 } else if (auto textEdit = qobject_cast<QTextEdit *>(object: currentWidget)) {
278 textEdit->insertPlainText(text: name);
279 }
280 }
281 }
282 });
283
284 // show dialog whenever the action is clicked
285 connect(sender: m_showAction, signal: &QAction::triggered, slot: [this]() {
286 show();
287 activateWindow();
288 });
289
290 resize(w: 400, h: 550);
291}
292
293KateVariableExpansionDialog::~KateVariableExpansionDialog()
294{
295 for (auto it = m_textEditButtons.begin(); it != m_textEditButtons.end(); ++it) {
296 if (it.value()) {
297 delete it.value();
298 }
299 }
300 m_textEditButtons.clear();
301}
302
303void KateVariableExpansionDialog::addVariable(const KTextEditor::Variable &variable)
304{
305 Q_ASSERT(variable.isValid());
306 m_variables.push_back(t: variable);
307
308 m_variableModel->setVariables(m_variables);
309}
310
311int KateVariableExpansionDialog::isEmpty() const
312{
313 return m_variables.isEmpty();
314}
315
316void KateVariableExpansionDialog::addWidget(QWidget *widget)
317{
318 m_widgets.push_back(t: widget);
319 widget->installEventFilter(filterObj: this);
320
321 connect(sender: widget, signal: &QObject::destroyed, context: this, slot: &KateVariableExpansionDialog::onObjectDeleted);
322}
323
324void KateVariableExpansionDialog::onObjectDeleted(QObject *object)
325{
326 m_widgets.removeAll(t: object);
327 if (m_widgets.isEmpty()) {
328 deleteLater();
329 }
330}
331
332bool KateVariableExpansionDialog::eventFilter(QObject *watched, QEvent *event)
333{
334 // filter line edit
335 if (watched == m_filterEdit) {
336 if (event->type() == QEvent::KeyPress) {
337 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
338 const bool forward2list = (keyEvent->key() == Qt::Key_Up) || (keyEvent->key() == Qt::Key_Down) || (keyEvent->key() == Qt::Key_PageUp)
339 || (keyEvent->key() == Qt::Key_PageDown) || (keyEvent->key() == Qt::Key_Enter) || (keyEvent->key() == Qt::Key_Return);
340 if (forward2list) {
341 QCoreApplication::sendEvent(receiver: m_listView, event);
342 return true;
343 }
344 }
345 return QDialog::eventFilter(watched, event);
346 }
347
348 // tracked widgets (tooltips, adding/removing the showAction)
349 switch (event->type()) {
350 case QEvent::FocusIn: {
351 if (auto lineEdit = qobject_cast<QLineEdit *>(object: watched)) {
352 lineEdit->addAction(action: m_showAction, position: QLineEdit::TrailingPosition);
353 } else if (auto textEdit = qobject_cast<QTextEdit *>(object: watched)) {
354 if (!m_textEditButtons.contains(key: textEdit)) {
355 m_textEditButtons[textEdit] = new TextEditButton(m_showAction, textEdit);
356 }
357 m_textEditButtons[textEdit]->raise();
358 m_textEditButtons[textEdit]->show();
359 }
360 break;
361 }
362 case QEvent::FocusOut: {
363 if (auto lineEdit = qobject_cast<QLineEdit *>(object: watched)) {
364 lineEdit->removeAction(action: m_showAction);
365 } else if (auto textEdit = qobject_cast<QTextEdit *>(object: watched)) {
366 if (m_textEditButtons.contains(key: textEdit)) {
367 delete m_textEditButtons[textEdit];
368 m_textEditButtons.remove(key: textEdit);
369 }
370 }
371 break;
372 }
373 case QEvent::ToolTip: {
374 QString inputText;
375 if (auto lineEdit = qobject_cast<QLineEdit *>(object: watched)) {
376 inputText = lineEdit->text();
377 }
378 QString toolTip;
379 if (!inputText.isEmpty()) {
380 auto activeView = KTextEditor::Editor::instance()->application()->activeMainWindow()->activeView();
381 toolTip = KTextEditor::Editor::instance()->expandText(text: inputText, view: activeView);
382 }
383
384 if (!toolTip.isEmpty()) {
385 auto helpEvent = static_cast<QHelpEvent *>(event);
386 QToolTip::showText(pos: helpEvent->globalPos(), text: toolTip, w: qobject_cast<QWidget *>(o: watched));
387 event->accept();
388 return true;
389 }
390 break;
391 }
392 default:
393 break;
394 }
395
396 // auto-hide on focus change
397 auto parentWindow = parentWidget()->window();
398 const bool keepVisible = isActiveWindow() || m_widgets.contains(t: parentWindow->focusWidget());
399 if (!keepVisible) {
400 hide();
401 }
402
403 return QDialog::eventFilter(watched, event);
404}
405
406// kate: space-indent on; indent-width 4; replace-tabs on;
407

source code of ktexteditor/src/utils/katevariableexpansionhelpers.cpp