KTextEditor

katevariableexpansionhelpers.cpp
1 /*
2  SPDX-FileCopyrightText: 2019 Dominik Haumann <[email protected]>
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  */
35 static 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 
85 class VariableItemModel : public QAbstractItemModel
86 {
87 public:
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(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 QVector<KTextEditor::Variable> &variables)
140  {
141  beginResetModel();
142  m_variables = variables;
143  endResetModel();
144  }
145 
146 private:
147  QVector<KTextEditor::Variable> m_variables;
148 };
149 
150 class TextEditButton : public QToolButton
151 {
152 public:
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(this);
160  show();
161  adjustPosition(m_watched->size());
162  }
163 
164 protected:
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 
177 public:
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 
193 private:
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 
207 private:
208  QWidget *m_watched;
209 };
210 
211 KateVariableExpansionDialog::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 
238  connect(m_filterEdit, &QLineEdit::textChanged, m_filterModel, &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(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 {
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 
293 KateVariableExpansionDialog::~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) {
341  QCoreApplication::sendEvent(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 *>(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  }
378  QString toolTip;
379  if (!inputText.isEmpty()) {
381  KTextEditor::Editor::instance()->expandText(inputText, activeView, toolTip);
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;
int indexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
void triggered(bool checked)
QEvent::Type type() const const
int width() const const
Variable for variable expansion.
Definition: variable.h:34
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
void onObjectDeleted(QObject *object)
Called whenever a widget was deleted.
void addVariable(const KTextEditor::Variable &variable)
Adds variable to the expansion list view.
KTextEditor::View * activeView()
Access the active view.
Definition: mainwindow.cpp:48
KTextEditor::MainWindow * activeMainWindow()
Accessor to the active main window.
Definition: application.cpp:40
qsizetype size() const const
void textChanged(const QString &text)
QWidget * viewport() const const
void showText(const QPoint &pos, const QString &text, QWidget *w)
void initFrom(const QWidget *widget)
void expandText(const QString &text, KTextEditor::View *view, QString &output) const
Expands arbitrary text that may contain arbitrary many variables.
bool isValid() const const
void installEventFilter(QObject *filterObj)
static Editor * instance()
Accessor to get the Editor instance.
Definition: ktexteditor.cpp:81
CaseInsensitive
DisplayRole
bool isEmpty() const const
int row() const const
bool sendEvent(QObject *receiver, QEvent *event)
virtual bool eventFilter(QObject *watched, QEvent *event)
int key() const const
int isEmpty() const
Returns true if no variables were added at all to the dialog.
void setFilterWildcard(const QString &pattern)
QString expandMacro(const QString &input, KTextEditor::View *view)
Expands the input text based on the view.
void activated(const QModelIndex &index)
QString i18n(const char *text, const TYPE &arg...)
QString & replace(int position, int n, QChar after)
QString mid(int position, int n) const const
virtual KTextEditor::Application * application() const =0
Current hosting application, if any set.
void currentRowChanged(const QModelIndex &current, const QModelIndex &previous)
Helper for macro expansion.
bool eventFilter(QObject *watched, QEvent *event) override
Reimplemented for the following reasons:
void drawComplexControl(QStyle::ComplexControl cc, const QStyleOptionComplex &option)
QIcon fromTheme(const QString &name)
virtual bool eventFilter(QObject *o, QEvent *e) override
A text widget with KXMLGUIClient that represents a Document.
Definition: view.h:146
void addWidget(QWidget *widget)
Adds widget to the list of widgets that trigger showing this dialog.
void destroyed(QObject *obj)
This file is part of the KDE documentation.
Documentation copyright © 1996-2021 The KDE developers.
Generated on Sat Oct 16 2021 22:59:33 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.