KTextEditor

katevariableexpansionhelpers.cpp
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
56{
58{
59 QString output = input;
60 QString oldStr;
61 do {
62 oldStr = output;
63 const int startIndex = output.indexOf(QLatin1String("%{"));
64 if (startIndex < 0) {
65 break;
66 }
67
68 const int endIndex = findClosing(output, startIndex + 2);
69 if (endIndex <= startIndex) {
70 break;
71 }
72
73 const int varLen = endIndex - (startIndex + 2);
74 QString variable = output.mid(startIndex + 2, varLen);
75 variable = expandMacro(variable, view);
76 if (KTextEditor::Editor::instance()->expandVariable(variable, view, variable)) {
77 output.replace(startIndex, endIndex - startIndex + 1, 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)
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(row, 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 {
142 m_variables = variables;
144 }
145
146private:
148};
149
150class TextEditButton : public QToolButton
151{
152public:
153 TextEditButton(QAction *showAction, QTextEdit *parent)
155 {
156 setAutoRaise(true);
157 setDefaultAction(showAction);
158 m_watched = parent->viewport();
159 m_watched->installEventFilter(this);
160 show();
161 adjustPosition(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);
170 initStyleOption(&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;
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(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(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(parentSize.width() - width() - rightMargin, topMargin);
202 } else {
203 move(0, 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(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
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(m_listView->selectionModel(),
249 [this, lblDescription, lblCurrentValue](const QModelIndex &current, const QModelIndex &) {
250 if (current.isValid()) {
251 const auto &var = m_variables[m_filterModel->mapToSource(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(var.name(), 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(m_listView, &QAbstractItemView::activated, [this](const QModelIndex &index) {
268 if (index.isValid()) {
269 const auto &var = m_variables[m_filterModel->mapToSource(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 *>(currentWidget)) {
276 lineEdit->insert(name);
277 } else if (auto textEdit = qobject_cast<QTextEdit *>(currentWidget)) {
278 textEdit->insertPlainText(name);
279 }
280 }
281 }
282 });
283
284 // show dialog whenever the action is clicked
285 connect(m_showAction, &QAction::triggered, [this]() {
286 show();
287 activateWindow();
288 });
289
290 resize(400, 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
304{
305 Q_ASSERT(variable.isValid());
306 m_variables.push_back(variable);
307
308 m_variableModel->setVariables(m_variables);
309}
310
312{
313 return m_variables.isEmpty();
314}
315
317{
318 m_widgets.push_back(widget);
319 widget->installEventFilter(this);
320
322}
323
325{
326 m_widgets.removeAll(object);
327 if (m_widgets.isEmpty()) {
328 deleteLater();
329 }
330}
331
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) {
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 *>(watched)) {
352 lineEdit->addAction(m_showAction, QLineEdit::TrailingPosition);
353 } else if (auto textEdit = qobject_cast<QTextEdit *>(watched)) {
354 if (!m_textEditButtons.contains(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 *>(watched)) {
364 lineEdit->removeAction(m_showAction);
365 } else if (auto textEdit = qobject_cast<QTextEdit *>(watched)) {
366 if (m_textEditButtons.contains(textEdit)) {
367 delete m_textEditButtons[textEdit];
368 m_textEditButtons.remove(textEdit);
369 }
370 }
371 break;
372 }
373 case QEvent::ToolTip: {
374 QString inputText;
375 if (auto lineEdit = qobject_cast<QLineEdit *>(watched)) {
376 inputText = lineEdit->text();
377 }
379 if (!inputText.isEmpty()) {
381 toolTip = KTextEditor::Editor::instance()->expandText(inputText, activeView);
382 }
383
384 if (!toolTip.isEmpty()) {
385 auto helpEvent = static_cast<QHelpEvent *>(event);
386 QToolTip::showText(helpEvent->globalPos(), toolTip, qobject_cast<QWidget *>(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(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;
KTextEditor::MainWindow * activeMainWindow()
Accessor to the active main window.
static Editor * instance()
Accessor to get the Editor instance.
QString expandText(const QString &text, KTextEditor::View *view) const
Expands arbitrary text that may contain arbitrary many variables.
virtual KTextEditor::Application * application() const =0
Current hosting application, if any set.
KTextEditor::View * activeView()
Access the active view.
Variable for variable expansion.
Definition variable.h:35
bool isValid() const
Returns true, if the name is non-empty and the function provided in the constructor is not a nullptr.
Definition variable.cpp:19
A text widget with KXMLGUIClient that represents a Document.
Definition view.h:244
void addWidget(QWidget *widget)
Adds widget to the list of widgets that trigger showing this dialog.
void onObjectDeleted(QObject *object)
Called whenever a widget was deleted.
void addVariable(const KTextEditor::Variable &variable)
Adds variable to the expansion list view.
bool eventFilter(QObject *watched, QEvent *event) override
Reimplemented for the following reasons:
int isEmpty() const
Returns true if no variables were added at all to the dialog.
QString i18n(const char *text, const TYPE &arg...)
Helper for macro expansion.
QString expandMacro(const QString &input, KTextEditor::View *view)
Expands the input text based on the view.
QModelIndex createIndex(int row, int column, const void *ptr) const const
void activated(const QModelIndex &index)
void triggered(bool checked)
bool sendEvent(QObject *receiver, QEvent *event)
virtual bool eventFilter(QObject *o, QEvent *e) override
iterator begin()
void clear()
bool contains(const Key &key) const const
iterator end()
bool remove(const Key &key)
void currentRowChanged(const QModelIndex &current, const QModelIndex &previous)
void textChanged(const QString &text)
bool contains(const AT &value) const const
bool isEmpty() const const
void push_back(parameter_type value)
qsizetype removeAll(const AT &t)
qsizetype size() const const
bool isValid() const const
int row() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void deleteLater()
void destroyed(QObject *obj)
virtual bool eventFilter(QObject *watched, QEvent *event)
void installEventFilter(QObject *filterObj)
QObject * parent() const const
int width() const const
void setFilterWildcard(const QString &pattern)
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
QString mid(qsizetype position, qsizetype n) const const
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
qsizetype size() const const
virtual void drawComplexControl(ComplexControl control, const QStyleOptionComplex *option, QPainter *painter, const QWidget *widget) const const=0
void initFrom(const QWidget *widget)
CaseInsensitive
DisplayRole
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void setAutoRaise(bool enable)
virtual bool event(QEvent *event) override
virtual void initStyleOption(QStyleOptionToolButton *option) const const
void setDefaultAction(QAction *action)
void showText(const QPoint &pos, const QString &text, QWidget *w, const QRect &rect, int msecDisplayTime)
virtual bool event(QEvent *event) override
void hide()
QWidget * parentWidget() const const
void move(const QPoint &)
virtual void resizeEvent(QResizeEvent *event)
void show()
QWidget * window() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:15:44 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.