File indexing completed on 2024-04-14 03:53:47
0001 /* 0002 * SPDX-FileCopyrightText: 2017 Marco Martin <mart@kde.org> 0003 * 0004 * SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "mnemonicattached.h" 0008 #include <QDebug> 0009 #include <QGuiApplication> 0010 #include <QQuickItem> 0011 #include <QQuickRenderControl> 0012 #include <QQuickWindow> 0013 #include <QRegularExpression> 0014 #include <QWindow> 0015 0016 QHash<QKeySequence, MnemonicAttached *> MnemonicAttached::s_sequenceToObject = QHash<QKeySequence, MnemonicAttached *>(); 0017 0018 // If pos points to alphanumeric X in "...(X)...", which is preceded or 0019 // followed only by non-alphanumerics, then "(X)" gets removed. 0020 static QString removeReducedCJKAccMark(const QString &label, int pos) 0021 { 0022 /* clang-format off */ 0023 if (pos > 0 && pos + 1 < label.length() 0024 && label[pos - 1] == QLatin1Char('(') 0025 && label[pos + 1] == QLatin1Char(')') 0026 && label[pos].isLetterOrNumber()) { /* clang-format on */ 0027 // Check if at start or end, ignoring non-alphanumerics. 0028 int len = label.length(); 0029 int p1 = pos - 2; 0030 while (p1 >= 0 && !label[p1].isLetterOrNumber()) { 0031 --p1; 0032 } 0033 ++p1; 0034 int p2 = pos + 2; 0035 while (p2 < len && !label[p2].isLetterOrNumber()) { 0036 ++p2; 0037 } 0038 --p2; 0039 0040 const QStringView strView(label); 0041 if (p1 == 0) { 0042 return strView.left(pos - 1) + strView.mid(p2 + 1); 0043 } else if (p2 + 1 == len) { 0044 return strView.left(p1) + strView.mid(pos + 2); 0045 } 0046 } 0047 return label; 0048 } 0049 0050 static QString removeAcceleratorMarker(const QString &label_) 0051 { 0052 QString label = label_; 0053 0054 int p = 0; 0055 bool accmarkRemoved = false; 0056 while (true) { 0057 p = label.indexOf(QLatin1Char('&'), p); 0058 if (p < 0 || p + 1 == label.length()) { 0059 break; 0060 } 0061 0062 if (label.at(p + 1).isLetterOrNumber()) { 0063 // Valid accelerator. 0064 const QStringView sv(label); 0065 label = sv.left(p) + sv.mid(p + 1); 0066 0067 // May have been an accelerator in CJK-style "(&X)" 0068 // at the start or end of text. 0069 label = removeReducedCJKAccMark(label, p); 0070 0071 accmarkRemoved = true; 0072 } else if (label.at(p + 1) == QLatin1Char('&')) { 0073 // Escaped accelerator marker. 0074 const QStringView sv(label); 0075 label = sv.left(p) + sv.mid(p + 1); 0076 } 0077 0078 ++p; 0079 } 0080 0081 // If no marker was removed, and there are CJK characters in the label, 0082 // also try to remove reduced CJK marker -- something may have removed 0083 // ampersand beforehand. 0084 if (!accmarkRemoved) { 0085 bool hasCJK = false; 0086 for (const QChar c : std::as_const(label)) { 0087 if (c.unicode() >= 0x2e00) { // rough, but should be sufficient 0088 hasCJK = true; 0089 break; 0090 } 0091 } 0092 if (hasCJK) { 0093 p = 0; 0094 while (true) { 0095 p = label.indexOf(QLatin1Char('('), p); 0096 if (p < 0) { 0097 break; 0098 } 0099 label = removeReducedCJKAccMark(label, p + 1); 0100 ++p; 0101 } 0102 } 0103 } 0104 0105 return label; 0106 } 0107 0108 class MnemonicEventFilter : public QObject 0109 { 0110 Q_OBJECT 0111 0112 public: 0113 static MnemonicEventFilter &instance() 0114 { 0115 static MnemonicEventFilter s_instance; 0116 return s_instance; 0117 } 0118 0119 bool eventFilter(QObject *watched, QEvent *event) override 0120 { 0121 Q_UNUSED(watched); 0122 0123 if (event->type() == QEvent::KeyPress) { 0124 QKeyEvent *ke = static_cast<QKeyEvent *>(event); 0125 if (ke->key() == Qt::Key_Alt) { 0126 m_altPressed = true; 0127 Q_EMIT altPressed(); 0128 } 0129 } else if (event->type() == QEvent::KeyRelease) { 0130 QKeyEvent *ke = static_cast<QKeyEvent *>(event); 0131 if (ke->key() == Qt::Key_Alt) { 0132 m_altPressed = false; 0133 Q_EMIT altReleased(); 0134 } 0135 } else if (event->type() == QEvent::ApplicationStateChange) { 0136 if (m_altPressed) { 0137 m_altPressed = false; 0138 Q_EMIT altReleased(); 0139 } 0140 } 0141 0142 return false; 0143 } 0144 0145 Q_SIGNALS: 0146 void altPressed(); 0147 void altReleased(); 0148 0149 private: 0150 MnemonicEventFilter() 0151 : QObject(nullptr) 0152 { 0153 qGuiApp->installEventFilter(this); 0154 } 0155 0156 bool m_altPressed = false; 0157 }; 0158 0159 MnemonicAttached::MnemonicAttached(QObject *parent) 0160 : QObject(parent) 0161 { 0162 connect(&MnemonicEventFilter::instance(), &MnemonicEventFilter::altPressed, this, &MnemonicAttached::onAltPressed); 0163 connect(&MnemonicEventFilter::instance(), &MnemonicEventFilter::altReleased, this, &MnemonicAttached::onAltReleased); 0164 } 0165 0166 MnemonicAttached::~MnemonicAttached() 0167 { 0168 s_sequenceToObject.remove(m_sequence); 0169 } 0170 0171 QWindow *MnemonicAttached::window() const 0172 { 0173 if (auto *parentItem = qobject_cast<QQuickItem *>(parent())) { 0174 if (auto *window = parentItem->window()) { 0175 if (auto *renderWindow = QQuickRenderControl::renderWindowFor(window)) { 0176 return renderWindow; 0177 } 0178 0179 return window; 0180 } 0181 } 0182 0183 return nullptr; 0184 } 0185 0186 void MnemonicAttached::onAltPressed() 0187 { 0188 if (!m_active || m_richTextLabel.isEmpty()) { 0189 return; 0190 } 0191 0192 auto *win = window(); 0193 if (!win || !win->isActive()) { 0194 return; 0195 } 0196 0197 m_actualRichTextLabel = m_richTextLabel; 0198 Q_EMIT richTextLabelChanged(); 0199 m_active = true; 0200 Q_EMIT activeChanged(); 0201 } 0202 0203 void MnemonicAttached::onAltReleased() 0204 { 0205 if (!m_active || m_richTextLabel.isEmpty()) { 0206 return; 0207 } 0208 0209 // Disabling menmonics again is always fine, e.g. on window deactivation, 0210 // don't check for window is active here. 0211 0212 m_actualRichTextLabel = removeAcceleratorMarker(m_label); 0213 Q_EMIT richTextLabelChanged(); 0214 m_active = false; 0215 Q_EMIT activeChanged(); 0216 } 0217 0218 // Algorithm adapted from KAccelString 0219 void MnemonicAttached::calculateWeights() 0220 { 0221 m_weights.clear(); 0222 0223 int pos = 0; 0224 bool start_character = true; 0225 bool wanted_character = false; 0226 0227 while (pos < m_label.length()) { 0228 QChar c = m_label[pos]; 0229 0230 // skip non typeable characters 0231 if (!c.isLetterOrNumber() && c != QLatin1Char('&')) { 0232 start_character = true; 0233 ++pos; 0234 continue; 0235 } 0236 0237 int weight = 1; 0238 0239 // add special weight to first character 0240 if (pos == 0) { 0241 weight += FIRST_CHARACTER_EXTRA_WEIGHT; 0242 // add weight to word beginnings 0243 } else if (start_character) { 0244 weight += WORD_BEGINNING_EXTRA_WEIGHT; 0245 start_character = false; 0246 } 0247 0248 // add weight to characters that have an & beforehand 0249 if (wanted_character) { 0250 weight += WANTED_ACCEL_EXTRA_WEIGHT; 0251 wanted_character = false; 0252 } 0253 0254 // add decreasing weight to left characters 0255 if (pos < 50) { 0256 weight += (50 - pos); 0257 } 0258 0259 // try to preserve the wanted accelerators 0260 /* clang-format off */ 0261 if (c == QLatin1Char('&') 0262 && (pos != m_label.length() - 1 0263 && m_label[pos + 1] != QLatin1Char('&') 0264 && m_label[pos + 1].isLetterOrNumber())) { /* clang-format on */ 0265 wanted_character = true; 0266 ++pos; 0267 continue; 0268 } 0269 0270 while (m_weights.contains(weight)) { 0271 ++weight; 0272 } 0273 0274 if (c != QLatin1Char('&')) { 0275 m_weights[weight] = c; 0276 } 0277 0278 ++pos; 0279 } 0280 0281 // update our maximum weight 0282 if (m_weights.isEmpty()) { 0283 m_weight = m_baseWeight; 0284 } else { 0285 m_weight = m_baseWeight + (std::prev(m_weights.cend())).key(); 0286 } 0287 } 0288 0289 void MnemonicAttached::updateSequence() 0290 { 0291 if (!m_sequence.isEmpty()) { 0292 s_sequenceToObject.remove(m_sequence); 0293 m_sequence = {}; 0294 } 0295 0296 calculateWeights(); 0297 0298 // Preserve strings like "One & Two" where & is not an accelerator escape 0299 const QString text = label().replace(QStringLiteral("& "), QStringLiteral("&& ")); 0300 m_actualRichTextLabel = removeAcceleratorMarker(text); 0301 0302 if (!m_enabled) { 0303 // was the label already completely plain text? try to limit signal emission 0304 if (m_mnemonicLabel != m_actualRichTextLabel) { 0305 m_mnemonicLabel = m_actualRichTextLabel; 0306 Q_EMIT mnemonicLabelChanged(); 0307 Q_EMIT richTextLabelChanged(); 0308 } 0309 return; 0310 } 0311 0312 m_mnemonicLabel = text; 0313 m_mnemonicLabel.replace(QRegularExpression(QLatin1String("\\&([^\\&])")), QStringLiteral("\\1")); 0314 0315 if (!m_weights.isEmpty()) { 0316 QMap<int, QChar>::const_iterator i = m_weights.constEnd(); 0317 do { 0318 --i; 0319 QChar c = i.value(); 0320 0321 QKeySequence ks(QStringLiteral("Alt+") % c); 0322 MnemonicAttached *otherMa = s_sequenceToObject.value(ks); 0323 Q_ASSERT(otherMa != this); 0324 if (!otherMa || otherMa->m_weight < m_weight) { 0325 // the old shortcut is less valuable than the current: remove it 0326 if (otherMa) { 0327 s_sequenceToObject.remove(otherMa->sequence()); 0328 otherMa->m_sequence = {}; 0329 } 0330 0331 s_sequenceToObject[ks] = this; 0332 m_sequence = ks; 0333 m_richTextLabel = text; 0334 m_richTextLabel.replace(QRegularExpression(QLatin1String("\\&([^\\&])")), QStringLiteral("\\1")); 0335 m_mnemonicLabel = text; 0336 const int mnemonicPos = m_mnemonicLabel.indexOf(c); 0337 0338 if (mnemonicPos > -1 && (mnemonicPos == 0 || m_mnemonicLabel[mnemonicPos - 1] != QLatin1Char('&'))) { 0339 m_mnemonicLabel.replace(mnemonicPos, 1, QStringLiteral("&") % c); 0340 } 0341 0342 const int richTextPos = m_richTextLabel.indexOf(c); 0343 if (richTextPos > -1) { 0344 m_richTextLabel.replace(richTextPos, 1, QLatin1String("<u>") % c % QLatin1String("</u>")); 0345 } 0346 0347 // remap the sequence of the previous shortcut 0348 if (otherMa) { 0349 otherMa->updateSequence(); 0350 } 0351 0352 break; 0353 } 0354 } while (i != m_weights.constBegin()); 0355 } 0356 0357 if (!m_sequence.isEmpty()) { 0358 Q_EMIT sequenceChanged(); 0359 } 0360 0361 Q_EMIT richTextLabelChanged(); 0362 Q_EMIT mnemonicLabelChanged(); 0363 } 0364 0365 void MnemonicAttached::setLabel(const QString &text) 0366 { 0367 if (m_label == text) { 0368 return; 0369 } 0370 0371 m_label = text; 0372 updateSequence(); 0373 Q_EMIT labelChanged(); 0374 } 0375 0376 QString MnemonicAttached::richTextLabel() const 0377 { 0378 if (!m_actualRichTextLabel.isEmpty()) { 0379 return m_actualRichTextLabel; 0380 } else { 0381 return removeAcceleratorMarker(m_label); 0382 } 0383 } 0384 0385 QString MnemonicAttached::mnemonicLabel() const 0386 { 0387 return m_mnemonicLabel; 0388 } 0389 0390 QString MnemonicAttached::label() const 0391 { 0392 return m_label; 0393 } 0394 0395 void MnemonicAttached::setEnabled(bool enabled) 0396 { 0397 if (m_enabled == enabled) { 0398 return; 0399 } 0400 0401 m_enabled = enabled; 0402 updateSequence(); 0403 Q_EMIT enabledChanged(); 0404 } 0405 0406 bool MnemonicAttached::enabled() const 0407 { 0408 return m_enabled; 0409 } 0410 0411 void MnemonicAttached::setControlType(MnemonicAttached::ControlType controlType) 0412 { 0413 if (m_controlType == controlType) { 0414 return; 0415 } 0416 0417 m_controlType = controlType; 0418 0419 switch (controlType) { 0420 case ActionElement: 0421 m_baseWeight = ACTION_ELEMENT_WEIGHT; 0422 break; 0423 case DialogButton: 0424 m_baseWeight = DIALOG_BUTTON_EXTRA_WEIGHT; 0425 break; 0426 case MenuItem: 0427 m_baseWeight = MENU_ITEM_WEIGHT; 0428 break; 0429 case FormLabel: 0430 m_baseWeight = FORM_LABEL_WEIGHT; 0431 break; 0432 default: 0433 m_baseWeight = SECONDARY_CONTROL_WEIGHT; 0434 break; 0435 } 0436 // update our maximum weight 0437 if (m_weights.isEmpty()) { 0438 m_weight = m_baseWeight; 0439 } else { 0440 m_weight = m_baseWeight + (std::prev(m_weights.constEnd())).key(); 0441 } 0442 Q_EMIT controlTypeChanged(); 0443 } 0444 0445 MnemonicAttached::ControlType MnemonicAttached::controlType() const 0446 { 0447 return m_controlType; 0448 } 0449 0450 QKeySequence MnemonicAttached::sequence() 0451 { 0452 return m_sequence; 0453 } 0454 0455 bool MnemonicAttached::active() const 0456 { 0457 return m_active; 0458 } 0459 0460 MnemonicAttached *MnemonicAttached::qmlAttachedProperties(QObject *object) 0461 { 0462 return new MnemonicAttached(object); 0463 } 0464 0465 void MnemonicAttached::setActive(bool active) 0466 { 0467 // We can't rely on previous value when it's true since it can be 0468 // caused by Alt key press and we need to remove the event filter 0469 // additionally. False should be ok as it's a default state. 0470 if (!m_active && m_active == active) { 0471 return; 0472 } 0473 0474 m_active = active; 0475 0476 if (m_active) { 0477 if (m_actualRichTextLabel != m_richTextLabel) { 0478 m_actualRichTextLabel = m_richTextLabel; 0479 Q_EMIT richTextLabelChanged(); 0480 } 0481 0482 } else { 0483 m_actualRichTextLabel = removeAcceleratorMarker(m_label); 0484 Q_EMIT richTextLabelChanged(); 0485 } 0486 0487 Q_EMIT activeChanged(); 0488 } 0489 0490 #include "mnemonicattached.moc"