Kirigami2

mnemonicattached.cpp
1 /*
2  * SPDX-FileCopyrightText: 2017 Marco Martin <[email protected]>
3  *
4  * SPDX-License-Identifier: LGPL-2.0-or-later
5  */
6 
7 #include "mnemonicattached.h"
8 #include <QDebug>
9 #include <QQuickItem>
10 #include <QQuickRenderControl>
11 
13 
14 // If pos points to alphanumeric X in "...(X)...", which is preceded or
15 // followed only by non-alphanumerics, then "(X)" gets removed.
16 static QString removeReducedCJKAccMark(const QString &label, int pos)
17 {
18  /* clang-format off */
19  if (pos > 0 && pos + 1 < label.length()
20  && label[pos - 1] == QLatin1Char('(')
21  && label[pos + 1] == QLatin1Char(')')
22  && label[pos].isLetterOrNumber()) { /* clang-format on */
23  // Check if at start or end, ignoring non-alphanumerics.
24  int len = label.length();
25  int p1 = pos - 2;
26  while (p1 >= 0 && !label[p1].isLetterOrNumber()) {
27  --p1;
28  }
29  ++p1;
30  int p2 = pos + 2;
31  while (p2 < len && !label[p2].isLetterOrNumber()) {
32  ++p2;
33  }
34  --p2;
35 
36  const QStringView strView(label);
37  if (p1 == 0) {
38  return strView.left(pos - 1) + strView.mid(p2 + 1);
39  } else if (p2 + 1 == len) {
40  return strView.left(p1) + strView.mid(pos + 2);
41  }
42  }
43  return label;
44 }
45 
46 static QString removeAcceleratorMarker(const QString &label_)
47 {
48  QString label = label_;
49 
50  int p = 0;
51  bool accmarkRemoved = false;
52  while (true) {
53  p = label.indexOf(QLatin1Char('&'), p);
54  if (p < 0 || p + 1 == label.length()) {
55  break;
56  }
57 
58  if (label.at(p + 1).isLetterOrNumber()) {
59  // Valid accelerator.
60  const QStringView sv(label);
61  label = sv.left(p) + sv.mid(p + 1);
62 
63  // May have been an accelerator in CJK-style "(&X)"
64  // at the start or end of text.
65  label = removeReducedCJKAccMark(label, p);
66 
67  accmarkRemoved = true;
68  } else if (label.at(p + 1) == QLatin1Char('&')) {
69  // Escaped accelerator marker.
70  const QStringView sv(label);
71  label = sv.left(p) + sv.mid(p + 1);
72  }
73 
74  ++p;
75  }
76 
77  // If no marker was removed, and there are CJK characters in the label,
78  // also try to remove reduced CJK marker -- something may have removed
79  // ampersand beforehand.
80  if (!accmarkRemoved) {
81  bool hasCJK = false;
82  for (const QChar c : std::as_const(label)) {
83  if (c.unicode() >= 0x2e00) { // rough, but should be sufficient
84  hasCJK = true;
85  break;
86  }
87  }
88  if (hasCJK) {
89  p = 0;
90  while (true) {
91  p = label.indexOf(QLatin1Char('('), p);
92  if (p < 0) {
93  break;
94  }
95  label = removeReducedCJKAccMark(label, p + 1);
96  ++p;
97  }
98  }
99  }
100 
101  return label;
102 }
103 
104 MnemonicAttached::MnemonicAttached(QObject *parent)
105  : QObject(parent)
106 {
107  QQuickItem *parentItem = qobject_cast<QQuickItem *>(parent);
108  if (parentItem) {
109  if (parentItem->window()) {
110  m_window = parentItem->window();
111  m_window->installEventFilter(this);
112  }
113  connect(parentItem, &QQuickItem::windowChanged, this, [this](QQuickWindow *window) {
114  removeEventFilterForWindow(m_window);
115  m_window = window;
116  installEventFilterForWindow(m_window);
117  });
118  }
119 }
120 
121 MnemonicAttached::~MnemonicAttached()
122 {
123  s_sequenceToObject.remove(m_sequence);
124 }
125 
126 bool MnemonicAttached::eventFilter(QObject *watched, QEvent *e)
127 {
128  Q_UNUSED(watched)
129 
130  if (m_richTextLabel.isEmpty()) {
131  return false;
132  }
133 
134  if (e->type() == QEvent::KeyPress) {
135  QKeyEvent *ke = static_cast<QKeyEvent *>(e);
136  if (ke->key() == Qt::Key_Alt) {
137  m_actualRichTextLabel = m_richTextLabel;
138  Q_EMIT richTextLabelChanged();
139  m_active = true;
140  Q_EMIT activeChanged();
141  }
142 
143  } else if (e->type() == QEvent::KeyRelease) {
144  QKeyEvent *ke = static_cast<QKeyEvent *>(e);
145  if (ke->key() == Qt::Key_Alt) {
146  m_actualRichTextLabel = removeAcceleratorMarker(m_label);
147  Q_EMIT richTextLabelChanged();
148  m_active = false;
149  Q_EMIT activeChanged();
150  }
151  }
152  return false;
153 }
154 
155 // Algorithm adapted from KAccelString
156 void MnemonicAttached::calculateWeights()
157 {
158  m_weights.clear();
159 
160  int pos = 0;
161  bool start_character = true;
162  bool wanted_character = false;
163 
164  while (pos < m_label.length()) {
165  QChar c = m_label[pos];
166 
167  // skip non typeable characters
168  if (!c.isLetterOrNumber() && c != QLatin1Char('&')) {
169  start_character = true;
170  ++pos;
171  continue;
172  }
173 
174  int weight = 1;
175 
176  // add special weight to first character
177  if (pos == 0) {
178  weight += FIRST_CHARACTER_EXTRA_WEIGHT;
179  // add weight to word beginnings
180  } else if (start_character) {
181  weight += WORD_BEGINNING_EXTRA_WEIGHT;
182  start_character = false;
183  }
184 
185  // add weight to characters that have an & beforehand
186  if (wanted_character) {
187  weight += WANTED_ACCEL_EXTRA_WEIGHT;
188  wanted_character = false;
189  }
190 
191  // add decreasing weight to left characters
192  if (pos < 50) {
193  weight += (50 - pos);
194  }
195 
196  // try to preserve the wanted accelerators
197  /* clang-format off */
198  if (c == QLatin1Char('&')
199  && (pos != m_label.length() - 1
200  && m_label[pos + 1] != QLatin1Char('&')
201  && m_label[pos + 1].isLetterOrNumber())) { /* clang-format on */
202  wanted_character = true;
203  ++pos;
204  continue;
205  }
206 
207  while (m_weights.contains(weight)) {
208  ++weight;
209  }
210 
211  if (c != QLatin1Char('&')) {
212  m_weights[weight] = c;
213  }
214 
215  ++pos;
216  }
217 
218  // update our maximum weight
219  if (m_weights.isEmpty()) {
220  m_weight = m_baseWeight;
221  } else {
222  m_weight = m_baseWeight + (m_weights.cend() - 1).key();
223  }
224 }
225 
226 bool MnemonicAttached::installEventFilterForWindow(QQuickWindow *wnd)
227 {
228  if (!wnd) {
229  return false;
230  }
231  QWindow *renderWindow = QQuickRenderControl::renderWindowFor(wnd);
232  // renderWindow means the widget is rendering somewhere else, like a QQuickWidget
233  if (renderWindow && renderWindow != m_window) {
234  renderWindow->installEventFilter(this);
235  } else {
236  wnd->installEventFilter(this);
237  }
238  return true;
239 }
240 
241 bool MnemonicAttached::removeEventFilterForWindow(QQuickWindow *wnd)
242 {
243  if (!wnd) {
244  return false;
245  }
246  QWindow *renderWindow = QQuickRenderControl::renderWindowFor(wnd);
247  if (renderWindow) {
248  renderWindow->removeEventFilter(this);
249  } else {
250  wnd->removeEventFilter(this);
251  }
252  return true;
253 }
254 
255 void MnemonicAttached::updateSequence()
256 {
257  if (!m_sequence.isEmpty()) {
258  s_sequenceToObject.remove(m_sequence);
259  m_sequence = {};
260  }
261 
262  calculateWeights();
263 
264  // Preserve strings like "One & Two" where & is not an accelerator escape
265  const QString text = label().replace(QStringLiteral("& "), QStringLiteral("&& "));
266 
267  if (!m_enabled) {
268  m_actualRichTextLabel = removeAcceleratorMarker(text);
269  // was the label already completely plain text? try to limit signal emission
270  if (m_mnemonicLabel != m_actualRichTextLabel) {
271  m_mnemonicLabel = m_actualRichTextLabel;
272  Q_EMIT mnemonicLabelChanged();
273  Q_EMIT richTextLabelChanged();
274  }
275  return;
276  }
277 
278  if (!m_weights.isEmpty()) {
280  do {
281  --i;
282  QChar c = i.value();
283 
284  QKeySequence ks(QStringLiteral("Alt+") % c);
285  MnemonicAttached *otherMa = s_sequenceToObject.value(ks);
286  Q_ASSERT(otherMa != this);
287  if (!otherMa || otherMa->m_weight < m_weight) {
288  // the old shortcut is less valuable than the current: remove it
289  if (otherMa) {
290  s_sequenceToObject.remove(otherMa->sequence());
291  otherMa->m_sequence = {};
292  }
293 
294  s_sequenceToObject[ks] = this;
295  m_sequence = ks;
296  m_richTextLabel = text;
297  m_richTextLabel.replace(QRegularExpression(QLatin1String("\\&([^\\&])")), QStringLiteral("\\1"));
298  m_actualRichTextLabel = m_richTextLabel;
299  m_mnemonicLabel = text;
300  const int mnemonicPos = m_mnemonicLabel.indexOf(c);
301 
302  if (mnemonicPos > -1 && (mnemonicPos == 0 || m_mnemonicLabel[mnemonicPos - 1] != QLatin1Char('&'))) {
303  m_mnemonicLabel.replace(mnemonicPos, 1, QStringLiteral("&") % c);
304  }
305 
306  const int richTextPos = m_richTextLabel.indexOf(c);
307  if (richTextPos > -1) {
308  m_richTextLabel.replace(richTextPos, 1, QLatin1String("<u>") % c % QLatin1String("</u>"));
309  }
310 
311  // remap the sequence of the previous shortcut
312  if (otherMa) {
313  otherMa->updateSequence();
314  }
315 
316  break;
317  }
318  } while (i != m_weights.constBegin());
319  }
320 
321  if (!m_sequence.isEmpty()) {
322  Q_EMIT sequenceChanged();
323  }
324  m_actualRichTextLabel = removeAcceleratorMarker(text);
325  m_mnemonicLabel = m_actualRichTextLabel;
326 
327  Q_EMIT richTextLabelChanged();
328  Q_EMIT mnemonicLabelChanged();
329 }
330 
331 void MnemonicAttached::setLabel(const QString &text)
332 {
333  if (m_label == text) {
334  return;
335  }
336 
337  m_label = text;
338  updateSequence();
339  Q_EMIT labelChanged();
340 }
341 
343 {
344  if (!m_actualRichTextLabel.isEmpty()) {
345  return m_actualRichTextLabel;
346  } else {
347  return removeAcceleratorMarker(m_label);
348  }
349 }
350 
352 {
353  return m_mnemonicLabel;
354 }
355 
357 {
358  return m_label;
359 }
360 
361 void MnemonicAttached::setEnabled(bool enabled)
362 {
363  if (m_enabled == enabled) {
364  return;
365  }
366 
367  m_enabled = enabled;
368  updateSequence();
369  Q_EMIT enabledChanged();
370 }
371 
372 bool MnemonicAttached::enabled() const
373 {
374  return m_enabled;
375 }
376 
377 void MnemonicAttached::setControlType(MnemonicAttached::ControlType controlType)
378 {
379  if (m_controlType == controlType) {
380  return;
381  }
382 
383  m_controlType = controlType;
384 
385  switch (controlType) {
386  case ActionElement:
387  m_baseWeight = ACTION_ELEMENT_WEIGHT;
388  break;
389  case DialogButton:
390  m_baseWeight = DIALOG_BUTTON_EXTRA_WEIGHT;
391  break;
392  case MenuItem:
393  m_baseWeight = MENU_ITEM_WEIGHT;
394  break;
395  case FormLabel:
396  m_baseWeight = FORM_LABEL_WEIGHT;
397  break;
398  default:
399  m_baseWeight = SECONDARY_CONTROL_WEIGHT;
400  break;
401  }
402  // update our maximum weight
403  if (m_weights.isEmpty()) {
404  m_weight = m_baseWeight;
405  } else {
406  m_weight = m_baseWeight + (m_weights.constEnd() - 1).key();
407  }
408  Q_EMIT controlTypeChanged();
409 }
410 
412 {
413  return m_controlType;
414 }
415 
417 {
418  return m_sequence;
419 }
420 
421 bool MnemonicAttached::active() const
422 {
423  return m_active;
424 }
425 
426 MnemonicAttached *MnemonicAttached::qmlAttachedProperties(QObject *object)
427 {
428  return new MnemonicAttached(object);
429 }
430 
431 void MnemonicAttached::setActive(bool active)
432 {
433  // We can't rely on previous value when it's true since it can be
434  // caused by Alt key press and we need to remove the event filter
435  // additionally. False should be ok as it's a default state.
436  if (!m_active && m_active == active) {
437  return;
438  }
439 
440  m_active = active;
441 
442  if (m_active) {
443  removeEventFilterForWindow(m_window);
444  if (m_actualRichTextLabel != m_richTextLabel) {
445  m_actualRichTextLabel = m_richTextLabel;
446  Q_EMIT richTextLabelChanged();
447  }
448 
449  } else {
450  installEventFilterForWindow(m_window);
451  m_actualRichTextLabel = removeAcceleratorMarker(m_label);
452  Q_EMIT richTextLabelChanged();
453  }
454 
455  Q_EMIT activeChanged();
456 }
457 
458 #include "moc_mnemonicattached.cpp"
void removeEventFilter(QObject *obj)
bool active
True when the user is pressing alt and the accelerators should be shown.
const T value(const Key &key, const T &defaultValue) const const
QQuickWindow * window() const const
bool isLetterOrNumber() const const
void windowChanged(QQuickWindow *window)
bool enabled
Only if true this mnemonic will be considered for the global assignment default: true.
QMap::const_iterator constEnd() const const
void installEventFilter(QObject *filterObj)
QString label
The label of the control we want to compute a mnemonic for, instance "Label:" or "&Ok".
int length() const const
int indexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
QString richTextLabel
The user-visible final label, which will have the shortcut letter underlined, such as "<u>O</u>k".
QString & replace(int position, int n, QChar after)
QString label(StandardShortcut id)
QWindow * renderWindowFor(QQuickWindow *win, QPoint *offset)
QString mnemonicLabel
The label with an "&" mnemonic in the place which will have the shortcut assigned,...
This Attached property is used to calculate automated keyboard sequences to trigger actions based upo...
int key() const const
QString left(int n) const const
QEvent::Type type() const const
const QChar at(int position) const const
QKeySequence sequence
The final key sequence assigned, if any: it will be Alt+alphanumeric char.
KJOBWIDGETS_EXPORT QWidget * window(KJob *job)
QString mid(int position, int n) const const
MnemonicAttached::ControlType controlType
The type of control this mnemonic is attached: different types of controls have different importance ...
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Sun Jan 29 2023 04:11:03 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.