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