Qt QShortcut Ambiguity

 · 4 min read
 · Zijian Jiang

I hope this answer still has value for anyone who comes across this shortcut context problem, and can save you couple hours of struggling with Qt QShortcut.

Long story in short: Qt think shortcuts with same key sequence(e.g. CTRL+F) defined on both parent and child widgets is the case of ambiguity, and signals activatedAmbiguously() will be emitted in one by one in bubble order(the order of top to bottom, first QMainWindow then QTextEdit then QMainWindow... to infinite loop).

From Qt offical document: When the user types the key sequence for a given shortcut, the shortcut’s activated() signal is emitted. (In the case of ambiguity, the activatedAmbiguously() signal is emitted.) A shortcut is “listened for” by Qt’s event loop when the shortcut’s parent widget is receiving events.

As a result, if you have QShortcut objects with same QKeySeqence CTRL+F on QMainWindow and QTextEdit(center widget of QMainWindow), no matter you focus(mouse click in) on QTextEdit or QMainWindow. When you press CTRL+F. None of shortcut will emit signal activated, but signal activatedAmbiguously will be emitted in the order of ['QMainWindow.QShortcut.activatedAmbiguously', 'QTextEdit.QShortcut.activatedAmbiguously', 'QMainWindow.QShortcut.activatedAmbiguously' ...].

This behavior is checked by me after several exhausting hours of testing QShortcut in different senario again and again. You can refer to the unittest(requires pytest, pytest-qt and PySide6) code I post below:

from unittest.mock import MagicMock
import pytest

def test_shortcut_override(qtbot):
    """
    In this test we have built a simple UI has an QTextEdit in an QMainWindow,
    Both QTextEdit and QMainWindow has same shortcut 'Ctrl+F'. The shortcuts
    emit `activatedAmbiguously()` signal in bubble order.
    """
    from PySide6 import QtWidgets, QtCore, QtGui
    import random

    mainWindow = QtWidgets.QMainWindow()
    qtbot.add_widget(mainWindow)

    # build UI hierarchy
    textEdit = QtWidgets.QTextEdit()
    mainWindow.setCentralWidget(textEdit)

    # window must be shown, otherwise the shortcut will never work since it
    # needs window to receive the shortcut event
    mainWindow.show()

    mainWindowSCAmbigouslyTrigger = MagicMock(returns=None)
    textEditSCAmbigouslyTrigger = MagicMock(returns=None)

    mainWindowMockSC = MagicMock(returns=None)
    textEditMockSC = MagicMock(returns=None)

    sc = QtGui.QShortcut('Ctrl+F', mainWindow, mainWindowMockSC, QtGui.Qt.ShortcutContext.WindowShortcut)
    sc.activatedAmbiguously.connect(mainWindowSCAmbigouslyTrigger)

    sc = QtGui.QShortcut('Ctrl+F', textEdit, textEditMockSC, QtGui.Qt.ShortcutContext.WindowShortcut)
    sc.activatedAmbiguously.connect(textEditSCAmbigouslyTrigger)

    mainWindowSCAmbiguouslyCount = 0
    textEditSCAmbiguouslyCount = 0

    for i in range(20):
        # no matter which widget is focus, it doesn't change the bubble order.
        widget = random.choice([textEdit, mainWindow])
        widget.setFocus()
        qtbot.keyClicks(widget, 'F', QtCore.Qt.KeyboardModifier.ControlModifier, delay=1)

        # Result 1) neither shortcut's signal `activated` not emitted, so these two not called.
        mainWindowMockSC.assert_not_called()
        textEditMockSC.assert_not_called()

        # Result 2) shortcuts are ambiguously activated in bubble order.
        if i % 2 == 0:
            mainWindowSCAmbiguouslyCount += 1
        else:
            textEditSCAmbiguouslyCount += 1

        assert mainWindowSCAmbigouslyTrigger.call_count == mainWindowSCAmbiguouslyCount
        assert textEditSCAmbigouslyTrigger.call_count == textEditSCAmbiguouslyCount

        print(f"'CTRL+F' has been triggered: {i + 1} times.")
        print(f"QMainWindow's shortcut ambiguously triggered: {mainWindowSCAmbigouslyTrigger.call_count} times.")
        print(f"QTextEdit's shortcut ambiguously triggered: {textEditSCAmbigouslyTrigger.call_count} times.")

I'm sure this "ambiguous" shortcut behavior is same in Qt/C++.

"Is there a way for a focussed widget to steal and consume a shortcut, no matter if there is another shortcut with context WindowShortcut or ApplicationShortcut?"

Saddly, the answer is NO, but since you know that shortcuts with same key sequence will emit activatedAmbiguously signal in bubble order. You can exploit this behavior by binding all activatedAmbiguously signal to one SLOT function, and in that function you can use QApplication::focusWidget() to get focused widget then trigger the shortcut action by yourself now.

I won't post the implementation here because since you've read this far, you should fully understand QShortcut ambiguity and how signals are emitted in bubble order now, go and try it yourself :)