on xenial help sign in
TouchDispatcher: synthesize MouseButtonDblClick events
645165eDaniel d'Andrada
libs/UbuntuGestures/DebugHelpers.cpp+1−1
@@ -83,7 +83,7 @@ QString mouseEventToString(const QMouseEvent *ev)
83 message.append("MouseButtonDblClick ");
83 message.append("MouseButtonDblClick ");
84 break;
84 break;
85 case QEvent::MouseMove:
85 case QEvent::MouseMove:
86− message.append("MouseButtonMove ");
86+ message.append("MouseMove ");
87 break;
87 break;
88 default:
88 default:
89 message.append("INVALID_MOUSE_EVENT_TYPE ");
89 message.append("INVALID_MOUSE_EVENT_TYPE ");
plugins/Ubuntu/Gestures/TouchDispatcher.cpp+43
@@ -16,7 +16,9 @@
16
16
17#include "TouchDispatcher.h"
17#include "TouchDispatcher.h"
18
18
19+#include <QGuiApplication>
19#include <QScopedPointer>
20#include <QScopedPointer>
21+#include <QStyleHints>
20
22
21#pragma GCC diagnostic push
23#pragma GCC diagnostic push
22#pragma GCC diagnostic ignored "-pedantic"
24#pragma GCC diagnostic ignored "-pedantic"
@@ -32,6 +34,7 @@
32TouchDispatcher::TouchDispatcher()
34TouchDispatcher::TouchDispatcher()
33 : m_status(NoActiveTouch)
35 : m_status(NoActiveTouch)
34 , m_touchMouseId(-1)
36 , m_touchMouseId(-1)
37+ , m_touchMousePressTimestamp(0)
35{
38{
36}
39}
37
40
@@ -148,6 +151,18 @@ void TouchDispatcher::dispatchTouchBegin(
148 #endif
151 #endif
149 m_status = DeliveringMouseEvents;
152 m_status = DeliveringMouseEvents;
150 m_touchMouseId = targetTouchPoints.at(0).id();
153 m_touchMouseId = targetTouchPoints.at(0).id();
154+
155+ if (checkIfDoubleClicked(timestamp)) {
156+ QScopedPointer<QMouseEvent> doubleClickEvent(
157+ touchToMouseEvent(QEvent::MouseButtonDblClick, targetTouchPoints.at(0), timestamp,
158+ modifiers, false /* transformNeeded */));
159+ #if TOUCHDISPATCHER_DEBUG
160+ qDebug() << "[TouchDispatcher] dispatching" << qPrintable(mouseEventToString(doubleClickEvent.data()))
161+ << "to" << m_targetItem.data();
162+ #endif
163+ QCoreApplication::sendEvent(targetItem, doubleClickEvent.data());
164+ }
165+
151 } else {
166 } else {
152 #if TOUCHDISPATCHER_DEBUG
167 #if TOUCHDISPATCHER_DEBUG
153 qDebug() << "[TouchDispatcher] Item rejected the QMouseEvent.";
168 qDebug() << "[TouchDispatcher] Item rejected the QMouseEvent.";
@@ -322,3 +337,31 @@ QMouseEvent *TouchDispatcher::touchToMouseEvent(
322 //QGuiApplicationPrivate::setMouseEventSource(me, Qt::MouseEventSynthesizedByQt);
337 //QGuiApplicationPrivate::setMouseEventSource(me, Qt::MouseEventSynthesizedByQt);
323 return me;
338 return me;
324}
339}
340+
341+/*
342+ Copied from qquickwindow.cpp which has:
343+ Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies)
344+ Under GPL 3.0 license.
345+*/
346+bool TouchDispatcher::checkIfDoubleClicked(ulong newPressEventTimestamp)
347+{
348+ bool doubleClicked;
349+
350+ if (m_touchMousePressTimestamp == 0) {
351+ // just initialize the variable
352+ m_touchMousePressTimestamp = newPressEventTimestamp;
353+ doubleClicked = false;
354+ } else {
355+ ulong timeBetweenPresses = newPressEventTimestamp - m_touchMousePressTimestamp;
356+ ulong doubleClickInterval = static_cast<ulong>(qApp->styleHints()->
357+ mouseDoubleClickInterval());
358+ doubleClicked = timeBetweenPresses < doubleClickInterval;
359+ if (doubleClicked) {
360+ m_touchMousePressTimestamp = 0;
361+ } else {
362+ m_touchMousePressTimestamp = newPressEventTimestamp;
363+ }
364+ }
365+
366+ return doubleClicked;
367+}
plugins/Ubuntu/Gestures/TouchDispatcher.h+3
@@ -71,6 +71,8 @@ private:
71 QMouseEvent *touchToMouseEvent(QEvent::Type type, const QTouchEvent::TouchPoint &p,
71 QMouseEvent *touchToMouseEvent(QEvent::Type type, const QTouchEvent::TouchPoint &p,
72 ulong timestamp, Qt::KeyboardModifiers modifiers, bool transformNeeded = true);
72 ulong timestamp, Qt::KeyboardModifiers modifiers, bool transformNeeded = true);
73
73
74+ bool checkIfDoubleClicked(ulong newPressEventTimestamp);
75+
74 QPointer<QQuickItem> m_targetItem;
76 QPointer<QQuickItem> m_targetItem;
75
77
76 enum {
78 enum {
@@ -81,6 +83,7 @@ private:
81 } m_status;
83 } m_status;
82
84
83 int m_touchMouseId;
85 int m_touchMouseId;
86+ ulong m_touchMousePressTimestamp;
84};
87};
85
88
86#endif // UBUNTU_TOUCH_DISPATCHER_H
89#endif // UBUNTU_TOUCH_DISPATCHER_H
qml/Greeter/Greeter.qml+1−1
@@ -132,7 +132,7 @@ Showable {
132 TouchGate {
132 TouchGate {
133 targetItem: dragHandle
133 targetItem: dragHandle
134 anchors.fill: targetItem
134 anchors.fill: targetItem
135− enabled: targetITem.enabled
135+ enabled: targetItem.enabled
136 }
136 }
137
137
138 Loader {
138 Loader {
qml/Greeter/Infographics.qml+20−15
@@ -26,8 +26,15 @@ Item {
26
26
27 property int animDuration: 10
27 property int animDuration: 10
28
28
29+ QtObject {
30+ id: d
31+ objectName: "infographicPrivate"
29− property bool __useDotAnimation: true
32+ property bool useDotAnimation: true
30− property int __circleModifier: __useDotAnimation ? 1 : 2
33+ property int circleModifier: useDotAnimation ? 1 : 2
34+ property bool animating: dotHideAnimTimer.running
35+ || dotShowAnimTimer.running
36+ || circleChangeAnimTimer.running
37+ }
31
38
32 Connections {
39 Connections {
33 target: model
40 target: model
@@ -46,7 +53,7 @@ Item {
46 dotHideAnimTimer.stop()
53 dotHideAnimTimer.stop()
47 notification.hideAnim.stop()
54 notification.hideAnim.stop()
48
55
49− if (__useDotAnimation) {
56+ if (d.useDotAnimation) {
50 dotShowAnimTimer.startFromBeginning()
57 dotShowAnimTimer.startFromBeginning()
51 }
58 }
52 notification.showAnim.start()
59 notification.showAnim.start()
@@ -57,7 +64,7 @@ Item {
57 circleChangeAnimTimer.stop()
64 circleChangeAnimTimer.stop()
58 notification.showAnim.stop()
65 notification.showAnim.stop()
59
66
60− if (__useDotAnimation) {
67+ if (d.useDotAnimation) {
61 dotHideAnimTimer.startFromBeginning()
68 dotHideAnimTimer.startFromBeginning()
62 } else {
69 } else {
63 circleChangeAnimTimer.startFromBeginning()
70 circleChangeAnimTimer.startFromBeginning()
@@ -148,21 +155,21 @@ Item {
148 property: "opacity"
155 property: "opacity"
149 to: pastCircle.circleOpacity
156 to: pastCircle.circleOpacity
150 easing.type: Easing.OutCurve
157 easing.type: Easing.OutCurve
151− duration: circleChangeAnimTimer.interval * __circleModifier
158+ duration: circleChangeAnimTimer.interval * d.circleModifier
152 }
159 }
153 PropertyAnimation {
160 PropertyAnimation {
154 target: pastCircle
161 target: pastCircle
155 property: "scale"
162 property: "scale"
156 to: modelData
163 to: modelData
157 easing.type: Easing.OutCurve
164 easing.type: Easing.OutCurve
158− duration: circleChangeAnimTimer.interval * __circleModifier
165+ duration: circleChangeAnimTimer.interval * d.circleModifier
159 }
166 }
160 ColorAnimation {
167 ColorAnimation {
161 target: pastCircle
168 target: pastCircle
162 property: "color"
169 property: "color"
163 to: Gradient.threeColorByIndex(index, count, infographic.model.secondColor)
170 to: Gradient.threeColorByIndex(index, count, infographic.model.secondColor)
164 easing.type: Easing.OutCurve
171 easing.type: Easing.OutCurve
165− duration: circleChangeAnimTimer.interval * __circleModifier
172+ duration: circleChangeAnimTimer.interval * d.circleModifier
166 }
173 }
167 }
174 }
168 }
175 }
@@ -209,21 +216,21 @@ Item {
209 property: "opacity"
216 property: "opacity"
210 to: presentCircle.circleOpacity
217 to: presentCircle.circleOpacity
211 easing.type: Easing.OutCurve
218 easing.type: Easing.OutCurve
212− duration: circleChangeAnimTimer.interval * __circleModifier
219+ duration: circleChangeAnimTimer.interval * d.circleModifier
213 }
220 }
214 PropertyAnimation {
221 PropertyAnimation {
215 target: presentCircle
222 target: presentCircle
216 property: "scale"
223 property: "scale"
217 to: modelData
224 to: modelData
218 easing.type: Easing.OutCurve
225 easing.type: Easing.OutCurve
219− duration: circleChangeAnimTimer.interval * __circleModifier
226+ duration: circleChangeAnimTimer.interval * d.circleModifier
220 }
227 }
221 ColorAnimation {
228 ColorAnimation {
222 target: presentCircle
229 target: presentCircle
223 property: "color"
230 property: "color"
224 to: Gradient.threeColorByIndex(index, infographic.model.currentDay, infographic.model.firstColor)
231 to: Gradient.threeColorByIndex(index, infographic.model.currentDay, infographic.model.firstColor)
225 easing.type: Easing.OutCurve
232 easing.type: Easing.OutCurve
226− duration: circleChangeAnimTimer.interval * __circleModifier
233+ duration: circleChangeAnimTimer.interval * d.circleModifier
227 }
234 }
228 }
235 }
229 }
236 }
@@ -385,7 +392,7 @@ Item {
385 from: notification.baseOpacity
392 from: notification.baseOpacity
386 to: 0.0
393 to: 0.0
387 duration: notification.duration * dots.count
394 duration: notification.duration * dots.count
388− onStopped: if (!__useDotAnimation) infographic.model.readyForDataChange()
395+ onStopped: if (!d.useDotAnimation) infographic.model.readyForDataChange()
389 }
396 }
390 }
397 }
391 }
398 }
@@ -394,10 +401,8 @@ Item {
394 anchors.fill: dataCircle
401 anchors.fill: dataCircle
395
402
396 onDoubleClicked: {
403 onDoubleClicked: {
397− if (!dotHideAnimTimer.running &&
404+ if (!d.animating) {
398− !dotShowAnimTimer.running &&
405+ d.useDotAnimation = false
399− !circleChangeAnimTimer.running) {
400− __useDotAnimation = false
401 infographic.model.nextDataSource()
406 infographic.model.nextDataSource()
402 }
407 }
403 }
408 }
tests/plugins/Ubuntu/Gestures/GestureTest.cpp+6
@@ -90,6 +90,7 @@ DummyItem::DummyItem(QQuickItem *parent)
90 mousePressEventHandler = defaultMouseEventHandler;
90 mousePressEventHandler = defaultMouseEventHandler;
91 mouseMoveEventHandler = defaultMouseEventHandler;
91 mouseMoveEventHandler = defaultMouseEventHandler;
92 mouseReleaseEventHandler = defaultMouseEventHandler;
92 mouseReleaseEventHandler = defaultMouseEventHandler;
93+ mouseDoubleClickEventHandler = defaultMouseEventHandler;
93}
94}
94
95
95void DummyItem::touchEvent(QTouchEvent *event)
96void DummyItem::touchEvent(QTouchEvent *event)
@@ -113,6 +114,11 @@ void DummyItem::mouseReleaseEvent(QMouseEvent *event)
113 mouseReleaseEventHandler(event);
114 mouseReleaseEventHandler(event);
114}
115}
115
116
117+void DummyItem::mouseDoubleClickEvent(QMouseEvent *event)
118+{
119+ mouseDoubleClickEventHandler(event);
120+}
121+
116void DummyItem::defaultTouchEventHandler(QTouchEvent *event)
122void DummyItem::defaultTouchEventHandler(QTouchEvent *event)
117{
123{
118 event->accept();
124 event->accept();
tests/plugins/Ubuntu/Gestures/GestureTest.h+2
@@ -52,12 +52,14 @@ public:
52 std::function<void(QMouseEvent*)> mousePressEventHandler;
52 std::function<void(QMouseEvent*)> mousePressEventHandler;
53 std::function<void(QMouseEvent*)> mouseMoveEventHandler;
53 std::function<void(QMouseEvent*)> mouseMoveEventHandler;
54 std::function<void(QMouseEvent*)> mouseReleaseEventHandler;
54 std::function<void(QMouseEvent*)> mouseReleaseEventHandler;
55+ std::function<void(QMouseEvent*)> mouseDoubleClickEventHandler;
55protected:
56protected:
56 void touchEvent(QTouchEvent *event) override;
57 void touchEvent(QTouchEvent *event) override;
57
58
58 void mousePressEvent(QMouseEvent *event) override;
59 void mousePressEvent(QMouseEvent *event) override;
59 void mouseMoveEvent(QMouseEvent *event) override;
60 void mouseMoveEvent(QMouseEvent *event) override;
60 void mouseReleaseEvent(QMouseEvent *event) override;
61 void mouseReleaseEvent(QMouseEvent *event) override;
62+ void mouseDoubleClickEvent(QMouseEvent *event) override;
61private:
63private:
62 static void defaultTouchEventHandler(QTouchEvent *event);
64 static void defaultTouchEventHandler(QTouchEvent *event);
63 static void defaultMouseEventHandler(QMouseEvent *event);
65 static void defaultMouseEventHandler(QMouseEvent *event);
tests/plugins/Ubuntu/Gestures/tst_TouchDispatcher.cpp+59
@@ -16,6 +16,7 @@
16
16
17#include <QtTest>
17#include <QtTest>
18#include <QQuickView>
18#include <QQuickView>
19+#include <QStyleHints>
19
20
20#include "GestureTest.h"
21#include "GestureTest.h"
21
22
@@ -29,6 +30,8 @@ public:
29private Q_SLOTS:
30private Q_SLOTS:
30 void sendMouseEventIfTouchIgnored_data();
31 void sendMouseEventIfTouchIgnored_data();
31 void sendMouseEventIfTouchIgnored();
32 void sendMouseEventIfTouchIgnored();
33+ void mouseDoubleClick_data();
34+ void mouseDoubleClick();
32};
35};
33
36
34tst_TouchDispatcher::tst_TouchDispatcher()
37tst_TouchDispatcher::tst_TouchDispatcher()
@@ -85,6 +88,62 @@ void tst_TouchDispatcher::sendMouseEventIfTouchIgnored()
85 QCOMPARE(gotMousePressEvent, shouldGetMousePress);
88 QCOMPARE(gotMousePressEvent, shouldGetMousePress);
86}
89}
87
90
91+void tst_TouchDispatcher::mouseDoubleClick_data()
92+{
93+ QTest::addColumn<ulong>("timeBetweenClicks");
94+ QTest::addColumn<bool>("shouldSendDoubleClick");
95+
96+ QTest::newRow("double click") << static_cast<ulong>(qApp->styleHints()->mouseDoubleClickInterval() / 10) << true;
97+ QTest::newRow("two separate clicks") << static_cast<ulong>(qApp->styleHints()->mouseDoubleClickInterval() * 2) << false;
98+}
99+
100+void tst_TouchDispatcher::mouseDoubleClick()
101+{
102+ QFETCH(ulong, timeBetweenClicks);
103+ QFETCH(bool, shouldSendDoubleClick);
104+ DummyItem *dummyItem = new DummyItem(m_view->rootObject());
105+ dummyItem->setAcceptedMouseButtons(Qt::LeftButton);
106+
107+ TouchDispatcher touchDispatcher;
108+ touchDispatcher.setTargetItem(dummyItem);
109+
110+ bool gotDoubleClickEvent = false;
111+ dummyItem->mouseDoubleClickEventHandler = [&](QMouseEvent *event) {
112+ gotDoubleClickEvent = true;
113+ event->accept();
114+ };
115+
116+ dummyItem->touchEventHandler = [&](QTouchEvent *event) {
117+ event->ignore();
118+ };
119+
120+ ulong doubleClickInterval = static_cast<ulong>(qApp->styleHints()->mouseDoubleClickInterval());
121+
122+ QList<QTouchEvent::TouchPoint> touchPoints;
123+ {
124+ QTouchEvent::TouchPoint touchPoint;
125+ touchPoint.setId(0);
126+ touchPoint.setState(Qt::TouchPointPressed);
127+ touchPoints.append(touchPoint);
128+ }
129+ ulong timestamp = 12345;
130+
131+ touchDispatcher.dispatch(QEvent::TouchBegin, m_device, Qt::NoModifier, touchPoints, m_view, timestamp);
132+
133+ touchPoints[0].setState(Qt::TouchPointReleased);
134+ timestamp += doubleClickInterval / 10;
135+
136+ touchDispatcher.dispatch(QEvent::TouchEnd, m_device, Qt::NoModifier, touchPoints, m_view, timestamp);
137+
138+ touchPoints[0].setId(1);
139+ touchPoints[0].setState(Qt::TouchPointPressed);
140+ timestamp += timeBetweenClicks;
141+
142+ touchDispatcher.dispatch(QEvent::TouchBegin, m_device, Qt::NoModifier, touchPoints, m_view, timestamp);
143+
144+ QCOMPARE(gotDoubleClickEvent, shouldSendDoubleClick);
145+}
146+
88QTEST_MAIN(tst_TouchDispatcher)
147QTEST_MAIN(tst_TouchDispatcher)
89
148
90#include "tst_TouchDispatcher.moc"
149#include "tst_TouchDispatcher.moc"
tests/qmltests/Greeter/tst_SingleGreeter.qml+69−26
@@ -27,50 +27,72 @@ Item {
27 width: units.gu(60)
27 width: units.gu(60)
28 height: units.gu(80)
28 height: units.gu(80)
29
29
30− Greeter {
30+ Loader {
31− id: greeter
31+ id: greeterLoader
32− width: parent.width
32+ anchors.fill: parent
33− height: parent.height
34− x: 0; y: 0
35
33
36− property int minX: 0
34+ property bool itemDestroyed: false
37
35
36+ sourceComponent: Component {
37+ Greeter {
38+ anchors.fill: greeterLoader
39+
40+ property int minX: 0
41+
38− onXChanged: {
42+ onXChanged: {
39− if (x < minX) {
43+ if (x < minX) {
40− minX = x;
44+ minX = x;
41− }
42− }
45+ }
43− }
46+ }
44
47
45− Component {
48+ Component.onDestruction: {
46− id: greeterComponent
49+ greeterLoader.itemDestroyed = true;
47− Greeter {
50+ }
48− SignalSpy {
51+ SignalSpy {
49− objectName: "selectedSpy"
52+ objectName: "selectedSpy"
50− target: parent
53+ target: parent
51− signalName: "selected"
54+ signalName: "selected"
55+ }
52 }
56 }
53 }
57 }
54 }
58 }
55
59
56 SignalSpy {
60 SignalSpy {
57 id: unlockSpy
61 id: unlockSpy
58− target: greeter
62+ target: greeterLoader.item
59 signalName: "unlocked"
63 signalName: "unlocked"
60 }
64 }
61
65
62 SignalSpy {
66 SignalSpy {
63 id: teaseSpy
67 id: teaseSpy
64− target: greeter
68+ target: greeterLoader.item
65 signalName: "tease"
69 signalName: "tease"
66 }
70 }
67
71
72+ SignalSpy {
73+ id: infographicDataChangedSpy
74+ target: LightDM.Infographic
75+ signalName: "dataChanged"
76+ }
77+
68 UT.UnityTestCase {
78 UT.UnityTestCase {
69 name: "SingleGreeter"
79 name: "SingleGreeter"
70 when: windowShown
80 when: windowShown
71
81
82+ property Greeter greeter: greeterLoader.item
83+
72 function cleanup() {
84 function cleanup() {
73 AccountsService.statsWelcomeScreen = true
85 AccountsService.statsWelcomeScreen = true
86+
87+ // force a reload so that we get a fresh Greeter for the next test
88+ greeterLoader.itemDestroyed = false;
89+ greeterLoader.active = false;
90+ tryCompare(greeterLoader, "itemDestroyed", true);
91+
92+ unlockSpy.clear();
93+ teaseSpy.clear();
94+
95+ greeterLoader.active = true;
74 }
96 }
75
97
76 function test_properties() {
98 function test_properties() {
@@ -103,11 +125,32 @@ Item {
103 }
125 }
104
126
105 function test_initial_selected_signal() {
127 function test_initial_selected_signal() {
106− var greeterObj = greeterComponent.createObject(this)
128+ var selectedSpy = findChild(greeter, "selectedSpy");
107− var spy = findChild(greeterObj, "selectedSpy")
129+ selectedSpy.wait();
108− spy.wait()
130+ tryCompare(selectedSpy, "count", 1);
109− tryCompare(spy, "count", 1)
131+ }
110− greeterObj.destroy()
132+
133+ /*
134+ Regression test for https://bugs.launchpad.net/ubuntu/+source/unity8/+bug/1388359
135+ "User metrics can no longer be changed by double tap"
136+ */
137+ function test_doubleTapSwitchesToNextInfographic() {
138+ infographicDataChangedSpy.clear();
139+
140+ var infographicPrivate = findInvisibleChild(greeter, "infographicPrivate");
141+ verify(infographicPrivate);
142+
143+ // wait for the UI to settle down before double tapping it
144+ tryCompare(infographicPrivate, "animating", false);
145+
146+ var dataCircle = findChild(greeter, "dataCircle");
147+ verify(dataCircle);
148+
149+ tap(dataCircle);
150+ wait(1);
151+ tap(dataCircle);
152+
153+ tryCompare(infographicDataChangedSpy, "count", 1);
111 }
154 }
112 }
155 }
113}
156}