// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN # include # include # include # include # include # include #endif static bool optIgnoreTouch = false; static QList optGestures; static QWidgetList mainWindows; static inline void drawEllipse(const QPointF ¢er, qreal hDiameter, qreal vDiameter, const QColor &color, QPainter &painter) { const QPen oldPen = painter.pen(); QPen pen = oldPen; pen.setColor(color); painter.setPen(pen); painter.drawEllipse(center, hDiameter / 2, vDiameter / 2); painter.setPen(oldPen); } static inline void fillEllipse(const QPointF ¢er, qreal hDiameter, qreal vDiameter, const QColor &color, QPainter &painter) { QPainterPath painterPath; painterPath.addEllipse(center, hDiameter / 2, vDiameter / 2); painter.fillPath(painterPath, color); } // Draws an arrow assuming a mathematical coordinate system, Y axis pointing // upwards, angle counterclockwise (that is, 45' is pointing up/right). static void drawArrow(const QPointF ¢er, qreal length, qreal angleDegrees, const QColor &color, int arrowSize, QPainter &painter) { painter.save(); painter.translate(center); // Transform center to (0,0) rotate and draw arrow pointing right. painter.rotate(-angleDegrees); QPen pen = painter.pen(); pen.setColor(color); pen.setWidth(2); painter.setPen(pen); const QPointF endPoint(length, 0); painter.drawLine(QPointF(0, 0), endPoint); painter.drawLine(endPoint, endPoint + QPoint(-arrowSize, -arrowSize)); painter.drawLine(endPoint, endPoint + QPoint(-arrowSize, arrowSize)); painter.restore(); } // Hierarchy of classes containing gesture parameters and drawing functionality. class Gesture { Q_DISABLE_COPY(Gesture) public: static Gesture *fromQGesture(const QWidget *w, const QGesture *source); virtual ~Gesture() {} virtual void draw(const QRectF &rect, QPainter &painter) const = 0; protected: explicit Gesture(const QWidget *w, const QGesture *source) : m_type(source->gestureType()) , m_hotSpot(w->mapFromGlobal(source->hotSpot().toPoint())) , m_hasHotSpot(source->hasHotSpot()) {} QPointF drawHotSpot(const QRectF &rect, QPainter &painter) const { const QPointF h = m_hasHotSpot ? m_hotSpot : rect.center(); painter.drawEllipse(h, 15, 15); return h; } private: Qt::GestureType m_type; QPointF m_hotSpot; bool m_hasHotSpot; }; class PanGesture : public Gesture { public: explicit PanGesture(const QWidget *w, const QPanGesture *source) : Gesture(w, source) , m_offset(source->offset()) {} void draw(const QRectF &rect, QPainter &painter) const override { const QPointF hotSpot = drawHotSpot(rect, painter); painter.drawLine(hotSpot, hotSpot + m_offset); } private: QPointF m_offset; }; class SwipeGesture : public Gesture { public: explicit SwipeGesture(const QWidget *w, const QSwipeGesture *source) : Gesture(w, source) , m_horizontal(source->horizontalDirection()), m_vertical(source->verticalDirection()) , m_angle(source->swipeAngle()) {} void draw(const QRectF &rect, QPainter &painter) const override; private: QSwipeGesture::SwipeDirection m_horizontal; QSwipeGesture::SwipeDirection m_vertical; qreal m_angle; }; static qreal swipeDirectionAngle(QSwipeGesture::SwipeDirection d) { switch (d) { case QSwipeGesture::NoDirection: case QSwipeGesture::Right: break; case QSwipeGesture::Left: return 180; case QSwipeGesture::Up: return 90; case QSwipeGesture::Down: return 270; } return 0; } void SwipeGesture::draw(const QRectF &rect, QPainter &painter) const { enum { arrowLength = 50, arrowHeadSize = 10 }; const QPointF hotSpot = drawHotSpot(rect, painter); drawArrow(hotSpot, arrowLength, swipeDirectionAngle(m_horizontal), Qt::red, arrowHeadSize, painter); drawArrow(hotSpot, arrowLength, swipeDirectionAngle(m_vertical), Qt::green, arrowHeadSize, painter); drawArrow(hotSpot, arrowLength, m_angle, Qt::blue, arrowHeadSize, painter); } Gesture *Gesture::fromQGesture(const QWidget *w, const QGesture *source) { Gesture *result = nullptr; switch (source->gestureType()) { case Qt::TapGesture: case Qt::TapAndHoldGesture: case Qt::PanGesture: result = new PanGesture(w, static_cast(source)); break; case Qt::PinchGesture: case Qt::CustomGesture: case Qt::LastGestureType: break; case Qt::SwipeGesture: result = new SwipeGesture(w, static_cast(source)); break; } return result; } typedef QSharedPointer GesturePtr; typedef QList GesturePtrs; typedef QList EventTypeVector; static EventTypeVector eventTypes; class EventFilter : public QObject { Q_OBJECT public: explicit EventFilter(const EventTypeVector &types, QObject *p) : QObject(p), m_types(types) {} bool eventFilter(QObject *, QEvent *) override; signals: void eventReceived(const QString &); private: const EventTypeVector m_types; }; static EventFilter *globalEventFilter = nullptr; bool EventFilter::eventFilter(QObject *o, QEvent *e) { static int n = 0; if (m_types.contains(e->type())) { QString message; QDebug debug(&message); debug << '#' << n++ << ' ' << o->objectName() << ' '; switch (e->type()) { case QEvent::Gesture: case QEvent::GestureOverride: debug << static_cast(e); // Special operator break; default: debug << e; break; } emit eventReceived(message); } return false; } enum PointType { TouchPoint, MousePress, MouseRelease }; struct Point { Point(const QPointF &p = QPoint(), PointType t = TouchPoint, Qt::MouseEventSource s = Qt::MouseEventNotSynthesized, QSizeF diameters = QSizeF(4, 4)) : pos(p), horizontalDiameter(qMax(2., diameters.width())), verticalDiameter(qMax(2., diameters.height())), type(t), source(s) {} QColor color() const; QPointF pos; qreal horizontalDiameter; qreal verticalDiameter; PointType type; Qt::MouseEventSource source; }; QColor Point::color() const { Qt::GlobalColor globalColor = Qt::black; if (type != TouchPoint) { switch (source) { case Qt::MouseEventSynthesizedBySystem: globalColor = Qt::red; break; case Qt::MouseEventSynthesizedByQt: globalColor = Qt::blue; break; case Qt::MouseEventSynthesizedByApplication: globalColor = Qt::green; break; case Qt::MouseEventNotSynthesized: break; } } const QColor result(globalColor); return type == MousePress ? result.lighter() : result; } class TouchTestWidget : public QWidget { Q_OBJECT Q_PROPERTY(bool drawPoints READ drawPoints WRITE setDrawPoints) public: explicit TouchTestWidget(QWidget *parent = nullptr) : QWidget(parent), m_drawPoints(true) { setAttribute(Qt::WA_AcceptTouchEvents); for (Qt::GestureType t : optGestures) grabGesture(t); } bool drawPoints() const { return m_drawPoints; } public slots: void clearPoints(); void setDrawPoints(bool drawPoints); signals: void logMessage(const QString &); protected: bool event(QEvent *event) override; void paintEvent(QPaintEvent *) override; private: void handleGestureEvent(QGestureEvent *gestureEvent); QList m_points; GesturePtrs m_gestures; bool m_drawPoints; }; void TouchTestWidget::clearPoints() { if (!m_points.isEmpty() || !m_gestures.isEmpty()) { m_points.clear(); m_gestures.clear(); update(); } } void TouchTestWidget::setDrawPoints(bool drawPoints) { if (m_drawPoints != drawPoints) { clearPoints(); m_drawPoints = drawPoints; } } bool TouchTestWidget::event(QEvent *event) { const QEvent::Type type = event->type(); switch (type) { case QEvent::MouseButtonPress: case QEvent::MouseButtonRelease: if (m_drawPoints) { const QMouseEvent *me = static_cast(event); m_points.append(Point(me->position(), type == QEvent::MouseButtonPress ? MousePress : MouseRelease, me->source())); update(); } break; case QEvent::TouchBegin: case QEvent::TouchUpdate: if (m_drawPoints) { for (const QEventPoint &p : static_cast(event)->points()) m_points.append(Point(p.position(), TouchPoint, Qt::MouseEventNotSynthesized, p.ellipseDiameters())); update(); } Q_FALLTHROUGH(); case QEvent::TouchEnd: if (optIgnoreTouch) event->ignore(); else event->accept(); return true; case QEvent::Gesture: handleGestureEvent(static_cast(event)); break; default: break; } return QWidget::event(event); } void TouchTestWidget::handleGestureEvent(QGestureEvent *gestureEvent) { const auto gestures = gestureEvent->gestures(); for (QGesture *gesture : gestures) { if (optGestures.contains(gesture->gestureType())) { switch (gesture->state()) { case Qt::NoGesture: break; case Qt::GestureStarted: case Qt::GestureUpdated: gestureEvent->accept(gesture); break; case Qt::GestureFinished: gestureEvent->accept(gesture); if (Gesture *g = Gesture::fromQGesture(this, gesture)) { m_gestures.append(GesturePtr(g)); update(); } break; case Qt::GestureCanceled: emit logMessage(QLatin1String("=== Qt::GestureCanceled ===")); break; } } } } void TouchTestWidget::paintEvent(QPaintEvent *) { // Draw touch points as dots, mouse press as light filled circles, mouse release as circles. QPainter painter(this); const QRectF geom = QRectF(QPointF(0, 0), QSizeF(size())); painter.fillRect(geom, Qt::white); painter.drawRect(QRectF(geom.topLeft(), geom.bottomRight() - QPointF(1, 1))); for (const Point &point : std::as_const(m_points)) { if (geom.contains(point.pos)) { if (point.type == MouseRelease) drawEllipse(point.pos, point.horizontalDiameter, point.verticalDiameter, point.color(), painter); else fillEllipse(point.pos, point.horizontalDiameter, point.verticalDiameter, point.color(), painter); } } for (const GesturePtr &gp : std::as_const(m_gestures)) gp->draw(geom, painter); } #ifdef Q_OS_WIN class SettingsDialog : public QDialog { Q_OBJECT public: explicit SettingsDialog(QWidget *parent); private slots: void touchTypeToggled(); private: using QWindowsApplication = QNativeInterface::Private::QWindowsApplication; using TouchWindowTouchType = QWindowsApplication::TouchWindowTouchType; using TouchWindowTouchTypes = QWindowsApplication::QWindowsApplication::TouchWindowTouchTypes; QCheckBox *m_fineCheckBox; QCheckBox *m_palmCheckBox; }; SettingsDialog::SettingsDialog(QWidget *parent) : QDialog(parent) { setWindowTitle("Settings"); auto layout = new QVBoxLayout(this); TouchWindowTouchTypes touchTypes; if (auto nativeWindowsApp = qGuiApp->nativeInterface()) touchTypes = nativeWindowsApp->touchWindowTouchType(); m_fineCheckBox = new QCheckBox("Fine Touch", this); m_fineCheckBox->setChecked(touchTypes.testFlag(TouchWindowTouchType::FineTouch)); layout->addWidget(m_fineCheckBox); connect(m_fineCheckBox, &QAbstractButton::toggled, this, &SettingsDialog::touchTypeToggled); m_palmCheckBox = new QCheckBox("Palm Touch", this); connect(m_palmCheckBox, &QAbstractButton::toggled, this, &SettingsDialog::touchTypeToggled); m_palmCheckBox->setChecked(touchTypes.testFlag(TouchWindowTouchType::WantPalmTouch)); layout->addWidget(m_palmCheckBox); auto box = new QDialogButtonBox(QDialogButtonBox::Close); connect(box, &QDialogButtonBox::rejected, this, &QDialog::reject); layout->addWidget(box); } void SettingsDialog::touchTypeToggled() { TouchWindowTouchTypes types; if (m_fineCheckBox->isChecked()) types.setFlag(TouchWindowTouchType::FineTouch); if (m_palmCheckBox->isChecked()) types.setFlag(TouchWindowTouchType::WantPalmTouch); if (auto nativeWindowsApp = qGuiApp->nativeInterface()) nativeWindowsApp->setTouchWindowTouchType(types); else qWarning("Missing Interface QWindowsApplication"); } #endif // Q_OS_WIN class MainWindow : public QMainWindow { Q_OBJECT MainWindow(); public: static MainWindow *createMainWindow(); QWidget *touchWidget() const { return m_touchWidget; } void setVisible(bool visible) override; public slots: void appendToLog(const QString &text) { m_logTextEdit->appendPlainText(text); } void dumpTouchDevices(); private slots: void settingsDialog(); private: void updateScreenLabel(); void newWindow() { MainWindow::createMainWindow(); } TouchTestWidget *m_touchWidget; QPlainTextEdit *m_logTextEdit; QLabel *m_screenLabel; }; MainWindow *MainWindow::createMainWindow() { MainWindow *result = new MainWindow; const QSize screenSize = QGuiApplication::primaryScreen()->availableGeometry().size(); result->resize(screenSize / 2); const QSize sizeDiff = screenSize - result->size(); const QPoint pos = QPoint(sizeDiff.width() / 2, sizeDiff.height() / 2); result->move(pos); result->show(); EventFilter *eventFilter = globalEventFilter; if (!eventFilter) { eventFilter = new EventFilter(eventTypes, result->touchWidget()); result->touchWidget()->installEventFilter(eventFilter); } QObject::connect(eventFilter, &EventFilter::eventReceived, result, &MainWindow::appendToLog); mainWindows.append(result); return result; } MainWindow::MainWindow() : m_touchWidget(new TouchTestWidget) , m_logTextEdit(new QPlainTextEdit) , m_screenLabel(new QLabel) { QString title; QTextStream(&title) << "Touch Event Tester " << QT_VERSION_STR << ' ' << qApp->platformName() << " #" << (mainWindows.size() + 1); setWindowTitle(title); setObjectName("MainWin"); QToolBar *toolBar = new QToolBar(this); addToolBar(Qt::TopToolBarArea, toolBar); QMenu *fileMenu = menuBar()->addMenu("File"); QAction *newWindowAction = fileMenu->addAction(QStringLiteral("New Window"), this, &MainWindow::newWindow); newWindowAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_N)); toolBar->addAction(newWindowAction); fileMenu->addSeparator(); QAction *dumpDeviceAction = fileMenu->addAction(QStringLiteral("Dump devices")); dumpDeviceAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_D)); connect(dumpDeviceAction, &QAction::triggered, this, &MainWindow::dumpTouchDevices); toolBar->addAction(dumpDeviceAction); QAction *clearLogAction = fileMenu->addAction(QStringLiteral("Clear Log")); clearLogAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_L)); connect(clearLogAction, &QAction::triggered, m_logTextEdit, &QPlainTextEdit::clear); toolBar->addAction(clearLogAction); QAction *toggleDrawPointAction = fileMenu->addAction(QStringLiteral("Draw Points")); toggleDrawPointAction->setCheckable(true); toggleDrawPointAction->setChecked(m_touchWidget->drawPoints()); connect(toggleDrawPointAction, &QAction::toggled, m_touchWidget, &TouchTestWidget::setDrawPoints); toolBar->addAction(toggleDrawPointAction); QAction *clearPointAction = fileMenu->addAction(QStringLiteral("Clear Points")); clearPointAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_P)); connect(clearPointAction, &QAction::triggered, m_touchWidget, &TouchTestWidget::clearPoints); toolBar->addAction(clearPointAction); QAction *quitAction = fileMenu->addAction(QStringLiteral("Quit")); quitAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_Q)); connect(quitAction, &QAction::triggered, qApp, &QCoreApplication::quit); toolBar->addAction(quitAction); auto settingsMenu = menuBar()->addMenu("Settings"); auto settingsAction = settingsMenu->addAction("Settings", this, &MainWindow::settingsDialog); #ifdef Q_OS_WIN Q_UNUSED(settingsAction); #else settingsAction->setEnabled(false); #endif QSplitter *mainSplitter = new QSplitter(Qt::Vertical, this); m_touchWidget->setObjectName(QStringLiteral("TouchWidget")); mainSplitter->addWidget(m_touchWidget); connect(m_touchWidget, &TouchTestWidget::logMessage, this, &MainWindow::appendToLog); m_logTextEdit->setObjectName(QStringLiteral("LogTextEdit")); mainSplitter->addWidget(m_logTextEdit); setCentralWidget(mainSplitter); statusBar()->addPermanentWidget(m_screenLabel); dumpTouchDevices(); } void MainWindow::settingsDialog() { #ifdef Q_OS_WIN SettingsDialog dialog(this); dialog.exec(); #endif } void MainWindow::setVisible(bool visible) { QMainWindow::setVisible(visible); connect(windowHandle(), &QWindow::screenChanged, this, &MainWindow::updateScreenLabel); updateScreenLabel(); } void MainWindow::updateScreenLabel() { QString text; QTextStream str(&text); const QScreen *screen = windowHandle()->screen(); const QRect geometry = screen->geometry(); const qreal dpr = screen->devicePixelRatio(); str << '"' << screen->name() << "\" " << geometry.width() << 'x' << geometry.height() << Qt::forcesign << geometry.x() << geometry.y() << Qt::noforcesign; if (!qFuzzyCompare(dpr, qreal(1))) str << ", dpr=" << dpr; m_screenLabel->setText(text); } void MainWindow::dumpTouchDevices() { QString message; QDebug debug(&message); const auto devices = QPointingDevice::devices(); debug << devices.size() << "Device(s):\n"; for (int i = 0; i < devices.size(); ++i) debug << "Device #" << i << devices.at(i) << '\n'; appendToLog(message); } int main(int argc, char *argv[]) { QApplication a(argc, argv); QCommandLineParser parser; parser.setApplicationDescription(QStringLiteral("Touch/Mouse tester")); parser.addHelpOption(); const QCommandLineOption mouseMoveOption(QStringLiteral("mousemove"), QStringLiteral("Log mouse move events")); parser.addOption(mouseMoveOption); const QCommandLineOption globalFilterOption(QStringLiteral("global"), QStringLiteral("Global event filter")); parser.addOption(globalFilterOption); const QCommandLineOption ignoreTouchOption(QStringLiteral("ignore"), QStringLiteral("Ignore touch events (for testing mouse emulation).")); parser.addOption(ignoreTouchOption); const QCommandLineOption noTouchLogOption(QStringLiteral("notouchlog"), QStringLiteral("Do not log touch events (for testing gestures).")); parser.addOption(noTouchLogOption); const QCommandLineOption noMouseLogOption(QStringLiteral("nomouselog"), QStringLiteral("Do not log mouse events (for testing gestures).")); parser.addOption(noMouseLogOption); const QCommandLineOption tapGestureOption(QStringLiteral("tap"), QStringLiteral("Grab tap gesture.")); parser.addOption(tapGestureOption); const QCommandLineOption tapAndHoldGestureOption(QStringLiteral("tap-and-hold"), QStringLiteral("Grab tap-and-hold gesture.")); parser.addOption(tapAndHoldGestureOption); const QCommandLineOption panGestureOption(QStringLiteral("pan"), QStringLiteral("Grab pan gesture.")); parser.addOption(panGestureOption); const QCommandLineOption pinchGestureOption(QStringLiteral("pinch"), QStringLiteral("Grab pinch gesture.")); parser.addOption(pinchGestureOption); const QCommandLineOption swipeGestureOption(QStringLiteral("swipe"), QStringLiteral("Grab swipe gesture.")); parser.addOption(swipeGestureOption); parser.process(QApplication::arguments()); optIgnoreTouch = parser.isSet(ignoreTouchOption); if (parser.isSet(tapGestureOption)) optGestures.append(Qt::TapGesture); if (parser.isSet(tapAndHoldGestureOption)) optGestures.append(Qt::TapAndHoldGesture); if (parser.isSet(panGestureOption)) optGestures.append(Qt::PanGesture); if (parser.isSet(pinchGestureOption)) optGestures.append(Qt::PinchGesture); if (parser.isSet(swipeGestureOption)) optGestures.append(Qt::SwipeGesture); if (!parser.isSet(noMouseLogOption)) eventTypes << QEvent::MouseButtonPress << QEvent::MouseButtonRelease << QEvent::MouseButtonDblClick; if (parser.isSet(mouseMoveOption)) eventTypes << QEvent::MouseMove; if (!parser.isSet(noTouchLogOption)) eventTypes << QEvent::TouchBegin << QEvent::TouchUpdate << QEvent::TouchEnd; if (!optGestures.isEmpty()) eventTypes << QEvent::Gesture << QEvent::GestureOverride; if (parser.isSet(globalFilterOption)) { globalEventFilter = new EventFilter(eventTypes, &a); a.installEventFilter(globalEventFilter); } MainWindow::createMainWindow(); const int exitCode = a.exec(); qDeleteAll(mainWindows); return exitCode; } #include "main.moc"