Skycoder42 / QHotkey

A global shortcut/hotkey for Desktop Qt-Applications
BSD 3-Clause "New" or "Revised" License
548 stars 161 forks source link

Bug: the `released` signal will not be properly emitted when pressing multiple hotkeys at the same time on windows #85

Closed FredBill1 closed 1 year ago

FredBill1 commented 1 year ago

Steps to reproduce

I'm using Qt6.4.3 on windows 11. Here is a minimum demo code for reproducing the bug:

#include <QApplication>
#include <QDebug>
#include <QHotkey>
int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    QHotkey key_a(QKeySequence(Qt::Key_A), true);
    QHotkey key_b(QKeySequence(Qt::Key_B), true);
    QObject::connect(&key_a, &QHotkey::activated, [&]() { qDebug() << "A pressed"; });
    QObject::connect(&key_b, &QHotkey::activated, [&]() { qDebug() << "B pressed"; });
    QObject::connect(&key_a, &QHotkey::released, [&]() { qDebug() << "A released"; });
    QObject::connect(&key_b, &QHotkey::released, [&]() { qDebug() << "B released"; });
    return app.exec();
}

Run the code in a debugger, and press the keys as the following sequence: press A-> press B -> release A -> release B.

The expected console output would be:

A pressed
B pressed
A released
B released

But the actual output is:

A pressed
B pressed
B released

Where A released is missing. Further more, when pressing multiple hotkeys at the same time, only the last hotkey would emit the released signal.

Cause of the bug

The QHotkeyPrivateWin class only polls the release of the last hotkey being pressed, and the previous hotkeys are replaced by the new pressed hotkey. The link to the related code is here.

bool QHotkeyPrivateWin::nativeEventFilter(const QByteArray &eventType, void *message, _NATIVE_EVENT_RESULT *result)
{
    Q_UNUSED(eventType)
    Q_UNUSED(result)

    MSG* msg = static_cast<MSG*>(message);
    if(msg->message == WM_HOTKEY) {
        QHotkey::NativeShortcut shortcut = {HIWORD(msg->lParam), LOWORD(msg->lParam)};
        this->activateShortcut(shortcut);
        this->polledShortcut = shortcut;  //! previous pressed hotkeys being replaced
        this->pollTimer.start();
    }

    return false;
}

void QHotkeyPrivateWin::pollForHotkeyRelease()
{
    //! only polls the last pressed hotkey
    bool pressed = (GetAsyncKeyState(this->polledShortcut.key) & (1 << 15)) != 0;
    if(!pressed) {
        this->pollTimer.stop();
        this->releaseShortcut(this->polledShortcut);
    }
}

Possible solution

Use a container like QList<QHotkey::NativeShortcut> to maintain all the keys being pressed but not yet released, and use std::remove_if to emit the released signal and remove the corresponding NativeShortcut from the container.