Tablet events behavior with stylus button click
-
Hello,
Problem ?
I am working on tablet input and I found out that there is an odd behavior with the tablet when using the stylus button (usually simulating a mouse button right click). Basically, some TabletRelease/MouseButtonRelease seems to be missing and the information in QEvent::button() and QEvent::buttons() are inconsistent.
I made a simple application tracking tablet/mouse events, keeping a list of pressed buttons. I place the stylus on the tablet and press the stylus button and below is info about one of the events I receive:
Event: 151972 event type: TabletPress watched class: QLabel device type: Stylus button: LeftButton buttons: RightButton m_keys: m_buttons: LeftButton, RightButtonThe problem here is that we have a TabletPress event with the button LeftButton but with buttons [RightButton], while in my private list m_buttons I have the accurate state of buttons pressed [LeftButton, RightButton].
Another problem is that later, in the same conditions as written above, I lift the stylus from the tablet: the event MouseButtonRelease/TabletRelease for the LeftButton will not be sent. The event's buttons will be correct ([RightButton]) while my private list m_buttons will not ([LeftButton, RightButton]) .
I tried to toggle the attribute Qt::AA_SynthesizeMouseForUnhandledTabletEvents but it does not change anything.
Is this the expected behavior or am I missing something ?
How to reproduce the problem
- Place the stylus on the tablet
- Press the stylus button (simulating a mouse right click button)
- Lift the stylus from the tablet (the event with inconsistent button/buttons is sent)
- Release the stylus button (there is no MouseButtonRelease/TabletRelease event sent here)
Simple test application code
I also added some fix in my code to be able to get the corresponding buttons state of the events in my private list.
mainwindow.h
#pragma once #include <QMainWindow> #include <QList> #include <QObject> #include <QEvent> #include <QSinglePointEvent> #include <QMetaEnum> #include <QTextEdit> #include <QPushButton> #include <QLabel> class TabletButtonInputDebugger : public QObject { Q_OBJECT public: TabletButtonInputDebugger(QObject* parent = nullptr); bool useFix() const; void setUseFix(bool useFix); void setDebugOutputTextEdit(QTextEdit* textEdit); void setDebugDownButtons(QLabel* label); protected: bool eventFilter(QObject* watched, QEvent* event) override; private: void updateInput( const QObject& watched, const QEvent& ev); void onKeyPress( const QObject& watched, const QKeyEvent& ev); void onKeyRelease( const QObject& watched, const QKeyEvent& ev); void onButtonPress( const QObject& watched, const QSinglePointEvent& ev); void onButtonRelease( const QObject& watched, const QSinglePointEvent& ev); void onButtonDoubleClick( const QObject& watched, const QSinglePointEvent& ev); void debugInput( const QObject& watched, const QInputEvent& event); void appendDebugLine(const QString& line); QList<Qt::Key> m_keys; QList<Qt::MouseButton> m_buttons; bool m_isStylusDown = false; bool m_isStylusDeviceUnstable = false; bool m_useFix = false; QTextEdit* m_debugOutputTextEdit = nullptr; QLabel* m_debugDownButtonsLabel = nullptr; }; class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = nullptr); private: TabletButtonInputDebugger m_tbid; };main_window.cpp
#include "mainwindow.h" #include <QSplitter> #include <QVBoxLayout> #include <QHBoxLayout> #include <QCheckBox> #include <QTimer> namespace { template <typename Enum> const char* enumValueToKey(Enum val) { const QMetaEnum metaEnum = QMetaEnum::fromType<Enum>(); return metaEnum.valueToKey(val); } QString toString(QInputDevice::DeviceType deviceType) { switch (deviceType) { default: case QInputDevice::DeviceType::Unknown: return "Unknown"; case QInputDevice::DeviceType::Mouse: return "Mouse"; case QInputDevice::DeviceType::TouchScreen: return "TouchScreen"; case QInputDevice::DeviceType::TouchPad: return "TouchPad"; case QInputDevice::DeviceType::Puck: return "Puck"; case QInputDevice::DeviceType::Stylus: return "Stylus"; case QInputDevice::DeviceType::Airbrush: return "Airbrush"; case QInputDevice::DeviceType::Keyboard: return "Keyboard"; } } QString toString(Qt::MouseButton button) { if (button == Qt::LeftButton) return "LeftButton"; if (button == Qt::RightButton) return "RightButton"; if (button == Qt::MiddleButton) return "MiddleButton"; if (button == Qt::BackButton) return "BackButton"; if (button == Qt::ForwardButton) return "ForwardButton"; if (button == Qt::TaskButton) return "TaskButton"; return "UnknownButton"; } QString toString(Qt::MouseButtons buttons) { QStringList names; if (buttons & Qt::LeftButton) names << "LeftButton"; if (buttons & Qt::RightButton) names << "RightButton"; if (buttons & Qt::MiddleButton) names << "MiddleButton"; if (buttons & Qt::BackButton) names << "BackButton"; if (buttons & Qt::ForwardButton) names << "ForwardButton"; if (buttons & Qt::TaskButton) names << "TaskButton"; if (names.isEmpty()) names << "NoButton"; return names.join("|"); } QString toString(Qt::Key key) { return QString(enumValueToKey(key)); } template <typename T> QString join(const QList<T>& list, const QString& sep) { QString res; for (const T& e : list) { res += toString(e) + sep; } res.chop(sep.size()); return res; } } TabletButtonInputDebugger::TabletButtonInputDebugger(QObject* parent) : QObject(parent) { } bool TabletButtonInputDebugger::useFix() const { return m_useFix; } void TabletButtonInputDebugger::setUseFix(bool useFix) { if (m_useFix == useFix) { return; } m_buttons.clear(); m_useFix = useFix; } void TabletButtonInputDebugger::setDebugOutputTextEdit(QTextEdit* textEdit) { m_debugOutputTextEdit = textEdit; } void TabletButtonInputDebugger::setDebugDownButtons(QLabel* label) { m_debugDownButtonsLabel = label; } bool TabletButtonInputDebugger::eventFilter(QObject* watched, QEvent* event) { updateInput(*watched, *event); return QObject::eventFilter(watched, event); } void TabletButtonInputDebugger::updateInput( const QObject& watched, const QEvent& ev) { // Ignoring non-input events. if (!ev.isInputEvent()) { return; } // Ignoring events on QWidgetWindow. if (!watched.isWidgetType()) { return; } switch (ev.type()) { case QEvent::KeyPress: onKeyPress(watched, static_cast<const QKeyEvent&>(ev)); break; case QEvent::KeyRelease: onKeyRelease(watched, static_cast<const QKeyEvent&>(ev)); break; case QEvent::MouseButtonPress: case QEvent::TabletPress: onButtonPress(watched, static_cast<const QSinglePointEvent&>(ev)); break; case QEvent::MouseButtonRelease: case QEvent::TabletRelease: onButtonRelease(watched, static_cast<const QSinglePointEvent&>(ev)); break; case QEvent::MouseButtonDblClick: onButtonDoubleClick(watched, static_cast<const QSinglePointEvent&>(ev)); break; case QEvent::MouseMove: case QEvent::TabletMove: break; default: // debugInput(watched, ev); break; } } void TabletButtonInputDebugger::onKeyPress( const QObject& watched, const QKeyEvent& ev) { if (ev.isAutoRepeat()) { return; } const Qt::Key key = static_cast<Qt::Key>(ev.key()); if (!m_keys.contains(key)) { m_keys.push_back(key); } } void TabletButtonInputDebugger::onKeyRelease( const QObject& watched, const QKeyEvent& ev) { if (ev.isAutoRepeat()) { return; } m_keys.removeAll(ev.key()); } void TabletButtonInputDebugger::onButtonPress( const QObject& watched, const QSinglePointEvent& ev) { if (m_useFix) { // If the stylus is down and we receive an event from another device, // we cannot track its down state anymore. It is unstable. if (m_isStylusDown && ev.deviceType() != QInputDevice::DeviceType::Stylus) { m_isStylusDown = false; m_isStylusDeviceUnstable = true; m_buttons.clear(); appendDebugLine(" -----------------"); appendDebugLine("| Stylus unstable |"); appendDebugLine(" -----------------"); appendDebugLine(""); } else { if (ev.deviceType() == QInputDevice::DeviceType::Stylus) { if (m_isStylusDeviceUnstable) { // Ignoring those events. return; } if (ev.button() == Qt::LeftButton) { m_isStylusDown = true; } } } } if (!m_buttons.contains(ev.button())) { m_buttons.push_back(ev.button()); } debugInput(watched, ev); } void TabletButtonInputDebugger::onButtonRelease( const QObject& watched, const QSinglePointEvent& ev) { if (m_useFix) { if (m_isStylusDeviceUnstable) { // If Qt tells that is that there are no buttons pressed, // the stylus is considered stable again. if (ev.buttons() == Qt::NoButton) { appendDebugLine(" ---------------------"); appendDebugLine("| Stylus stable again |"); appendDebugLine(" ---------------------"); appendDebugLine(""); m_isStylusDeviceUnstable = false; m_buttons.clear(); } } else { if (ev.deviceType() == QInputDevice::DeviceType::Stylus && ev.button() == Qt::LeftButton) { m_isStylusDown = false; } } } m_buttons.removeAll(ev.button()); debugInput(watched, ev); } void TabletButtonInputDebugger::onButtonDoubleClick( const QObject& watched, const QSinglePointEvent& ev) { debugInput(watched, ev); } void TabletButtonInputDebugger::debugInput( const QObject& watched, const QInputEvent& event) { const auto eventTypeStr = enumValueToKey(event.type()); const auto watchedClassName = watched.metaObject()->className(); const auto deviceTypeStr = toString(event.deviceType()); const auto m_keysStr = join(m_keys, ", "); const auto m_buttonsStr = join(m_buttons, ", "); appendDebugLine(QString() + "Event: " + QString::number(event.timestamp())); appendDebugLine(QString() + " event type: " + eventTypeStr); appendDebugLine(QString() + " watched class: " + watchedClassName); appendDebugLine(QString() + " device type: " + deviceTypeStr); if (event.type() == QEvent::KeyPress || event.type() == QEvent::KeyRelease) { const auto& ke = static_cast<const QKeyEvent&>(event); appendDebugLine(QString() + " key: " + toString(static_cast<Qt::Key>(ke.key()))); } if (event.isSinglePointEvent()) { const auto& spe = static_cast<const QSinglePointEvent&>(event); appendDebugLine(QString() + " button: " + toString(spe.button())); appendDebugLine(QString() + " buttons: " + toString(spe.buttons())); } appendDebugLine(QString() + " m_keys: " + m_keysStr); appendDebugLine(QString() + " m_buttons: " + m_buttonsStr); if (m_debugDownButtonsLabel) { m_debugDownButtonsLabel->setText(m_buttonsStr); } appendDebugLine(""); } void TabletButtonInputDebugger::appendDebugLine(const QString& line) { qDebug() << line; if (m_debugOutputTextEdit) { m_debugOutputTextEdit->append(line); } } MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { auto centralSplitterWidget = new QSplitter(this); setCentralWidget(centralSplitterWidget); centralSplitterWidget->setHandleWidth(20); centralSplitterWidget->setChildrenCollapsible(false); auto testAreaWidget = new QLabel(centralSplitterWidget); centralSplitterWidget->addWidget(testAreaWidget); testAreaWidget->setText("Test area"); testAreaWidget->setAlignment(Qt::AlignCenter); testAreaWidget->setMouseTracking(true); testAreaWidget->setTabletTracking(true); auto middlePaneWidget = new QWidget(centralSplitterWidget); centralSplitterWidget->addWidget(middlePaneWidget); auto middlePaneLayout = new QVBoxLayout(middlePaneWidget); auto outputTextEdit = new QTextEdit(middlePaneWidget); middlePaneLayout->addWidget(outputTextEdit, 1); outputTextEdit->setReadOnly(true); auto clearOutputTextEditButton = new QPushButton(middlePaneWidget); middlePaneLayout->addWidget(clearOutputTextEditButton); clearOutputTextEditButton->setText("Clear"); connect( clearOutputTextEditButton, &QPushButton::clicked, this, [outputTextEdit]() { outputTextEdit->clear(); }); auto rightPaneWidget = new QWidget(centralSplitterWidget); centralSplitterWidget->addWidget(rightPaneWidget); auto rightPaneLayout = new QVBoxLayout(rightPaneWidget); rightPaneLayout->setAlignment(Qt::AlignTop); rightPaneLayout->addSpacerItem(new QSpacerItem(-1, -1)); auto downButtonsLayout = new QHBoxLayout(); rightPaneLayout->addLayout(downButtonsLayout); downButtonsLayout->addWidget(new QLabel("Buttons down: ", rightPaneWidget)); auto downButtonsLabel = new QLabel(rightPaneWidget); downButtonsLayout->addWidget(downButtonsLabel, 1); auto toggleFixCheckBox = new QCheckBox(rightPaneWidget); rightPaneLayout->addWidget(toggleFixCheckBox); toggleFixCheckBox->setText("Use fix"); toggleFixCheckBox->setChecked(m_tbid.useFix()); connect( toggleFixCheckBox, &QCheckBox::toggled, this, [this](bool state) { m_tbid.setUseFix(state); }); auto synthesizeMouseForUnhandledTabletEventsButton = new QCheckBox(rightPaneWidget); rightPaneLayout->addWidget(synthesizeMouseForUnhandledTabletEventsButton); synthesizeMouseForUnhandledTabletEventsButton->setText("AA_SynthesizeMouseForUnhandledTabletEventsButton"); synthesizeMouseForUnhandledTabletEventsButton->setChecked(qApp->testAttribute(Qt::AA_SynthesizeMouseForUnhandledTabletEvents)); connect( synthesizeMouseForUnhandledTabletEventsButton, &QCheckBox::toggled, this, [](bool state) { qApp->setAttribute(Qt::AA_SynthesizeMouseForUnhandledTabletEvents, state); }); // - Tablet debugger - testAreaWidget->installEventFilter(&m_tbid); // Also tested with the application, same problem. // qApp->installEventFilter(&m_tbid); m_tbid.setDebugOutputTextEdit(outputTextEdit); m_tbid.setDebugDownButtons(downButtonsLabel); }main.cpp
#include "mainwindow.h" #include <QApplication> int main(int argc, char *argv[]) { QApplication app(argc, argv); MainWindow w; w.show(); return app.exec(); }Info
OS: Windows 11
Qt: 6.9.1 (I also tested with Qt 6.7.2.) -
I cannot edit my previous message (considered being a spam). I add corrections and additional info below:
How to reproduce the problem
- Place the stylus on the tablet
- Press the stylus button (simulating a mouse right click button) (the event with inconsistent button/buttons is sent)
- Lift the stylus from the tablet (there is no MouseButtonRelease/TabletRelease event sent here)
- Release the stylus button
Info
OS: Windows 11
Qt: 6.9.1 (I also tested with Qt 6.7.2.)
Tablet: Gaomon M6 -
I filed it as a bug here.