qtqt5qwidget

How to make custom QWidget receive all key press events, even shortcuts of global actions?


In Qt 5, assume the following custom widget:

class QMeow final :
    public QWidget
{
public:
    explicit QMeow()
    {
        this->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
        this->setFocusPolicy(Qt::StrongFocus);
    }

protected:
    void keyPressEvent(QKeyEvent * const event) override
    {
        _s = !_s;
        this->update();
        QWidget::keyPressEvent(event);
    }

    void paintEvent(QPaintEvent *) override
    {
        QPainter painter {this};

        painter.fillRect(this->rect(), QColor {_s ? "red" : "blue"});
    }

private:
    bool _s = false;
};

An instance of this widget changes its background color between red and blue when any key is pressed.

Now, let's make a window and add a QLineEdit as well as a QMeow, also setting an action with the shortcut key A:

QApplication app {argc, argv};
QWidget window;
QVBoxLayout layout {&window};

layout.addWidget(new QLineEdit);
layout.addWidget(new QMeow);

QAction action {"hello"};

action.setShortcut(Qt::Key_A);
window.addAction(&action);

window.show();
app.exec();

If you try this, you'll notice that:

How can QMeow::keyPressEvent() get called for any key, including action shortcuts, like QLineEdit?

I tried QWidget::grabKeyboard(), different focus policies, and looking at qlineedit.cpp without success.


Solution

  • Text input widgets in Qt normally use an internal "text control" which is used as a "proxy" for any text-related aspect, including keyboard input.

    A focused widget always receives a special ShortcutOverride event (a QKeyEvent) whenever it may trigger a shortcut valid for the current context.

    See the relevant documentation about QEvent::ShortcutOverride:

    Key press in child, for overriding shortcut key handling (QKeyEvent). When a shortcut is about to trigger, ShortcutOverride is sent to the active window. This allows clients (e.g. widgets) to signal that they will handle the shortcut themselves, by accepting the event. If the shortcut override is accepted, the event is delivered as a normal key press to the focus widget. Otherwise, it triggers the shortcut action, if one exists.

    Similarly, the related section of the QKeyEvent docs:

    For QEvent::ShortcutOverride the receiver needs to explicitly accept the event to trigger the override. Calling ignore() on a key event will propagate it to the parent widget. The event is propagated up the parent widget chain until a widget accepts it or an event filter consumes it.

    This is exactly what text input widgets do: they override event() (QLineEdit::event()) and send ShortcutOverride events to the internal text control object (QWidgetLineControl::processShortcutOverrideEvent()), which contains the following:

    ...
    } else if (ke->modifiers() == Qt::NoModifier || ke->modifiers() == Qt::ShiftModifier
               || ke->modifiers() == Qt::KeypadModifier) {
        if (ke->key() < Qt::Key_Escape) {
            if (!isReadOnly())
                ke->accept();
    ...
    

    Since standard alphanumeric keys are below Qt::Key_Escape (0x01000000, and the Qt::Key enum mostly uses the standard ASCII numeration for them, with upper case for letters) this means that shortcuts using such keys are always accepted if they don't have modifiers, use Shift or are pressed within the numeric pad. The result is that they accept the shortcut, preventing propagation to parents, which will then be "converted" to a standard KeyPress and eventually sent to the keyPressEvent() handler.

    The solution is then to override event(), check for ShortcutOverride events, and accept it (assuming you do want to accept such key press).