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

KDE's Doxygen guidelines are available online.