diff --git a/src/mnemonicattached.cpp b/src/mnemonicattached.cpp index ab992511..a67843de 100644 --- a/src/mnemonicattached.cpp +++ b/src/mnemonicattached.cpp @@ -1,337 +1,327 @@ /* * Copyright (C) 2017 by Marco Martin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Library General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "mnemonicattached.h" #include #include #include #include QHash MnemonicAttached::s_sequenceToObject = QHash(); MnemonicAttached::MnemonicAttached(QObject *parent) : QObject(parent) { QQuickItem *parentItem = qobject_cast(parent); if (parentItem) { if (parentItem->window()) { m_window = parentItem->window(); m_window->installEventFilter(this); } connect(parentItem, &QQuickItem::windowChanged, this, [this](QQuickWindow *window) { if (m_window) { QWindow *renderWindow = QQuickRenderControl::renderWindowFor(m_window); if (renderWindow) { renderWindow->removeEventFilter(this); } else { m_window->removeEventFilter(this); } } m_window = window; if (m_window) { QWindow *renderWindow = QQuickRenderControl::renderWindowFor(m_window); //renderWindow means the widget is rendering somewhere else, like a QQuickWidget if (renderWindow && renderWindow != m_window) { renderWindow->installEventFilter(this); } else { m_window->installEventFilter(this); } } - updateSequence(); }); } } MnemonicAttached::~MnemonicAttached() { s_sequenceToObject.remove(m_sequence); } bool MnemonicAttached::eventFilter(QObject *watched, QEvent *e) { Q_UNUSED(watched) if (m_richTextLabel.isEmpty()) { return false; } if (e->type() == QEvent::KeyPress) { QKeyEvent *ke = static_cast(e); if (ke->key() == Qt::Key_Alt) { m_actualRichTextLabel = m_richTextLabel; emit richTextLabelChanged(); } } else if (e->type() == QEvent::KeyRelease) { QKeyEvent *ke = static_cast(e); if (ke->key() == Qt::Key_Alt) { m_actualRichTextLabel = m_label; m_actualRichTextLabel.replace(QRegularExpression(QStringLiteral("\\&([^\\&])")), QStringLiteral("\\1")); emit richTextLabelChanged(); } } return false; } //Algorythm adapted from KAccelString void MnemonicAttached::calculateWeights() { m_weights.clear(); int pos = 0; bool start_character = true; bool wanted_character = false; while (pos < m_label.length()) { QChar c = m_label[pos]; - // try to preserve the wanted accelerators - if (c == QLatin1Char('&') && (pos == m_label.length() - 1 || m_label[pos+1] != QLatin1Char('&'))) { - wanted_character = true; - ++pos; - continue; - } - // skip non typeable characters if (!c.isLetterOrNumber()) { start_character = true; ++pos; continue; } int weight = 1; - // see if we are rendering to an offscreen widget - // in this case means our qml scene is inside qwidgets: we can't check - // our automatic shortcuts aren't conflicting with the ones from qwidgets - QWindow *renderWindow = QQuickRenderControl::renderWindowFor(m_window); - if (!m_window || (renderWindow && m_window != renderWindow)) { - weight = -150; - } // add special weight to first character if (pos == 0) { weight += FIRST_CHARACTER_EXTRA_WEIGHT; } // add weight to word beginnings if (start_character) { weight += WORD_BEGINNING_EXTRA_WEIGHT; start_character = false; } // add weight to word beginnings if (wanted_character) { weight += WANTED_ACCEL_EXTRA_WEIGHT; wanted_character = false; } // add decreasing weight to left characters if (pos < 50) { weight += (50 - pos); } + // try to preserve the wanted accelerators + if (c == QLatin1Char('&') && (pos == m_label.length() - 1 || m_label[pos+1] != QLatin1Char('&'))) { + wanted_character = true; + ++pos; + continue; + } + while (m_weights.contains(weight)) { ++weight; } - if (weight > 0) { - m_weights[weight] = c; - } + m_weights[weight] = c; ++pos; } //update our maximum weight if (m_weights.isEmpty()) { m_weight = m_baseWeight; } else { m_weight = m_baseWeight + m_weights.keys().last(); } } void MnemonicAttached::updateSequence() { if (!m_sequence.isEmpty()) { s_sequenceToObject.remove(m_sequence); m_sequence = {}; } calculateWeights(); const QString text = label(); if (!m_enabled) { m_actualRichTextLabel = text; m_actualRichTextLabel.replace(QRegularExpression(QStringLiteral("\\&([^\\&])")), QStringLiteral("\\1")); //was the label already completely plain text? try to limit signal emission if (m_mnemonicLabel != m_actualRichTextLabel) { m_mnemonicLabel = m_actualRichTextLabel; emit mnemonicLabelChanged(); emit richTextLabelChanged(); } return; } if (m_weights.isEmpty()) { return; } QMap::const_iterator i = m_weights.constEnd(); do { --i; QChar c = i.value(); QKeySequence ks(QStringLiteral("Alt+") % c); MnemonicAttached *otherMa = s_sequenceToObject.value(ks); Q_ASSERT(otherMa != this); if (!otherMa || otherMa->m_weight < m_weight) { //the old shortcut is less valuable than the current: remove it if (otherMa) { s_sequenceToObject.remove(otherMa->sequence()); otherMa->m_sequence = {}; } s_sequenceToObject[ks] = this; m_sequence = ks; m_richTextLabel = text; m_richTextLabel.replace(QRegularExpression(QLatin1String("\\&([^\\&])")), QStringLiteral("\\1")); m_actualRichTextLabel = m_richTextLabel; m_mnemonicLabel = m_richTextLabel; const int mnemonicPos = m_mnemonicLabel.indexOf(c); if (mnemonicPos > -1) { m_mnemonicLabel.replace(mnemonicPos, 1, c); } const int richTextPos = m_richTextLabel.indexOf(c); if (richTextPos > -1) { m_richTextLabel.replace(richTextPos, 1, QLatin1String("") % c % QLatin1String("")); } //remap the sequence of the previous shortcut if (otherMa) { otherMa->updateSequence(); } break; } } while (i != m_weights.constBegin()); if (!m_sequence.isEmpty()) { emit sequenceChanged(); } else { m_actualRichTextLabel = text; m_actualRichTextLabel.replace(QRegularExpression(QStringLiteral("\\&([^\\&])")), QStringLiteral("\\1")); m_mnemonicLabel = m_actualRichTextLabel; } emit richTextLabelChanged(); emit mnemonicLabelChanged(); } void MnemonicAttached::setLabel(const QString &text) { if (m_label == text) { return; } m_label = text; updateSequence(); emit labelChanged(); } QString MnemonicAttached::richTextLabel() const { return !m_actualRichTextLabel.isEmpty() ? m_actualRichTextLabel : m_label; } QString MnemonicAttached::mnemonicLabel() const { return m_mnemonicLabel; } QString MnemonicAttached::label() const { return m_label; } void MnemonicAttached::setEnabled(bool enabled) { if (m_enabled == enabled) { return; } m_enabled = enabled; updateSequence(); emit enabledChanged(); } bool MnemonicAttached::enabled() const { return m_enabled; } void MnemonicAttached::setControlType(MnemonicAttached::ControlType controlType) { if (m_controlType == controlType) { return; } m_controlType = controlType; switch (controlType) { case ActionElement: m_baseWeight = ACTION_ELEMENT_WEIGHT; break; case DialogButton: m_baseWeight = DIALOG_BUTTON_EXTRA_WEIGHT; break; case MenuItem: m_baseWeight = MENU_ITEM_WEIGHT; break; case FormLabel: m_baseWeight = FORM_LABEL_WEIGHT; break; default: m_baseWeight = SECONDARY_CONTROL_WEIGHT; break; } //update our maximum weight if (m_weights.isEmpty()) { m_weight = m_baseWeight; } else { m_weight = m_baseWeight + (m_weights.constEnd() - 1).key(); } emit controlTypeChanged(); } MnemonicAttached::ControlType MnemonicAttached::controlType() const { return m_controlType; } QKeySequence MnemonicAttached::sequence() { return m_sequence; } MnemonicAttached *MnemonicAttached::qmlAttachedProperties(QObject *object) { return new MnemonicAttached(object); } #include "moc_mnemonicattached.cpp" diff --git a/src/mnemonicattached.h b/src/mnemonicattached.h index 8803f29e..5b9c0e24 100644 --- a/src/mnemonicattached.h +++ b/src/mnemonicattached.h @@ -1,169 +1,169 @@ /* * Copyright (C) 2017 by Marco Martin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Library General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef MNEMONICATTACHED_H #define MNEMONICATTACHED_H #include #include #include class QQuickItem; /** * This Attached property is used to calculate automated keyboard sequences * to trigger actions based upon their text: if an "&" mnemonic is * used (ie "&Ok"), the system will attempt to assign the desired letter giving * it priority, otherwise a letter among the ones in the label will be used if * possible and not conflicting. * Different kinds of controls will have different priorities in assigning the * shortcut: for instance the "Ok/Cancel" buttons in a dialog will have priority * over fields of a FormLayout. * @see ControlType * * Usually the developer shouldn't use this directly as base components * already use this, but only when implementing a custom graphical Control. * @since 2.3 */ class MnemonicAttached : public QObject { Q_OBJECT /** * The label of the control we want to compute a mnemonic for, instance * "Label:" or "&Ok" */ Q_PROPERTY(QString label READ label WRITE setLabel NOTIFY labelChanged) /** * The user-visible final label, which will have the shortcut letter underlined, * such as "<u>O</u>k" */ Q_PROPERTY(QString richTextLabel READ richTextLabel NOTIFY richTextLabelChanged) /** * The label with an "&" mnemonic in the place which will have the shortcut * assigned, regardless the & wasassigned by the user or automatically generated. */ Q_PROPERTY(QString mnemonicLabel READ mnemonicLabel NOTIFY mnemonicLabelChanged) /** * Only if true this mnemonic will be considered for the global assignment * default: true */ Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged) /** * the type of control this mnemonic is attached: different types of controls have different importance and priority for shortcut assignment. * @see ControlType */ Q_PROPERTY(MnemonicAttached::ControlType controlType READ controlType WRITE setControlType NOTIFY controlTypeChanged) /** * The final key sequence assigned, if any: it will be Alt+alphanumeric char */ Q_PROPERTY(QKeySequence sequence READ sequence NOTIFY sequenceChanged) public: enum ControlType { ActionElement, /** pushbuttons, checkboxes etc */ DialogButton, /** buttons for dialogs */ MenuItem, /** Menu items */ FormLabel, /** Buddy label in a FormLayout*/ SecondaryControl /** Other controls that are considered not much important and low priority for shortcuts */ }; Q_ENUM(ControlType) explicit MnemonicAttached(QObject *parent = nullptr); ~MnemonicAttached(); void setLabel(const QString &text); QString label() const; QString richTextLabel() const; QString mnemonicLabel() const; void setEnabled(bool enabled); bool enabled() const; void setControlType(MnemonicAttached::ControlType controlType); ControlType controlType() const; QKeySequence sequence(); //QML attached property static MnemonicAttached *qmlAttachedProperties(QObject *object); protected: bool eventFilter(QObject *watched, QEvent *e) override; void updateSequence(); Q_SIGNALS: void labelChanged(); void enabledChanged(); void sequenceChanged(); void richTextLabelChanged(); void mnemonicLabelChanged(); void controlTypeChanged(); private: void calculateWeights(); //TODO: to have support for DIALOG_BUTTON_EXTRA_WEIGHT etc, a type enum should be exported enum { // Additional weight for first character in string FIRST_CHARACTER_EXTRA_WEIGHT = 50, // Additional weight for the beginning of a word WORD_BEGINNING_EXTRA_WEIGHT = 50, // Additional weight for a 'wanted' accelerator ie string with '&' - WANTED_ACCEL_EXTRA_WEIGHT = 450, + WANTED_ACCEL_EXTRA_WEIGHT = 150, // Default weight for an 'action' widget (ie, pushbuttons) ACTION_ELEMENT_WEIGHT = 50, // Additional weight for the dialog buttons (large, we basically never want these reassigned) DIALOG_BUTTON_EXTRA_WEIGHT = 300, // Weight for FormLayout labels (low) FORM_LABEL_WEIGHT = 20, // Weight for Secondary controls which are considered less important (low) SECONDARY_CONTROL_WEIGHT = 10, // Default weight for menu items MENU_ITEM_WEIGHT = 250 }; //order word letters by weight int m_weight = 0; int m_baseWeight = 0; ControlType m_controlType = SecondaryControl; QMap m_weights; QString m_label; QString m_actualRichTextLabel; QString m_richTextLabel; QString m_mnemonicLabel; QKeySequence m_sequence; bool m_enabled = true; QPointer m_window; //global mapping of mnemonics //TODO: map by QWindow static QHash s_sequenceToObject; }; QML_DECLARE_TYPEINFO(MnemonicAttached, QML_HAS_ATTACHED_PROPERTIES) #endif // MnemonicATTACHED_H