Uploaded image for project: 'Qt'
  1. Qt
  2. QTBUG-116873

Investigations, tasks, and issues related to shortcut handling in Qt

    XMLWordPrintable

Details

    • Shortcut handling in Qt
    • All
    • e2738ca94 (dev), 3aff1e167 (dev), 0bfb25d17 (dev), 709c93083 (dev), 96e762e5a (dev), ca68fa01f (dev), f8f5e2c12 (dev)

    Description

      Background

      Shortcut handling in Qt today is supported via two separate mechanisms.

      QKeyEvent

      Whenever a key is pressed or released, the QPA platform layer picks up the event from the system, and forwards it to QGuiApplication via QWindowSystemInterface::handle{Extended}KeyEvent(). The QPA layer transforms properties of the system event to properties reflected in the QKeyEvent sent by QGuiApplicationPrivate::processKeyEvent().

      Some important properties of a QKeyEvent are:

      nativeScanCode()

      This represents a hardware dependent code for a specific key.  It is not affected by the active keyboard layout (language) or modifiers keys, so the code will be the same for Shift+5 or Control+5 as it will be for pressing 5 alone.

      Being hardware dependent, this property is not practical to use for shortcut handling, as the value may depends on the hardware keyboard attached.

      🐛 On X keyboard platforms, this isn’t actually the scan code (EV_MSC+MSC_SCAN of the evdev event). It’s instead the hardware-independent xkb_keycode_t, which should really be exposed as the virtual key (see below).

      nativeVirtualKey()

      This represents a hardware independent code for a specific key.

      Whether this value is affected by the active keyboard layout (language) or modifiers keys depends on the platform. See comment-746563 for details.

      This disparency makes the property unusable for shortcut handling, as depending on the platform the value may or may not be the same for Shift+5 as it is for 5.

      Similarly, a game implementing WASD for movement can not trust that these keys will match when the player is simultaneously pressing a modifier to look around, shoot, crouch, etc.

      💡We should consider changing this behaviour to be uniform across platforms. Doing so will likely cause regressions for clients relying on the existing behaviour, so might only be an option for Qt 7. Alternatively, we could add new API to QKeyEvent that provides a well defined property to access the key without modifiers or keyboard map applied, similar to e.g. KeyboardEvent.code on the Web.

      key()

      This is documented to be a hardware and window system (platform) independent code of the key that was pressed.

      The Qt::Key enum provides list of possible values.

      It’s not documented, but if the incoming event does not match to one of these predefined values we return the unicode value of the key instead.

      It’s not documented, but the value is in many cases affected by the active keyboard layout (language) or modifiers keys, so it’s not strictly a representation of the underlying key that was pressed, but rather the resulting character.

      However, we do canonicalisation of the value, so it’s not a pure representation of the resulting character either:

      • We turn lower case values into their upper case representation
        • This behaviour is documented
        • Most Qt::Key enum keys have their values defined as the upper case representation
          • Except a few, which are defined as the lower case representation
            • Qt::Key_ydiaeresis
              • Transformation to and from lower cases is 1:1
                • 💡We should canonicalise this by lower-casing instead
            • Qt::Key_micro
              • Transformation to upper case results in the Greek capital letter Mu, which is not 1:1 with Micro
                • 💡We should not upper case, nor lower case this value
        • Canonicalisation is in most cases done via a plain QChar::toUpper()
          • The logic to do this canonicalisation is spread around and duplicated in the various platform backends
          • 💡We should share this logic in a single place, so we have a consistent case-canonicalisation
      • On macOS we use the key without modifiers applied in some cases
        • After this change when the control modifier is pressed
          • The use-case in the linked bug reports is that shortcuts are not triggered as expected
        • After this change when then option modifier is pressed
          • The rationale in the bug report being that while text insertion should use the modified key, shortcuts should not
        • ❗️In other words, QKeyEvent::key() was used for shortcut handling, and the expectation was a “stable” key, independent of modifiers
      • On X11 (and other X keyboard platforms) we explicitly use a latin1 keymap when the control modifier is pressed
        • This was introduced in this change if the modifier resulted in a character outside of latin1
        • And then extended recently to include latin1 characters as well, in this change
        • ❗️In other words, QKeyEvent::key() is used for shortcut handling, and the expectation is a “stable” key, independent of keyboard layouts
      • Windows
        • TODO: Investigate situation here

      Due to all the heuristics added for QKeyEvent::key() it’s sort-of like a canonicalised representation of the key, independent of the keyboard layout and modifiers, except when it’s not, so it can’t really be trusted either. The heuristics, added on here and there over time, also differ between platforms.😢 

      text()

      This represents the character(s) that were produced by pressing the key. It does take keyboard layout and modifiers into account, and we do not do any canonicalisation.

      Because we pass the characters through from the platform as is, the text may contain non-printable characters as well, such as end of text, or delete. This means the API is not directly usable for text insertion. And because there might be symbols outside of the printable range that still have visual representations (in a given font), we can’t filter the text on e.g. QChar::isPrint(). See this change and related bug reports for more information.

      🤔 Perhaps we should at least filter out non-printable text that’s not in the private use area?

      Key event matching

      At the most primitive level, the properties of an incoming key event can be compared against a predefined “shortcut”, and if there’s a match, the shortcut’s action is triggered.

      Typical ways to detect a shortcut are for example:

      if (keyEvent->modifiers() & Qt::ControlModifier && keyEvent->key() == Qt::Key_U)
          update(); 
      

      Or via QKeyEvent::matches(QKeySequence::StandardKey) / operator==(QKeySequence::StandardKey), which does a simple comparison of the key event, represented as a QKeySequence, against the default key binding for the given standard key.

      if (keyEvent.matches(QKeySequence::Copy))
          copy(); 
      

      QShortcut

      An alternative way of detecting shortcuts is via the QShortcut API. The application creates QShortcut instances, with a given QKeySequence and action callback, e.g.:

      new QShortcut(QKeySequence("Ctrl+C"), this, []{
          qDebug() << "Copy";
      }); 

      When a key combination is pressed, the platform layer forwards the event to QWindowSystemInterface::handleShortcutEvent(), which goes through two steps:

      1. Send a QEvent::ShortcutOverride event to the window, and check whether the event was accepted.
        • If the event is accepted it implies that the shortcut is overridden by some object in the event delivery path (typically the focus object).
        • If so, we do not look up the shortcut in the shortcut map (below), but instead deliver the event as a regular key event, so that the target that accepted the shortcut override event can handle it. 
        • Note that we only do this if the shortcut map hasn't found a partial shortcut match yet. If it has, the shortcut can not be overridden.
      2. If not overridden, we pass the event to the QGuiApplication's QShortcutMap::tryShortcut()
        • The shortcut map starts off by passing the event on to QKeyMapper::possibleKeys()
          • Each platform has a dedicated implementation of this function
          • The result of the function is a list of key combinations that can be expressed given the incoming key and modifiers
            • For example, pressing Alt (⌥) + Shift (⇧) + 5 on a US keyboard layout on macOS could mean:
              • Alt+Shift+5 (verbatim modifiers + base key)
              • Alt+%
              • Shift+∞
              • fi (no modifiers, only resulting key after pressing modifiers)
          • We want to match registered shortcuts that match any of these combinations
            • This is different from the naive/simple key matching for plain key events
        • We then try to match the possible key combinations against the registered shortcuts
          • We do so via QShortcutMap::matches, which is basically the same as QKeySequence::matches
            • Except that it special cases Key_hyphen as Key_Minus, "as people mix these up all the time and they are conceptually the same."
        • If there's a partial sequence match we keep looking for full matches for later key events, but skip sending QEvent::ShortcutOverride for those
          • As we already determined that we're prioritizing the QShortcut machinery
        • If there's a full match, the QShortcut's action is triggered

      Test cases

      Ctrl++ on US English keyboard layout

      The + symbol on a US English keyboard requires the Shift modifier to be pressed:

      A keyboard shortcut of Ctrl++ requires the user to then press Ctrl+Shift+=

      A plain QKeySequence match based on the key event will not match, while a QShortcut will, due to its possibleKeys() machinery.

      We also report a different key on macOS (Key_Equal) than what we do on X11 and Windows (Key_Plus).

      And while macOS and X11 matches the key and text, Windows has Key_Plus but text "=".

       

      Preliminary conclusions

      • The QKeyEvent::key() and QShortcut (former QAccel) APIs are from the Qt 1 era
        • Both are intended for and used for shortcut handling
        • QKeyEvent::key() for per widget local shortcuts
          • Text navigation e.g.
        • QShortcut for window or application wide (global) shortcuts
          • Toolbars, menus
        • Except QShortcut can have a widget-local context as well, so what's really the difference here?
      • QKeyEvent::key() has over time grown more and more canonicalized
        • Upper-casing ("a" and "A" are both Qt::Key_A)
        • Masking away other modifiers (Alt+U is Qt::Key_U, even if it produces "ü") 
        • Mapping using explicit latin1 layout rather than current layout
      • QShortcut, independently, has learned to be more lenient in its matching, via QKeyMapper::possibleKeys()
        • This is done in a "non-destructive" way, compared to the key event, which is "flattened"
      • It's too late to change QKeyEvent::key()
        • People rely on its canonicalisation in various ways
      • The shortcut matching between a key event and a QShortcut should be the same
        • QKeyMapper::possibleKeys() should always treat the canonicalized QKeyEvent key+modifiers as one of the possible combinations
        • QKeyEvent::maches() should learn a QKeySequence overload that goes via QKeyMapper::possibleKeys()
          • And the StandardKeys overload should go via this as well
      • The QKeyEvent::key() canonlicalisation should match between platforms as much as possible
        • E.g. the upper-casing should be shared in a QKeyMapper helper

      Attachments

        For Gerrit Dashboard: QTBUG-116873
        # Subject Branch Project Status CR V

        Activity

          People

            vestbo Tor Arne Vestbø
            vestbo Tor Arne Vestbø
            Votes:
            0 Vote for this issue
            Watchers:
            9 Start watching this issue

            Dates

              Created:
              Updated: