on xenial help sign in
Add touch ownership logic on top of qt input handling

It does mainly 3 things: - Adds TouchRegistry. - Adds TouchGate - Modifies DirectionalDragArea so that it uses TouchRegistry and only grabs a touch once it has recognized a gesture done by it. TouchRegistry was put into a library as both unity8 and Ubuntu.Gestures plugin need to use it. The next step is to have surfaces participating in the touch ownership negotiation. But for now they're put behind TouchGates so that they only receive thouches whose ownership has already been settled. The most noticeable improvement of this patch is that taps near screen edges will now reach applications. It also makes right-edge and bottom-edge drags only animate once a directional drag gesture has been fully recognized. Fixes: 1236290 Approved by: Albert Astals Cid

commit8b2cc4c318ff27af6656d39538d0e8eeaf83e263 authorDaniel d'Andrada <daniel.dandrada@canonical.com> dateMon 13 Oct 2014, 15:42 (12 years ago) parenta326304ef63f10
79 files changed+4622−469
CMakeLists.txt+2 debian/control+1−1 debian/unity8-private.install+1 libs/CMakeLists.txt+1 libs/UbuntuGestures/CMakeLists.txt+40 libs/UbuntuGestures/CandidateInactivityTimer.cpp+41 libs/UbuntuGestures/CandidateInactivityTimer.h+49 libs/UbuntuGestures/DebugHelpers.cpp+68 libs/UbuntuGestures/DebugHelpers.h+29 libs/UbuntuGestures/Pool.h+134 libs/UbuntuGestures/Timer.cpp+109 libs/UbuntuGestures/Timer.h+105 libs/UbuntuGestures/TouchOwnershipEvent.cpp+35 libs/UbuntuGestures/TouchOwnershipEvent.h+50 libs/UbuntuGestures/TouchRegistry.cpp+500 libs/UbuntuGestures/TouchRegistry.h+182 plugins/Ubuntu/Gestures/UbuntuGesturesGlobal.h → libs/UbuntuGestures/UbuntuGesturesGlobal.h+1−1 libs/UbuntuGestures/UnownedTouchEvent.cpp+39 libs/UbuntuGestures/UnownedTouchEvent.h+45 plugins/Ubuntu/Gestures/AxisVelocityCalculator.h+2−2 plugins/Ubuntu/Gestures/CMakeLists.txt+24−5 plugins/Ubuntu/Gestures/Direction.h+2−2 plugins/Ubuntu/Gestures/DirectionalDragArea.cpp+317−191 plugins/Ubuntu/Gestures/DirectionalDragArea.h+37−29 plugins/Ubuntu/Gestures/TimeSource.h+2−2 plugins/Ubuntu/Gestures/TouchGate.cpp+347 plugins/Ubuntu/Gestures/TouchGate.h+126 plugins/Ubuntu/Gestures/UbuntuGesturesQmlGlobal.h+23 plugins/Ubuntu/Gestures/plugin.cpp+3−1 plugins/Ubuntu/Gestures/plugin.h+1−1 plugins/Ubuntu/Gestures/qmldir+1−1 qml/Components/DragHandle.qml+2−2 qml/Components/EdgeDragArea.qml+2 qml/Components/InputMethod.qml+13 qml/Dash/Dash.qml+29−7 qml/Dash/DashContent.qml+4−1 qml/Dash/GenericScopeView.qml+2 qml/Launcher/Launcher.qml+3−1 qml/Launcher/LauncherPanel.qml+1 qml/Stages/ApplicationWindow.qml−18 qml/Stages/PhoneStage.qml+14−5 qml/Stages/SessionContainer.qml+1−13 qml/Stages/SurfaceContainer.qml+37−6 src/CMakeLists.txt+6 src/Dash/CMakeLists.txt+5 src/Dash/main.cpp+6 src/main.cpp+5 tests/CMakeLists.txt+1 tests/libs/CMakeLists.txt+1 tests/libs/UbuntuGestures/CMakeLists.txt+20 tests/libs/UbuntuGestures/tst_TouchRegistry.cpp+803 tests/mocks/Unity/Application/CMakeLists.txt+1 tests/mocks/Unity/Application/MirSurfaceItem.cpp+18−22 tests/mocks/Unity/Application/MirSurfaceItem.h+18−1 tests/mocks/Unity/Application/MirSurfaceItem.qml−12 tests/mocks/Unity/Application/UbuntuKeyboardInfo.cpp+28 tests/mocks/Unity/Application/UbuntuKeyboardInfo.h+60 tests/mocks/Unity/Application/plugin.cpp+7 tests/plugins/Ubuntu/Gestures/CMakeLists.txt+9−2 tests/plugins/Ubuntu/Gestures/RightwardsLauncher.qml+3 tests/plugins/Ubuntu/Gestures/touchGateExample.qml+27 tests/plugins/Ubuntu/Gestures/tst_DirectionalDragArea.cpp+491−14 tests/plugins/Ubuntu/Gestures/tst_DirectionalDragArea.qml+1 tests/plugins/Ubuntu/Gestures/tst_TouchGate.cpp+345 tests/qmltests/Components/CMakeLists.txt+2−1 tests/qmltests/Components/tst_DragHandle.cpp+12−15 tests/qmltests/Greeter/tst_SingleGreeter.qml+1−1 tests/qmltests/Launcher/tst_Launcher.qml+117−101 tests/qmltests/Stages/tst_ApplicationWindow.qml+2−2 tests/qmltests/Stages/tst_PhoneStage.qml+2−1 tests/qmltests/Stages/tst_SurfaceContainer.qml+26−1 tests/qmltests/tst_Shell.qml+118−3 tests/uqmlscene/CMakeLists.txt+2 tests/uqmlscene/README+3−1 tests/uqmlscene/main.cpp+9 tests/utils/modules/Unity/Test/CMakeLists.txt+2 tests/utils/modules/Unity/Test/UnityTestCase.qml+2 tests/utils/modules/Unity/Test/testutil.cpp+41−2 tests/utils/modules/Unity/Test/testutil.h+3−1
CMakeLists.txt+2
@@ -58,6 +58,7 @@ find_package(Qt5DBus 5.2 REQUIRED)
5858include(GNUInstallDirs)
5959
6060set(SHELL_APP_DIR ${CMAKE_INSTALL_DATADIR}/unity8)
61+set(SHELL_PRIVATE_LIBDIR ${CMAKE_INSTALL_LIBDIR}/unity8)
6162
6263execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE} --variable=plugindir_suffix unity-shell-api OUTPUT_VARIABLE SHELL_INSTALL_QML OUTPUT_STRIP_TRAILING_WHITESPACE)
6364if(SHELL_INSTALL_QML STREQUAL "")
@@ -109,6 +110,7 @@ include_directories(
109110
110111# add subdirectories to build
111112add_subdirectory(include)
113+add_subdirectory(libs)
112114add_subdirectory(src)
113115add_subdirectory(tools)
114116add_subdirectory(qml)
debian/control+1−1
@@ -86,7 +86,7 @@ Depends: gsettings-desktop-schemas,
8686 qmenumodel-qml (>= 0.2.8),
8787 qml-module-qtquick-xmllistmodel,
8888 qtdeclarative5-gsettings1.0,
89− qtdeclarative5-qtmir-plugin (>= 0.4.3),
89+ qtdeclarative5-qtmir-plugin (>= 0.4.4),
9090 qtdeclarative5-ubuntu-telephony0.1,
9191 unity-launcher-impl-4,
9292 unity8-common (= ${source:Version}),
debian/unity8-private.install+1
@@ -9,5 +9,6 @@ usr/lib/*/unity8/qml/SessionBroadcast
99usr/lib/*/unity8/qml/Ubuntu
1010usr/lib/*/unity8/qml/Unity
1111usr/lib/*/unity8/qml/Utils
12+usr/lib/*/unity8/libUbuntuGestures*
1213usr/share/accountsservice/interfaces
1314usr/share/dbus-1/interfaces
libs/CMakeLists.txtnew file+1
@@ -0,0 +1 @@
1+add_subdirectory(UbuntuGestures)
libs/UbuntuGestures/CMakeLists.txtnew file+40
@@ -0,0 +1,40 @@
1+# in order to include Qt's private headers
2+remove_definitions(-DQT_NO_KEYWORDS)
3+
4+set(UbuntuGestures_SOURCES
5+ CandidateInactivityTimer.cpp
6+ DebugHelpers.cpp
7+ Timer.cpp
8+ TouchOwnershipEvent.cpp
9+ TouchRegistry.cpp
10+ UnownedTouchEvent.cpp
11+)
12+
13+add_definitions(-DUBUNTUGESTURES_LIBRARY)
14+
15+add_library(UbuntuGestures SHARED ${UbuntuGestures_SOURCES})
16+
17+qt5_use_modules(UbuntuGestures Core Quick)
18+
19+# So that Foo.cpp can #include "Foo.moc"
20+include_directories(${CMAKE_CURRENT_BINARY_DIR})
21+
22+install(TARGETS UbuntuGestures
23+ DESTINATION ${SHELL_PRIVATE_LIBDIR})
24+
25+
26+# There's no cmake var for v8 include path :-/ so create one
27+LIST(GET Qt5Core_INCLUDE_DIRS 0 QtCoreDir0)
28+if(${Qt5Core_VERSION_STRING} VERSION_LESS "5.1.0")
29+ SET(Qt5V8_PRIVATE_INCLUDE_DIR ${QtCoreDir0}/../QtV8/${Qt5Core_VERSION_STRING}/QtV8)
30+else()
31+ SET(Qt5V8_PRIVATE_INCLUDE_DIR ${QtCoreDir0}/QtV8/${Qt5Core_VERSION_STRING}/QtV8)
32+endif()
33+
34+# DANGER! DANGER! Using Qt's private API!
35+include_directories(
36+ ${Qt5Qml_PRIVATE_INCLUDE_DIRS}
37+ ${Qt5Quick_INCLUDE_DIRS}
38+ ${Qt5Quick_PRIVATE_INCLUDE_DIRS}
39+ ${Qt5V8_PRIVATE_INCLUDE_DIR}
40+)
libs/UbuntuGestures/CandidateInactivityTimer.cppnew file+41
@@ -0,0 +1,41 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "CandidateInactivityTimer.h"
18+
19+namespace UbuntuGestures {
20+
21+CandidateInactivityTimer::CandidateInactivityTimer(int touchId, QQuickItem *candidate,
22+ AbstractTimerFactory &timerFactory, QObject *parent)
23+ : QObject(parent)
24+ , m_touchId(touchId)
25+ , m_candidate(candidate)
26+{
27+ m_timer = timerFactory.createTimer(this);
28+ connect(m_timer, &AbstractTimer::timeout,
29+ this, &CandidateInactivityTimer::onTimeout);
30+ m_timer->setInterval(durationMs);
31+ m_timer->setSingleShot(true);
32+ m_timer->start();
33+}
34+
35+void CandidateInactivityTimer::onTimeout()
36+{
37+ qWarning("[TouchRegistry] Candidate for touch %d defaulted!", m_touchId);
38+ Q_EMIT candidateDefaulted(m_touchId, m_candidate);
39+}
40+
41+} // namespace UbuntuGestures
libs/UbuntuGestures/CandidateInactivityTimer.hnew file+49
@@ -0,0 +1,49 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef UBUNTUGESTURES_CANDIDATE_INACTIVITY_TIMER_H
18+#define UBUNTUGESTURES_CANDIDATE_INACTIVITY_TIMER_H
19+
20+#include <QObject>
21+
22+class QQuickItem;
23+
24+#include "Timer.h"
25+
26+namespace UbuntuGestures {
27+
28+class UBUNTUGESTURES_EXPORT CandidateInactivityTimer : public QObject {
29+ Q_OBJECT
30+public:
31+ CandidateInactivityTimer(int touchId, QQuickItem *candidate,
32+ AbstractTimerFactory &timerFactory,
33+ QObject *parent = nullptr);
34+
35+ const int durationMs = 350;
36+
37+Q_SIGNALS:
38+ void candidateDefaulted(int touchId, QQuickItem *candidate);
39+private Q_SLOTS:
40+ void onTimeout();
41+private:
42+ AbstractTimer *m_timer;
43+ int m_touchId;
44+ QQuickItem *m_candidate;
45+};
46+
47+} // namespace UbuntuGestures
48+
49+#endif // UBUNTUGESTURES_CANDIDATE_INACTIVITY_TIMER_H
libs/UbuntuGestures/DebugHelpers.cppnew file+68
@@ -0,0 +1,68 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "DebugHelpers.h"
18+#include <QTouchEvent>
19+
20+QString touchPointStateToString(Qt::TouchPointState state)
21+{
22+ switch (state) {
23+ case Qt::TouchPointPressed:
24+ return QString("pressed");
25+ case Qt::TouchPointMoved:
26+ return QString("moved");
27+ case Qt::TouchPointStationary:
28+ return QString("stationary");
29+ case Qt::TouchPointReleased:
30+ return QString("released");
31+ default:
32+ return QString("INVALID_STATE");
33+ }
34+}
35+
36+QString touchEventToString(const QTouchEvent *ev)
37+{
38+ QString message;
39+
40+ switch (ev->type()) {
41+ case QEvent::TouchBegin:
42+ message.append("TouchBegin ");
43+ break;
44+ case QEvent::TouchUpdate:
45+ message.append("TouchUpdate ");
46+ break;
47+ case QEvent::TouchEnd:
48+ message.append("TouchEnd ");
49+ break;
50+ case QEvent::TouchCancel:
51+ message.append("TouchCancel ");
52+ break;
53+ default:
54+ message.append("INVALID_TOUCH_EVENT_TYPE ");
55+ }
56+
57+ foreach(const QTouchEvent::TouchPoint& touchPoint, ev->touchPoints()) {
58+ message.append(
59+ QString("(id:%1, state:%2, scenePos:(%3,%4)) ")
60+ .arg(touchPoint.id())
61+ .arg(touchPointStateToString(touchPoint.state()))
62+ .arg(touchPoint.scenePos().x())
63+ .arg(touchPoint.scenePos().y())
64+ );
65+ }
66+
67+ return message;
68+}
libs/UbuntuGestures/DebugHelpers.hnew file+29
@@ -0,0 +1,29 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef UBUNTUGESTURES_DEBUG_HELPER_H
18+#define UBUNTUGESTURES_DEBUG_HELPER_H
19+
20+#include <QString>
21+
22+#include "UbuntuGesturesGlobal.h"
23+
24+class QTouchEvent;
25+
26+UBUNTUGESTURES_EXPORT QString touchPointStateToString(Qt::TouchPointState state);
27+UBUNTUGESTURES_EXPORT QString touchEventToString(const QTouchEvent *ev);
28+
29+#endif // UBUNTUGESTURES_DEBUG_HELPER_H
libs/UbuntuGestures/Pool.hnew file+134
@@ -0,0 +1,134 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef UBUNTUGESTURES_POOL_H
18+#define UBUNTUGESTURES_POOL_H
19+
20+#include <QVector>
21+
22+#include "UbuntuGesturesGlobal.h"
23+
24+/*
25+ An object pool.
26+ Avoids unnecessary creations/initializations and deletions/destructions of items. Useful
27+ in a scenario where items are created and destroyed very frequently but the total number
28+ of items at any given time remains small. They're stored in a unordered fashion.
29+
30+ To be used in Pool, ItemType needs to have the following methods:
31+
32+ - ItemType();
33+
34+ A constructor that takes no parameters. An object contructed with it must return false if
35+ isValid() is called.
36+
37+ - bool isValid() const;
38+
39+ Returns wheter the object holds a valid , "filled" state or is empty.
40+ Used by Pool to check if the slot occupied by this object is actually available.
41+
42+ - void reset();
43+
44+ Resets the object to its initial, empty, state. After calling this method, isValid() must
45+ return false.
46+ */
47+template <class ItemType> class Pool
48+{
49+public:
50+ Pool() : m_lastUsedIndex(-1) {
51+ }
52+
53+ class Iterator {
54+ public:
55+ Iterator() : index(-1), item(nullptr) {}
56+ Iterator(int index, ItemType *item)
57+ : index(index), item(item) {}
58+
59+ ItemType *operator->() const { return item; }
60+ ItemType &operator*() const { return *item; }
61+ ItemType &value() const { return *item; }
62+
63+ Iterator &operator= (const Iterator& other) {
64+ index = other.index;
65+ item = other.item;
66+
67+ // by convention, always return *this
68+ return *this;
69+ }
70+
71+ operator bool() const { return item != nullptr; }
72+
73+ int index;
74+ ItemType *item;
75+ };
76+
77+ Iterator &end() const { return Iterator(); }
78+
79+ ItemType &getEmptySlot() {
80+ Q_ASSERT(m_lastUsedIndex < m_slots.size());
81+
82+ // Look for an in-between vacancy first
83+ for (int i = 0; i < m_lastUsedIndex; ++i) {
84+ ItemType &item = m_slots[i];
85+ if (!item.isValid()) {
86+ return item;
87+ }
88+ }
89+
90+ ++m_lastUsedIndex;
91+ if (m_lastUsedIndex >= m_slots.size()) {
92+ m_slots.resize(m_lastUsedIndex + 1);
93+ }
94+
95+ return m_slots[m_lastUsedIndex];
96+ }
97+
98+ void freeSlot(Iterator &iterator) {
99+ m_slots[iterator.index].reset();
100+ if (iterator.index == m_lastUsedIndex) {
101+ do {
102+ --m_lastUsedIndex;
103+ } while (m_lastUsedIndex >= 0 && !m_slots.at(m_lastUsedIndex).isValid());
104+ }
105+ }
106+
107+ // Iterates through all valid items (i.e. the occupied slots)
108+ // calling the given function, with the option of ending the loop early.
109+ //
110+ // bool Func(Iterator& item)
111+ //
112+ // Returning true means it wants to continue the "for" loop, false
113+ // terminates the loop.
114+ template<typename Func> void forEach(Func func) {
115+ Iterator it;
116+ for (it.index = 0; it.index <= m_lastUsedIndex; ++it.index) {
117+ it.item = &m_slots[it.index];
118+ if (!it.item->isValid())
119+ continue;
120+
121+ if (!func(it))
122+ break;
123+ }
124+ }
125+
126+ bool isEmpty() const { return m_lastUsedIndex == -1; }
127+
128+
129+private:
130+ QVector<ItemType> m_slots;
131+ int m_lastUsedIndex;
132+};
133+
134+#endif // UBUNTUGESTURES_POOL_H
libs/UbuntuGestures/Timer.cppnew file+109
@@ -0,0 +1,109 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "Timer.h"
18+
19+namespace UbuntuGestures {
20+
21+Timer::Timer(QObject *parent) : AbstractTimer(parent)
22+{
23+ m_timer.setSingleShot(false);
24+ connect(&m_timer, &QTimer::timeout, this, &AbstractTimer::timeout);
25+}
26+
27+int Timer::interval() const
28+{
29+ return m_timer.interval();
30+}
31+
32+void Timer::setInterval(int msecs)
33+{
34+ m_timer.setInterval(msecs);
35+}
36+
37+void Timer::start()
38+{
39+ m_timer.start();
40+ AbstractTimer::start();
41+}
42+
43+void Timer::stop()
44+{
45+ m_timer.stop();
46+ AbstractTimer::stop();
47+}
48+
49+bool Timer::isSingleShot() const
50+{
51+ return m_timer.isSingleShot();
52+}
53+
54+void Timer::setSingleShot(bool value)
55+{
56+ m_timer.setSingleShot(value);
57+}
58+
59+/////////////////////////////////// FakeTimer //////////////////////////////////
60+
61+FakeTimer::FakeTimer(QObject *parent)
62+ : UbuntuGestures::AbstractTimer(parent)
63+ , m_interval(0)
64+ , m_singleShot(false)
65+{
66+}
67+
68+int FakeTimer::interval() const
69+{
70+ return m_interval;
71+}
72+
73+void FakeTimer::setInterval(int msecs)
74+{
75+ m_interval = msecs;
76+}
77+
78+bool FakeTimer::isSingleShot() const
79+{
80+ return m_singleShot;
81+}
82+
83+void FakeTimer::setSingleShot(bool value)
84+{
85+ m_singleShot = value;
86+}
87+
88+/////////////////////////////////// FakeTimerFactory //////////////////////////////////
89+
90+AbstractTimer *FakeTimerFactory::createTimer(QObject *parent)
91+{
92+ FakeTimer *fakeTimer = new FakeTimer(parent);
93+
94+ timers.append(fakeTimer);
95+
96+ return fakeTimer;
97+}
98+
99+void FakeTimerFactory::makeRunningTimersTimeout()
100+{
101+ for (int i = 0; i < timers.count(); ++i) {
102+ FakeTimer *timer = timers[i].data();
103+ if (timer && timer->isRunning()) {
104+ timer->emitTimeout();
105+ }
106+ }
107+}
108+
109+} // namespace UbuntuGestures
libs/UbuntuGestures/Timer.hnew file+105
@@ -0,0 +1,105 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef UBUNTUGESTURES_TIMER_H
18+#define UBUNTUGESTURES_TIMER_H
19+
20+#include "UbuntuGesturesGlobal.h"
21+
22+#include <QObject>
23+#include <QPointer>
24+#include <QTimer>
25+
26+namespace UbuntuGestures {
27+
28+/* Defines an interface for a Timer. Useful for tests. */
29+class UBUNTUGESTURES_EXPORT AbstractTimer : public QObject
30+{
31+ Q_OBJECT
32+public:
33+ AbstractTimer(QObject *parent) : QObject(parent), m_isRunning(false) {}
34+ virtual int interval() const = 0;
35+ virtual void setInterval(int msecs) = 0;
36+ virtual void start() { m_isRunning = true; }
37+ virtual void stop() { m_isRunning = false; }
38+ bool isRunning() const { return m_isRunning; }
39+ virtual bool isSingleShot() const = 0;
40+ virtual void setSingleShot(bool value) = 0;
41+Q_SIGNALS:
42+ void timeout();
43+private:
44+ bool m_isRunning;
45+};
46+
47+/* Essentially a QTimer wrapper */
48+class UBUNTUGESTURES_EXPORT Timer : public AbstractTimer
49+{
50+ Q_OBJECT
51+public:
52+ Timer(QObject *parent = nullptr);
53+
54+ int interval() const override;
55+ void setInterval(int msecs) override;
56+ void start() override;
57+ void stop() override;
58+ bool isSingleShot() const override;
59+ void setSingleShot(bool value) override;
60+private:
61+ QTimer m_timer;
62+};
63+
64+/* For tests */
65+class UBUNTUGESTURES_EXPORT FakeTimer : public AbstractTimer
66+{
67+ Q_OBJECT
68+public:
69+ FakeTimer(QObject *parent = nullptr);
70+
71+ virtual void emitTimeout() { Q_EMIT timeout(); }
72+
73+ int interval() const override;
74+ void setInterval(int msecs) override;
75+ bool isSingleShot() const override;
76+ void setSingleShot(bool value) override;
77+private:
78+ int m_interval;
79+ bool m_singleShot;
80+};
81+
82+class UBUNTUGESTURES_EXPORT AbstractTimerFactory
83+{
84+public:
85+ virtual ~AbstractTimerFactory() {}
86+ virtual AbstractTimer *createTimer(QObject *parent = nullptr) = 0;
87+};
88+
89+class UBUNTUGESTURES_EXPORT TimerFactory : public AbstractTimerFactory
90+{
91+public:
92+ AbstractTimer *createTimer(QObject *parent = nullptr) override { return new Timer(parent); }
93+};
94+
95+class UBUNTUGESTURES_EXPORT FakeTimerFactory : public AbstractTimerFactory
96+{
97+public:
98+ AbstractTimer *createTimer(QObject *parent = nullptr) override;
99+ void makeRunningTimersTimeout();
100+ QList<QPointer<FakeTimer>> timers;
101+};
102+
103+} // namespace UbuntuGestures
104+
105+#endif // UBUNTUGESTURES_TIMER_H
libs/UbuntuGestures/TouchOwnershipEvent.cppnew file+35
@@ -0,0 +1,35 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "TouchOwnershipEvent.h"
18+
19+QEvent::Type TouchOwnershipEvent::m_touchOwnershipType = (QEvent::Type)-1;
20+
21+TouchOwnershipEvent::TouchOwnershipEvent(int touchId, bool gained)
22+ : QEvent(touchOwnershipEventType())
23+ , m_touchId(touchId)
24+ , m_gained(gained)
25+{
26+}
27+
28+QEvent::Type TouchOwnershipEvent::touchOwnershipEventType()
29+{
30+ if (m_touchOwnershipType == (QEvent::Type)-1) {
31+ m_touchOwnershipType = (QEvent::Type)registerEventType();
32+ }
33+
34+ return m_touchOwnershipType;
35+}
libs/UbuntuGestures/TouchOwnershipEvent.hnew file+50
@@ -0,0 +1,50 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef UBUNTU_TOUCHOWNERSHIPEVENT_H
18+#define UBUNTU_TOUCHOWNERSHIPEVENT_H
19+
20+#include <QEvent>
21+#include "UbuntuGesturesGlobal.h"
22+
23+/*
24+ When an item get an ownership event for a touch it can grab/steal that touch
25+ with a clean conscience.
26+ */
27+class UBUNTUGESTURES_EXPORT TouchOwnershipEvent : public QEvent
28+{
29+public:
30+ TouchOwnershipEvent(int touchId, bool gained);
31+
32+ static Type touchOwnershipEventType();
33+
34+ /*
35+ Whether ownership was gained (true) or lost (false)
36+ */
37+ bool gained() const { return m_gained; }
38+
39+ /*
40+ Id of the touch whose ownership was granted.
41+ */
42+ int touchId() const { return m_touchId; }
43+
44+private:
45+ static Type m_touchOwnershipType;
46+ int m_touchId;
47+ bool m_gained;
48+};
49+
50+#endif // UBUNTU_TOUCHOWNERSHIPEVENT_H
libs/UbuntuGestures/TouchRegistry.cppnew file+500
@@ -0,0 +1,500 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "TouchRegistry.h"
18+
19+#include <QCoreApplication>
20+#include <QDebug>
21+
22+#pragma GCC diagnostic push
23+#pragma GCC diagnostic ignored "-pedantic"
24+#include <private/qquickitem_p.h>
25+#pragma GCC diagnostic pop
26+
27+#include "CandidateInactivityTimer.h"
28+#include "Timer.h"
29+#include "TouchOwnershipEvent.h"
30+#include "UnownedTouchEvent.h"
31+
32+#define TOUCHREGISTRY_DEBUG 0
33+
34+#if TOUCHREGISTRY_DEBUG
35+ #include "DebugHelpers.h"
36+ #define UG_DEBUG qDebug() << "[TouchRegistry]"
37+#endif // TOUCHREGISTRY_DEBUG
38+
39+using namespace UbuntuGestures;
40+
41+TouchRegistry *TouchRegistry::m_instance = nullptr;
42+
43+TouchRegistry::TouchRegistry(QObject *parent)
44+ : TouchRegistry(parent, new TimerFactory)
45+{
46+}
47+
48+TouchRegistry::TouchRegistry(QObject *parent, AbstractTimerFactory *timerFactory)
49+ : QObject(parent)
50+ , m_inDispatchLoop(false)
51+ , m_timerFactory(timerFactory)
52+{
53+ if (m_instance == nullptr) {
54+ m_instance = this;
55+ } else {
56+ qFatal("Cannot have more than one instance of TouchRegistry. It must be a singleton.");
57+ }
58+}
59+
60+TouchRegistry::~TouchRegistry()
61+{
62+ Q_ASSERT(m_instance != nullptr);
63+ m_instance = nullptr;
64+ delete m_timerFactory;
65+}
66+
67+void TouchRegistry::update(const QTouchEvent *event)
68+{
69+ #if TOUCHREGISTRY_DEBUG
70+ UG_DEBUG << "got" << qPrintable(touchEventToString(event));
71+ #endif
72+
73+ const QList<QTouchEvent::TouchPoint> &touchPoints = event->touchPoints();
74+ for (int i = 0; i < touchPoints.count(); ++i) {
75+ const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
76+ if (touchPoint.state() == Qt::TouchPointPressed) {
77+ TouchInfo &touchInfo = m_touchInfoPool.getEmptySlot();
78+ touchInfo.init(touchPoint.id());
79+ } else if (touchPoint.state() == Qt::TouchPointReleased) {
80+ Pool<TouchInfo>::Iterator touchInfo = findTouchInfo(touchPoint.id());
81+
82+ touchInfo->physicallyEnded = true;
83+ }
84+ }
85+
86+ deliverTouchUpdatesToUndecidedCandidatesAndWatchers(event);
87+
88+ freeEndedTouchInfos();
89+}
90+
91+void TouchRegistry::deliverTouchUpdatesToUndecidedCandidatesAndWatchers(const QTouchEvent *event)
92+{
93+ // TODO: Look into how we could optimize this whole thing.
94+ // Although it's not really a problem as we should have at most two candidates
95+ // for each point and there should not be many active points at any given moment.
96+ // But having three nested for-loops does scare.
97+
98+ // TODO: Don't send it to the object that is already receiving the regular event
99+ // because QQuickWindow is sending it to him (i.e., he's the touch owner from Qt's point of view)
100+ // Problem is, we cannnot easily get this information.
101+
102+ const QList<QTouchEvent::TouchPoint> &updatedTouchPoints = event->touchPoints();
103+
104+ // Maps an item to the touches in this event he should be informed about.
105+ // E.g.: a QTouchEvent might have three touches but a given item might be interested in only
106+ // one of them. So he will get a UnownedTouchEvent from this QTouchEvent containing only that
107+ // touch point.
108+ QMap<QQuickItem*, QList<int>> touchIdsForItems;
109+
110+ // Build touchIdsForItems
111+ m_touchInfoPool.forEach([&](Pool<TouchInfo>::Iterator &touchInfo) {
112+ if (touchInfo->isOwned() && touchInfo->watchers.isEmpty())
113+ return true;
114+
115+ for (int j = 0; j < updatedTouchPoints.count(); ++j) {
116+ if (updatedTouchPoints[j].id() == touchInfo->id) {
117+ if (!touchInfo->isOwned()) {
118+ for (int i = 0; i < touchInfo->candidates.count(); ++i) {
119+ CandidateInfo &candidate = touchInfo->candidates[i];
120+ Q_ASSERT(!candidate.item.isNull());
121+ touchIdsForItems[candidate.item.data()].append(touchInfo->id);
122+ }
123+ }
124+
125+ const QList<QPointer<QQuickItem>> &watchers = touchInfo->watchers;
126+ for (int i = 0; i < watchers.count(); ++i) {
127+ if (!watchers[i].isNull()) {
128+ touchIdsForItems[watchers[i].data()].append(touchInfo->id);
129+ }
130+ }
131+
132+ return true;
133+ }
134+ }
135+
136+ return true;
137+ });
138+
139+ // TODO: Consider what happens if an item calls any of TouchRegistry's public methods
140+ // from the event handler callback.
141+ m_inDispatchLoop = true;
142+ auto it = touchIdsForItems.constBegin();
143+ while (it != touchIdsForItems.constEnd()) {
144+ QQuickItem *item = it.key();
145+ const QList<int> &touchIds = it.value();
146+ dispatchPointsToItem(event, touchIds, item);
147+ ++it;
148+ };
149+ m_inDispatchLoop = false;
150+}
151+
152+void TouchRegistry::freeEndedTouchInfos()
153+{
154+ m_touchInfoPool.forEach([&](Pool<TouchInfo>::Iterator &touchInfo) {
155+ if (touchInfo->ended()) {
156+ m_touchInfoPool.freeSlot(touchInfo);
157+ }
158+ return true;
159+ });
160+}
161+
162+/*
163+ Extracts the touches with the given touchIds from event and send them in a
164+ UnownedTouchEvent to the given item
165+ */
166+void TouchRegistry::dispatchPointsToItem(const QTouchEvent *event, const QList<int> &touchIds,
167+ QQuickItem *item)
168+{
169+ Qt::TouchPointStates touchPointStates = 0;
170+ QList<QTouchEvent::TouchPoint> touchPoints;
171+
172+ const QList<QTouchEvent::TouchPoint> &allTouchPoints = event->touchPoints();
173+
174+ QTransform windowToCandidateTransform = QQuickItemPrivate::get(item)->windowToItemTransform();
175+ QMatrix4x4 windowToCandidateMatrix(windowToCandidateTransform);
176+
177+ for (int i = 0; i < allTouchPoints.count(); ++i) {
178+ const QTouchEvent::TouchPoint &originalTouchPoint = allTouchPoints[i];
179+ if (touchIds.contains(originalTouchPoint.id())) {
180+ QTouchEvent::TouchPoint touchPoint = originalTouchPoint;
181+
182+ translateTouchPointFromScreenToWindowCoords(touchPoint);
183+
184+ // Set the point's local coordinates to that of the item
185+ touchPoint.setRect(windowToCandidateTransform.mapRect(touchPoint.sceneRect()));
186+ touchPoint.setStartPos(windowToCandidateTransform.map(touchPoint.startScenePos()));
187+ touchPoint.setLastPos(windowToCandidateTransform.map(touchPoint.lastScenePos()));
188+ touchPoint.setVelocity(windowToCandidateMatrix.mapVector(touchPoint.velocity()).toVector2D());
189+
190+ touchPoints.append(touchPoint);
191+ touchPointStates |= touchPoint.state();
192+ }
193+ }
194+
195+ QTouchEvent *eventForItem = new QTouchEvent(event->type(),
196+ event->device(),
197+ event->modifiers(),
198+ touchPointStates,
199+ touchPoints);
200+ eventForItem->setWindow(event->window());
201+ eventForItem->setTimestamp(event->timestamp());
202+ eventForItem->setTarget(event->target());
203+
204+ UnownedTouchEvent unownedTouchEvent(eventForItem);
205+
206+ #if TOUCHREGISTRY_DEBUG
207+ UG_DEBUG << "Sending unowned" << qPrintable(touchEventToString(eventForItem))
208+ << "to" << item;
209+ #endif
210+
211+ QCoreApplication::sendEvent(item, &unownedTouchEvent);
212+}
213+
214+void TouchRegistry::translateTouchPointFromScreenToWindowCoords(QTouchEvent::TouchPoint &touchPoint)
215+{
216+ touchPoint.setScreenRect(touchPoint.sceneRect());
217+ touchPoint.setStartScreenPos(touchPoint.startScenePos());
218+ touchPoint.setLastScreenPos(touchPoint.lastScenePos());
219+
220+ touchPoint.setSceneRect(touchPoint.rect());
221+ touchPoint.setStartScenePos(touchPoint.startPos());
222+ touchPoint.setLastScenePos(touchPoint.lastPos());
223+}
224+
225+bool TouchRegistry::eventFilter(QObject *watched, QEvent *event)
226+{
227+ Q_UNUSED(watched);
228+
229+ switch (event->type()) {
230+ case QEvent::TouchBegin:
231+ case QEvent::TouchUpdate:
232+ case QEvent::TouchEnd:
233+ case QEvent::TouchCancel:
234+ update(static_cast<QTouchEvent*>(event));
235+ break;
236+ default:
237+ // do nothing
238+ break;
239+ }
240+
241+ // Do not filter out the event. i.e., let it be handled further as
242+ // we're just monitoring events
243+ return false;
244+}
245+
246+void TouchRegistry::addCandidateOwnerForTouch(int id, QQuickItem *candidate)
247+{
248+ #if TOUCHREGISTRY_DEBUG
249+ UG_DEBUG << "addCandidateOwnerForTouch id" << id << "candidate" << candidate;
250+ #endif
251+
252+ Pool<TouchInfo>::Iterator touchInfo = findTouchInfo(id);
253+ if (!touchInfo) { qFatal("TouchRegistry: Failed to find TouchInfo"); }
254+
255+ if (touchInfo->isOwned()) {
256+ qWarning("TouchRegistry: trying to add candidate owner for a touch that's already owned");
257+ return;
258+ }
259+
260+ // TODO: Check if candidate already exists
261+
262+ CandidateInfo candidateInfo;
263+ candidateInfo.undecided = true;
264+ candidateInfo.item = candidate;
265+ candidateInfo.inactivityTimer = new CandidateInactivityTimer(id, candidate,
266+ *m_timerFactory,
267+ this);
268+ connect(candidateInfo.inactivityTimer, &CandidateInactivityTimer::candidateDefaulted,
269+ this, &TouchRegistry::rejectCandidateOwnerForTouch);
270+
271+ touchInfo->candidates.append(candidateInfo);
272+}
273+
274+void TouchRegistry::addTouchWatcher(int touchId, QQuickItem *watcher)
275+{
276+ #if TOUCHREGISTRY_DEBUG
277+ UG_DEBUG << "addTouchWatcher id" << touchId << "watcher" << watcher;
278+ #endif
279+
280+ Pool<TouchInfo>::Iterator touchInfo = findTouchInfo(touchId);
281+ if (!touchInfo) { qFatal("TouchRegistry: Failed to find TouchInfo"); }
282+
283+ // TODO: Check if watcher already exists
284+
285+ touchInfo->watchers.append(watcher);
286+}
287+
288+void TouchRegistry::removeCandidateOwnerForTouch(int id, QQuickItem *candidate)
289+{
290+ #if TOUCHREGISTRY_DEBUG
291+ UG_DEBUG << "removeCandidateOwnerForTouch id" << id << "candidate" << candidate;
292+ #endif
293+
294+ Pool<TouchInfo>::Iterator touchInfo = findTouchInfo(id);
295+ if (!touchInfo) { qFatal("TouchRegistry: Failed to find TouchInfo"); }
296+
297+ int indexRemoved = -1;
298+
299+ // TODO: check if the candidate is in fact the owner of the touch
300+
301+ for (int i = 0; i < touchInfo->candidates.count() && indexRemoved == -1; ++i) {
302+ CandidateInfo &candidateInfo = touchInfo->candidates[i];
303+ if (candidateInfo.item == candidate) {
304+ Q_ASSERT(i > 0 || candidateInfo.undecided);
305+ if (i == 0 && !candidateInfo.undecided) {
306+ qCritical("TouchRegistry: touch owner is being removed.");
307+ }
308+ delete candidateInfo.inactivityTimer;
309+ candidateInfo.inactivityTimer = nullptr;
310+ touchInfo->candidates.removeAt(i);
311+ indexRemoved = i;
312+ }
313+ }
314+
315+ if (indexRemoved == 0) {
316+ // the top candidate has been removed. if the new top candidate
317+ // wants the touch let him know he's now the owner.
318+ if (touchInfo->isOwned()) {
319+ touchInfo->notifyCandidatesOfOwnershipResolution();
320+ }
321+ }
322+
323+ if (!m_inDispatchLoop && touchInfo->ended()) {
324+ m_touchInfoPool.freeSlot(touchInfo);
325+ }
326+}
327+
328+void TouchRegistry::requestTouchOwnership(int id, QQuickItem *candidate)
329+{
330+ #if TOUCHREGISTRY_DEBUG
331+ UG_DEBUG << "requestTouchOwnership id " << id << "candidate" << candidate;
332+ #endif
333+
334+ Pool<TouchInfo>::Iterator touchInfo = findTouchInfo(id);
335+ if (!touchInfo) { qFatal("TouchRegistry: Failed to find TouchInfo"); }
336+
337+ Q_ASSERT(!touchInfo->isOwned());
338+
339+ int candidateIndex = -1;
340+ for (int i = 0; i < touchInfo->candidates.count(); ++i) {
341+ CandidateInfo &candidateInfo = touchInfo->candidates[i];
342+ if (candidateInfo.item == candidate) {
343+ candidateInfo.undecided = false;
344+ delete candidateInfo.inactivityTimer;
345+ candidateInfo.inactivityTimer = nullptr;
346+ candidateIndex = i;
347+ break;
348+ }
349+ }
350+
351+ // add it as a candidate if not present yet
352+ if (candidateIndex < 0) {
353+ CandidateInfo candidateInfo;
354+ candidateInfo.undecided = false;
355+ candidateInfo.item = candidate;
356+ candidateInfo.inactivityTimer = nullptr;
357+ touchInfo->candidates.append(candidateInfo);
358+ // it's the last one
359+ candidateIndex = touchInfo->candidates.count() - 1;
360+ }
361+
362+ // If it's the top candidate it means it's now the owner. Let
363+ // it know about it.
364+ if (candidateIndex == 0) {
365+ touchInfo->notifyCandidatesOfOwnershipResolution();
366+ }
367+}
368+
369+Pool<TouchRegistry::TouchInfo>::Iterator TouchRegistry::findTouchInfo(int id)
370+{
371+ Pool<TouchInfo>::Iterator touchInfo;
372+
373+ m_touchInfoPool.forEach([&](Pool<TouchInfo>::Iterator &someTouchInfo) -> bool {
374+ if (someTouchInfo->id == id) {
375+ touchInfo = someTouchInfo;
376+ return false;
377+ } else {
378+ return true;
379+ }
380+ });
381+
382+ return touchInfo;
383+}
384+
385+
386+void TouchRegistry::rejectCandidateOwnerForTouch(int id, QQuickItem *candidate)
387+{
388+ // NB: It's technically possible that candidate is a dangling pointer at this point.
389+ // Although that would most likely be due to a bug in our code.
390+ // In any case, only dereference it after it's confirmed that it indeed exists.
391+
392+ #if TOUCHREGISTRY_DEBUG
393+ UG_DEBUG << "rejectCandidateOwnerForTouch id" << id << "candidate" << (void*)candidate;
394+ #endif
395+
396+ Pool<TouchInfo>::Iterator touchInfo = findTouchInfo(id);
397+ if (!touchInfo) {
398+ #if TOUCHREGISTRY_DEBUG
399+ UG_DEBUG << "Failed to find TouchInfo for id" << id;
400+ #endif
401+ return;
402+ }
403+
404+ int rejectedCandidateIndex = -1;
405+
406+ // Check if the given candidate is valid and still undecided
407+ for (int i = 0; i < touchInfo->candidates.count() && rejectedCandidateIndex == -1; ++i) {
408+ CandidateInfo &candidateInfo = touchInfo->candidates[i];
409+ if (candidateInfo.item == candidate) {
410+ Q_ASSERT(i > 0 || candidateInfo.undecided);
411+ if (i == 0 && !candidateInfo.undecided) {
412+ qCritical() << "TouchRegistry: Can't reject item (" << (void*)candidate
413+ << ") as it already owns touch" << id;
414+ return;
415+ } else {
416+ // we found the guy and it's all fine.
417+ rejectedCandidateIndex = i;
418+ }
419+ }
420+ }
421+
422+ // If we reached this point it's because the given candidate exists and is indeed undecided.
423+
424+ Q_ASSERT(rejectedCandidateIndex >= 0 && rejectedCandidateIndex < touchInfo->candidates.size());
425+
426+ {
427+ TouchOwnershipEvent lostOwnershipEvent(id, false /*gained*/);
428+ QCoreApplication::sendEvent(candidate, &lostOwnershipEvent);
429+ }
430+
431+ touchInfo->candidates.removeAt(rejectedCandidateIndex);
432+
433+ if (rejectedCandidateIndex == 0) {
434+ // the top candidate has been removed. if the new top candidate
435+ // wants the touch let him know he's now the owner.
436+ if (touchInfo->isOwned()) {
437+ touchInfo->notifyCandidatesOfOwnershipResolution();
438+ }
439+ }
440+}
441+
442+////////////////////////////////////// TouchRegistry::TouchInfo ////////////////////////////////////
443+
444+TouchRegistry::TouchInfo::TouchInfo(int id)
445+{
446+ init(id);
447+}
448+
449+void TouchRegistry::TouchInfo::reset()
450+{
451+ id = -1;
452+
453+ for (int i = 0; i < candidates.count(); ++i) {
454+ CandidateInfo &candidate = candidates[i];
455+ delete candidate.inactivityTimer;
456+ candidate.inactivityTimer.clear(); // shoundn't be needed but anyway...
457+ }
458+}
459+
460+void TouchRegistry::TouchInfo::init(int id)
461+{
462+ this->id = id;
463+ physicallyEnded = false;
464+ candidates.clear();
465+ watchers.clear();
466+}
467+
468+bool TouchRegistry::TouchInfo::isOwned() const
469+{
470+ return !candidates.isEmpty() && !candidates.first().undecided;
471+}
472+
473+bool TouchRegistry::TouchInfo::ended() const
474+{
475+ Q_ASSERT(isValid());
476+ return physicallyEnded && (isOwned() || candidates.isEmpty());
477+}
478+
479+void TouchRegistry::TouchInfo::notifyCandidatesOfOwnershipResolution()
480+{
481+ Q_ASSERT(isOwned());
482+
483+ #if TOUCHREGISTRY_DEBUG
484+ UG_DEBUG << "sending TouchOwnershipEvent(id =" << id
485+ << " gained) to candidate" << candidates[0].item;
486+ #endif
487+
488+ TouchOwnershipEvent gainedOwnershipEvent(id, true /*gained*/);
489+ QCoreApplication::sendEvent(candidates[0].item, &gainedOwnershipEvent);
490+
491+
492+ TouchOwnershipEvent lostOwnershipEvent(id, false /*gained*/);
493+ for (int i = 1; i < candidates.count(); ++i) {
494+ #if TOUCHREGISTRY_DEBUG
495+ UG_DEBUG << "sending TouchWonershipEvent(id =" << id << " lost) to candidate"
496+ << candidates[i].item;
497+ #endif
498+ QCoreApplication::sendEvent(candidates[i].item, &lostOwnershipEvent);
499+ }
500+}
libs/UbuntuGestures/TouchRegistry.hnew file+182
@@ -0,0 +1,182 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef UNITY_TOUCHREGISTRY_H
18+#define UNITY_TOUCHREGISTRY_H
19+
20+#include <QQuickItem>
21+#include <QObject>
22+#include <QPointer>
23+#include <QTouchEvent>
24+#include <QVector>
25+
26+#include "UbuntuGesturesGlobal.h"
27+#include "CandidateInactivityTimer.h"
28+#include "Pool.h"
29+
30+namespace UbuntuGestures {
31+ class AbstractTimerFactory;
32+}
33+
34+/*
35+ Where the ownership of touches is registered.
36+
37+ Singleton used for adding a touch point ownership model analogous to the one
38+ described in the XInput 2.2 protocol[1] on top of the existing input dispatch logic in QQuickWindow.
39+
40+ It provides a much more flexible and powerful way of dealing with pointer ownership than the existing
41+ mechanisms in Qt. Namely QQuickItem::grabTouchPoints, QuickItem::keepTouchGrab,
42+ QQuickItem::setFiltersChildMouseEvents, QQuickItem::ungrabTouchPoints and QQuickItem::touchUngrabEvent.
43+
44+ Usage:
45+
46+ 1- An item receives a a new touch point. If he's not sure whether he wants it yet, he calls:
47+ TouchRegistry::instance()->addCandidateOwnerForTouch(touchId, this);
48+ touchEvent->ignore();
49+ Ignoring the event is crucial so that it can be seen by other interested parties, which will
50+ behave similarly.
51+
52+ 2- That item will then start receiving UnownedTouchEvents for that touch from step 1. Once he's
53+ made a decision he calls either:
54+ TouchRegistry::instance()->requestTouchOwnership(touchId, this);
55+ If he wants the touch point or:
56+ TouchRegistry::instance()->removeCandidateOwnerForTouch(touchId, this);
57+  if he does not want it.
58+
59+ Candidates are put in a priority queue. The first one to call addCandidateOwnerForTouch() will
60+ take precedence over the others for receiving ownership over the touch point (from now on called
61+ simply top-candidate).
62+
63+ If the top-candidate calls requestTouchOwnership() he will immediately receive a
64+ TouchOwnershipEvent(gained=true) for that touch point. He can then safely call
65+ QQuickItem::grabTouchPoints to actually get the owned touch points. The other candidates
66+ will receive TouchOwnershipEvent(gained=false) and will no longer receive UnownedTouchEvents
67+ for that touch point. They will have to undo whatever action they were performing with that
68+ touch point.
69+
70+ But if the top-candidate calls removeCandidateOwnerForTouch() instead, he's popped from the
71+ candidacy queue and ownership is given to the new top-most candidate if he has already
72+ made his decision, that is.
73+
74+ The TouchRegistry cannot enforce the results of this pointer ownership negotiation (i.e.,
75+ who gets to grab the touch points) as that would clash with QQuickWindow's input event
76+ dispatching logic. The candidates have to respect the decision and grab the touch points
77+ themselves.
78+
79+ If an item wants ownership over touches as soon as he receives the TouchBegin for them, his step 1
80+ would be instead:
81+ TouchRegistry::instance()->requestTouchOwnership(touchId, this);
82+ return true;
83+ He would then be notified once ownership has been granted to him, from which point onwards he could
84+ safely assume other TouchRegistry users wouldn't snatch this touch away from him.
85+
86+ Items oblivious to TouchRegistry will lose their touch points without warning, just like in plain Qt.
87+
88+ [1] - http://www.x.org/releases/X11R7.7/doc/inputproto/XI2proto.txt (see multitouch-ownership)
89+ */
90+class UBUNTUGESTURES_EXPORT TouchRegistry : public QObject
91+{
92+ Q_OBJECT
93+public:
94+ TouchRegistry(QObject *parent = nullptr);
95+ // Useful for tests, where you should feed a fake timer
96+ TouchRegistry(QObject *parent, UbuntuGestures::AbstractTimerFactory *timerFactory);
97+
98+ virtual ~TouchRegistry();
99+
100+ // Returns a pointer to the application's TouchRegistry instance.
101+ // If no instance has been allocated, null is returned.
102+ static TouchRegistry *instance() { return m_instance; }
103+
104+ void update(const QTouchEvent *event);
105+
106+ // Calls update() if the given event is a QTouchEvent
107+ bool eventFilter(QObject *watched, QEvent *event) override;
108+
109+ // An item that might later request ownership over the given touch point.
110+ // He will be kept informed about that touch point through UnownedTouchEvents
111+ // All candidates must eventually decide whether they want to own the touch point
112+ // or not. That decision is informed through requestTouchOwnership() or
113+ // removeCandidateOwnerForTouch()
114+ void addCandidateOwnerForTouch(int id, QQuickItem *candidate);
115+
116+ // The same as rejecting ownership of a touch
117+ void removeCandidateOwnerForTouch(int id, QQuickItem *candidate);
118+
119+ // The candidate object wants to be the owner of the touch with the given id.
120+ // If he's currently the oldest/top-most candidate, he will get an ownership
121+ // event immediately. If not, he will get ownership if (or once) he becomes the
122+ // top-most candidate.
123+ void requestTouchOwnership(int id, QQuickItem *candidate);
124+
125+ // An item that has no interest (effective or potential) in owning a touch point
126+ // but would nonetheless like to be kept up-to-date on its state.
127+ void addTouchWatcher(int touchId, QQuickItem *watcherItem);
128+
129+private Q_SLOTS:
130+ void rejectCandidateOwnerForTouch(int id, QQuickItem *candidate);
131+
132+private:
133+ class CandidateInfo {
134+ public:
135+ bool undecided;
136+ // TODO: Prune candidates that become null and resolve ownership accordingly.
137+ QPointer<QQuickItem> item;
138+ QPointer<UbuntuGestures::CandidateInactivityTimer> inactivityTimer;
139+ };
140+
141+ class TouchInfo {
142+ public:
143+ TouchInfo() : id(-1) {}
144+ TouchInfo(int id);
145+ bool isValid() const { return id >= 0; }
146+ void reset();
147+ void init(int id);
148+ int id;
149+ bool physicallyEnded;
150+ bool isOwned() const;
151+ bool ended() const;
152+ void notifyCandidatesOfOwnershipResolution();
153+
154+ // TODO optimize storage (s/QList/Pool)
155+ QList<CandidateInfo> candidates;
156+ QList<QPointer<QQuickItem>> watchers;
157+ };
158+
159+ Pool<TouchInfo>::Iterator findTouchInfo(int id);
160+
161+ void deliverTouchUpdatesToUndecidedCandidatesAndWatchers(const QTouchEvent *event);
162+
163+ static void translateTouchPointFromScreenToWindowCoords(QTouchEvent::TouchPoint &touchPoint);
164+
165+ static void dispatchPointsToItem(const QTouchEvent *event, const QList<int> &touchIds,
166+ QQuickItem *item);
167+ void freeEndedTouchInfos();
168+
169+ Pool<TouchInfo> m_touchInfoPool;
170+
171+ // the singleton instance
172+ static TouchRegistry *m_instance;
173+
174+ bool m_inDispatchLoop;
175+
176+ UbuntuGestures::AbstractTimerFactory *m_timerFactory;
177+
178+ friend class tst_TouchRegistry;
179+ friend class tst_DirectionalDragArea;
180+};
181+
182+#endif // UNITY_TOUCHREGISTRY_H
plugins/Ubuntu/Gestures/UbuntuGesturesGlobal.h → libs/UbuntuGestures/UbuntuGesturesGlobal.hrenamed+1−1
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013 Canonical, Ltd.
2+ * Copyright (C) 2014 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
libs/UbuntuGestures/UnownedTouchEvent.cppnew file+39
@@ -0,0 +1,39 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "UnownedTouchEvent.h"
18+
19+QEvent::Type UnownedTouchEvent::m_unownedTouchEventType = (QEvent::Type)-1;
20+
21+UnownedTouchEvent::UnownedTouchEvent(QTouchEvent *touchEvent)
22+ : QEvent(unownedTouchEventType())
23+ , m_touchEvent(touchEvent)
24+{
25+}
26+
27+QEvent::Type UnownedTouchEvent::unownedTouchEventType()
28+{
29+ if (m_unownedTouchEventType == (QEvent::Type)-1) {
30+ m_unownedTouchEventType = (QEvent::Type)registerEventType();
31+ }
32+
33+ return m_unownedTouchEventType;
34+}
35+
36+QTouchEvent *UnownedTouchEvent::touchEvent()
37+{
38+ return m_touchEvent.data();
39+}
libs/UbuntuGestures/UnownedTouchEvent.hnew file+45
@@ -0,0 +1,45 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef UBUNTU_UNOWNEDTOUCHEVENT_H
18+#define UBUNTU_UNOWNEDTOUCHEVENT_H
19+
20+#include <QTouchEvent>
21+#include <QScopedPointer>
22+#include "UbuntuGesturesGlobal.h"
23+
24+/*
25+ A touch event with touch points that do not belong the item receiving it.
26+
27+ See TouchRegistry::addCandidateOwnerForTouch and TouchRegistry::addTouchWatcher
28+ */
29+class UBUNTUGESTURES_EXPORT UnownedTouchEvent : public QEvent
30+{
31+public:
32+ UnownedTouchEvent(QTouchEvent *touchEvent);
33+ static Type unownedTouchEventType();
34+
35+ // TODO: It might be cleaner to store the information directly in UnownedTouchEvent
36+ // instead of carrying around a synthesized QTouchEvent. But the latter option
37+ // is very convenient.
38+ QTouchEvent *touchEvent();
39+
40+private:
41+ static Type m_unownedTouchEventType;
42+ QScopedPointer<QTouchEvent> m_touchEvent;
43+};
44+
45+#endif // UBUNTU_UNOWNEDTOUCHEVENT_H
plugins/Ubuntu/Gestures/AxisVelocityCalculator.h+2−2
@@ -21,7 +21,7 @@
2121#ifndef VELOCITY_CALCULATOR_H
2222#define VELOCITY_CALCULATOR_H
2323
24−#include "UbuntuGesturesGlobal.h"
24+#include "UbuntuGesturesQmlGlobal.h"
2525#include <stdint.h>
2626#include <QtCore/QObject>
2727#include "TimeSource.h"
@@ -49,7 +49,7 @@
4949 }
5050 }
5151 */
52−class UBUNTUGESTURES_EXPORT AxisVelocityCalculator : public QObject
52+class UBUNTUGESTURESQML_EXPORT AxisVelocityCalculator : public QObject
5353{
5454 Q_OBJECT
5555
plugins/Ubuntu/Gestures/CMakeLists.txt+24−5
@@ -1,19 +1,38 @@
1−set(UbuntuGestureQml_SOURCES
1+# in order to include Qt's private headers
2+remove_definitions(-DQT_NO_KEYWORDS)
3+
4+set(UbuntuGesturesQml_SOURCES
25 plugin.cpp
36 AxisVelocityCalculator.cpp
47 Direction.cpp
58 DirectionalDragArea.cpp
69 PressedOutsideNotifier.cpp
710 TimeSource.cpp
11+ TouchGate.cpp
812)
913
10−add_definitions(-DUBUNTUGESTURES_LIBRARY)
14+add_definitions(-DUBUNTUGESTURESQML_LIBRARY)
1115
12−add_library(UbuntuGestureQml MODULE ${UbuntuGestureQml_SOURCES})
16+add_library(UbuntuGesturesQml MODULE ${UbuntuGesturesQml_SOURCES})
17+target_link_libraries(UbuntuGesturesQml UbuntuGestures)
1318
14−qt5_use_modules(UbuntuGestureQml Core Quick)
19+qt5_use_modules(UbuntuGesturesQml Core Quick)
1520
1621# So that Foo.cpp can #include "Foo.moc"
1722include_directories(${CMAKE_CURRENT_BINARY_DIR})
1823
19−add_unity8_plugin(Ubuntu.Gestures 0.1 Ubuntu/Gestures TARGETS UbuntuGestureQml)
24+include_directories(${CMAKE_SOURCE_DIR}/libs/UbuntuGestures)
25+
26+# There's no cmake var for v8 include path :-/ so create one
27+LIST(GET Qt5Core_INCLUDE_DIRS 0 QtCoreDir0)
28+SET(Qt5V8_PRIVATE_INCLUDE_DIR ${QtCoreDir0}/QtV8/${Qt5Core_VERSION_STRING}/QtV8)
29+
30+# DANGER! DANGER! Using Qt's private API!
31+include_directories(
32+ ${Qt5Qml_PRIVATE_INCLUDE_DIRS}
33+ ${Qt5Quick_INCLUDE_DIRS}
34+ ${Qt5Quick_PRIVATE_INCLUDE_DIRS}
35+ ${Qt5V8_PRIVATE_INCLUDE_DIR}
36+)
37+
38+add_unity8_plugin(Ubuntu.Gestures 0.1 Ubuntu/Gestures TARGETS UbuntuGesturesQml)
plugins/Ubuntu/Gestures/Direction.h+2−2
@@ -17,14 +17,14 @@
1717#ifndef DIRECTION_H
1818#define DIRECTION_H
1919
20−#include "UbuntuGesturesGlobal.h"
20+#include "UbuntuGesturesQmlGlobal.h"
2121#include <QObject>
2222
2323/*
2424 A Direction enum wrapper so that we can do things like "direction: Direction.Righwards"
2525 from QML.
2626 */
27−class UBUNTUGESTURES_EXPORT Direction : public QObject {
27+class UBUNTUGESTURESQML_EXPORT Direction : public QObject {
2828 Q_OBJECT
2929 Q_ENUMS(Type)
3030
plugins/Ubuntu/Gestures/DirectionalDragArea.cpp+317−191
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013 Canonical, Ltd.
2+ * Copyright (C) 2013-2014 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -14,64 +14,32 @@
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
1515 */
1616
17+#define ACTIVETOUCHESINFO_DEBUG 0
18+#define DIRECTIONALDRAGAREA_DEBUG 0
19+
1720#include "DirectionalDragArea.h"
1821
22+#include <QQuickWindow>
1923#include <QtCore/qmath.h>
20−#include <QtCore/QTimer>
2124#include <QDebug>
2225
23−using namespace UbuntuGestures;
26+#pragma GCC diagnostic push
27+#pragma GCC diagnostic ignored "-pedantic"
28+#include <private/qquickwindow_p.h>
29+#pragma GCC diagnostic pop
2430
25−#define DIRECTIONALDRAGAREA_DEBUG 0
31+// local
32+#include "TouchOwnershipEvent.h"
33+#include "TouchRegistry.h"
34+#include "UnownedTouchEvent.h"
35+
36+using namespace UbuntuGestures;
2637
2738#if DIRECTIONALDRAGAREA_DEBUG
2839#define DDA_DEBUG(msg) qDebug("[DDA] " msg)
29−namespace {
30−QString touchPointStateToString(Qt::TouchPointState state) {
31− switch (state) {
32− case Qt::TouchPointPressed:
33− return QString("pressed");
34− case Qt::TouchPointMoved:
35− return QString("moved");
36− case Qt::TouchPointStationary:
37− return QString("stationary");
38− default: // Qt::TouchPointReleased:
39− return QString("released");
40− }
41−}
42−QString touchEventToString(QTouchEvent *ev)
43−{
44− QString message;
45−
46− switch (ev->type()) {
47− case QEvent::TouchBegin:
48− message.append("TouchBegin ");
49− break;
50− case QEvent::TouchUpdate:
51− message.append("TouchUpdate ");
52− break;
53− case QEvent::TouchEnd:
54− message.append("TouchEnd ");
55− break;
56− default: //QEvent::TouchCancel
57− message.append("TouchCancel ");
58− }
59−
60− for (int i=0; i < ev->touchPoints().size(); ++i) {
61−
62− const QTouchEvent::TouchPoint& touchPoint = ev->touchPoints().at(i);
63− message.append(
64− QString("(id:%1, state:%2, scenePos:(%3,%4)) ")
65− .arg(touchPoint.id())
66− .arg(touchPointStateToString(touchPoint.state()))
67− .arg(touchPoint.scenePos().x())
68− .arg(touchPoint.scenePos().y())
69− );
70− }
71−
72− return message;
73−}
40+#include "DebugHelpers.h"
7441
42+namespace {
7543const char *statusToString(DirectionalDragArea::Status status)
7644{
7745 if (status == DirectionalDragArea::WaitingForTouch) {
@@ -88,23 +56,6 @@ const char *statusToString(DirectionalDragArea::Status status)
8856#define DDA_DEBUG(msg) do{}while(0)
8957#endif // DIRECTIONALDRAGAREA_DEBUG
9058
91−// Essentially a QTimer wrapper
92−class RecognitionTimer : public UbuntuGestures::AbstractTimer
93−{
94− Q_OBJECT
95−public:
96− RecognitionTimer(QObject *parent) : UbuntuGestures::AbstractTimer(parent) {
97− m_timer.setSingleShot(false);
98− connect(&m_timer, &QTimer::timeout,
99− this, &UbuntuGestures::AbstractTimer::timeout);
100− }
101− virtual int interval() const { return m_timer.interval(); }
102− virtual void setInterval(int msecs) { m_timer.setInterval(msecs); }
103− virtual void start() { m_timer.start(); UbuntuGestures::AbstractTimer::start(); }
104− virtual void stop() { m_timer.stop(); UbuntuGestures::AbstractTimer::stop(); }
105−private:
106− QTimer m_timer;
107−};
10859
10960DirectionalDragArea::DirectionalDragArea(QQuickItem *parent)
11061 : QQuickItem(parent)
@@ -126,12 +77,14 @@ DirectionalDragArea::DirectionalDragArea(QQuickItem *parent)
12677 , m_timeSource(new RealTimeSource)
12778 , m_activeTouches(m_timeSource)
12879{
129− setRecognitionTimer(new RecognitionTimer(this));
80+ setRecognitionTimer(new Timer(this));
13081 m_recognitionTimer->setInterval(60);
82+ m_recognitionTimer->setSingleShot(false);
13183
13284 m_velocityCalculator = new AxisVelocityCalculator(this);
13385
134− connect(this, &QQuickItem::enabledChanged, this, &DirectionalDragArea::onEnabledChanged);
86+ connect(this, &QQuickItem::enabledChanged, this, &DirectionalDragArea::giveUpIfDisabledOrInvisible);
87+ connect(this, &QQuickItem::visibleChanged, this, &DirectionalDragArea::giveUpIfDisabledOrInvisible);
13588}
13689
13790Direction::Type DirectionalDragArea::direction() const
@@ -214,6 +167,7 @@ void DirectionalDragArea::setRecognitionTimer(UbuntuGestures::AbstractTimer *tim
214167{
215168 int interval = 0;
216169 bool timerWasRunning = false;
170+ bool wasSingleShot = false;
217171
218172 // can be null when called from the constructor
219173 if (m_recognitionTimer) {
@@ -226,6 +180,7 @@ void DirectionalDragArea::setRecognitionTimer(UbuntuGestures::AbstractTimer *tim
226180
227181 m_recognitionTimer = timer;
228182 timer->setInterval(interval);
183+ timer->setSingleShot(wasSingleShot);
229184 connect(timer, &UbuntuGestures::AbstractTimer::timeout,
230185 this, &DirectionalDragArea::checkSpeed);
231186 if (timerWasRunning) {
@@ -280,8 +235,144 @@ qreal DirectionalDragArea::touchSceneY() const
280235 return m_previousScenePos.y();
281236}
282237
238+bool DirectionalDragArea::event(QEvent *event)
239+{
240+ if (event->type() == TouchOwnershipEvent::touchOwnershipEventType()) {
241+ touchOwnershipEvent(static_cast<TouchOwnershipEvent *>(event));
242+ return true;
243+ } else if (event->type() == UnownedTouchEvent::unownedTouchEventType()) {
244+ unownedTouchEvent(static_cast<UnownedTouchEvent *>(event));
245+ return true;
246+ } else {
247+ return QQuickItem::event(event);
248+ }
249+}
250+
251+void DirectionalDragArea::touchOwnershipEvent(TouchOwnershipEvent *event)
252+{
253+ if (event->gained()) {
254+ QVector<int> ids;
255+ ids.append(event->touchId());
256+ DDA_DEBUG("grabbing touch");
257+ grabTouchPoints(ids);
258+
259+ // Work around for Qt bug. If we grab a touch that is being used for mouse pointer
260+ // emulation it will cause the emulation logic to go nuts.
261+ // Thus we have to also grab the mouse in this case.
262+ // TODO: Report bug to Qt
263+ if (window()) {
264+ QQuickWindowPrivate *windowPrivate = QQuickWindowPrivate::get(window());
265+ if (windowPrivate->touchMouseId == event->touchId() && window()->mouseGrabberItem()) {
266+ DDA_DEBUG("removing mouse grabber");
267+ window()->mouseGrabberItem()->ungrabMouse();
268+ }
269+ }
270+ } else {
271+ // We still wanna know when it ends for keeping the composition time window up-to-date
272+ TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
273+
274+ setStatus(WaitingForTouch);
275+ }
276+}
277+
278+void DirectionalDragArea::unownedTouchEvent(UnownedTouchEvent *unownedTouchEvent)
279+{
280+ QTouchEvent *event = unownedTouchEvent->touchEvent();
281+
282+ Q_ASSERT(!event->touchPointStates().testFlag(Qt::TouchPointPressed));
283+
284+ #if DIRECTIONALDRAGAREA_DEBUG
285+ // TODO Consider using qCDebug() when available (Qt 5.2)
286+ qDebug() << "[DDA] Unowned" << m_timeSource->msecsSinceReference()
287+ << qPrintable(touchEventToString(event));
288+ #endif
289+
290+ switch (m_status) {
291+ case WaitingForTouch:
292+ // do nothing
293+ break;
294+ case Undecided:
295+ Q_ASSERT(isEnabled() && isVisible());
296+ unownedTouchEvent_undecided(unownedTouchEvent);
297+ break;
298+ default: // Recognized:
299+ // do nothing
300+ break;
301+ }
302+
303+ m_activeTouches.update(event);
304+}
305+
306+void DirectionalDragArea::unownedTouchEvent_undecided(UnownedTouchEvent *unownedTouchEvent)
307+{
308+ const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(unownedTouchEvent->touchEvent());
309+ if (!touchPoint) {
310+ qCritical() << "DirectionalDragArea[status=Undecided]: touch " << m_touchId
311+ << "missing from UnownedTouchEvent without first reaching state Qt::TouchPointReleased. "
312+ "Considering it as released.";
313+
314+ TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
315+ setStatus(WaitingForTouch);
316+ return;
317+ }
318+
319+ const QPointF &touchScenePos = touchPoint->scenePos();
320+
321+ if (touchPoint->state() == Qt::TouchPointReleased) {
322+ // touch has ended before recognition concluded
323+ DDA_DEBUG("Touch has ended before recognition concluded");
324+ TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
325+ emitSignalIfTapped();
326+ setStatus(WaitingForTouch);
327+ return;
328+ }
329+
330+ m_previousDampedScenePos.setX(m_dampedScenePos.x());
331+ m_previousDampedScenePos.setY(m_dampedScenePos.y());
332+ m_dampedScenePos.update(touchScenePos);
333+ updateVelocityCalculator(touchScenePos);
334+
335+ if (!pointInsideAllowedArea()) {
336+ DDA_DEBUG("Rejecting gesture because touch point is outside allowed area.");
337+ TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
338+ // We still wanna know when it ends for keeping the composition time window up-to-date
339+ TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
340+ setStatus(WaitingForTouch);
341+ return;
342+ }
343+
344+ if (!movingInRightDirection()) {
345+ DDA_DEBUG("Rejecting gesture because touch point is moving in the wrong direction.");
346+ TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
347+ // We still wanna know when it ends for keeping the composition time window up-to-date
348+ TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
349+ setStatus(WaitingForTouch);
350+ return;
351+ }
352+
353+ setPreviousPos(touchPoint->pos());
354+ setPreviousScenePos(touchScenePos);
355+
356+ if (isWithinTouchCompositionWindow()) {
357+ // There's still time for some new touch to appear and ruin our party as it would be combined
358+ // with our m_touchId one and therefore deny the possibility of a single-finger gesture.
359+ DDA_DEBUG("Sill within composition window. Let's wait more.");
360+ return;
361+ }
362+
363+ if (movedFarEnough(touchScenePos)) {
364+ TouchRegistry::instance()->requestTouchOwnership(m_touchId, this);
365+ setStatus(Recognized);
366+ } else {
367+ DDA_DEBUG("Didn't move far enough yet. Let's wait more.");
368+ }
369+}
370+
283371void DirectionalDragArea::touchEvent(QTouchEvent *event)
284372{
373+ // TODO: Consider when more than one touch starts in the same event (although it's not possible
374+ // with Mir's android-input). Have to track them all. Consider it a plus/bonus.
375+
285376 #if DIRECTIONALDRAGAREA_DEBUG
286377 // TODO Consider using qCDebug() when available (Qt 5.2)
287378 qDebug() << "[DDA]" << m_timeSource->msecsSinceReference()
@@ -310,29 +401,34 @@ void DirectionalDragArea::touchEvent(QTouchEvent *event)
310401
311402void DirectionalDragArea::touchEvent_absent(QTouchEvent *event)
312403{
404+ // TODO: accept/reject is for the whole event, not per touch id. See how that affects us.
405+
313406 if (!event->touchPointStates().testFlag(Qt::TouchPointPressed)) {
314407 // Nothing to see here. No touch starting in this event.
315408 return;
316409 }
317410
411+ // to be proven wrong, if that's the case
412+ bool allGood = true;
413+
318414 if (isWithinTouchCompositionWindow()) {
319415 // too close to the last touch start. So we consider them as starting roughly at the same time.
320416 // Can't be a single-touch gesture.
321417 #if DIRECTIONALDRAGAREA_DEBUG
322418 qDebug("[DDA] A new touch point came in but we're still within time composition window. Ignoring it.");
323419 #endif
324− return;
420+ allGood = false;
325421 }
326422
327423 const QList<QTouchEvent::TouchPoint> &touchPoints = event->touchPoints();
328424
329425 const QTouchEvent::TouchPoint *newTouchPoint = nullptr;
330− for (int i = 0; i < touchPoints.count(); ++i) {
426+ for (int i = 0; i < touchPoints.count() && allGood; ++i) {
331427 const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
332428 if (touchPoint.state() == Qt::TouchPointPressed) {
333429 if (newTouchPoint) {
334430 // more than one touch starting in this QTouchEvent. Can't be a single-touch gesture
335− return;
431+ allGood = false;
336432 } else {
337433 // that's our candidate
338434 m_touchId = touchPoint.id();
@@ -341,85 +437,61 @@ void DirectionalDragArea::touchEvent_absent(QTouchEvent *event)
341437 }
342438 }
343439
344− Q_ASSERT(newTouchPoint);
440+ if (allGood) {
441+ Q_ASSERT(newTouchPoint);
345442
346− // If we have made this far, we are good to go to the next status.
347−
348− m_startPos = newTouchPoint->pos();
349− m_startScenePos = newTouchPoint->scenePos();
350− m_touchId = newTouchPoint->id();
351− m_dampedScenePos.reset(m_startScenePos);
352− m_velocityCalculator->setTrackedPosition(0.);
353− m_velocityCalculator->reset();
354− m_numSamplesOnLastSpeedCheck = 0;
355− m_silenceTime = 0;
356− setPreviousPos(m_startPos);
357− setPreviousScenePos(m_startScenePos);
358− updateSceneDirectionVector();
359−
360− setStatus(Undecided);
443+ m_startPos = newTouchPoint->pos();
444+ m_startScenePos = newTouchPoint->scenePos();
445+ m_touchId = newTouchPoint->id();
446+ m_dampedScenePos.reset(m_startScenePos);
447+ m_velocityCalculator->setTrackedPosition(0.);
448+ m_velocityCalculator->reset();
449+ m_numSamplesOnLastSpeedCheck = 0;
450+ m_silenceTime = 0;
451+ setPreviousPos(m_startPos);
452+ setPreviousScenePos(m_startScenePos);
453+ updateSceneDirectionVector();
454+
455+ if (recognitionIsDisabled()) {
456+ // Behave like a dumb TouchArea
457+ TouchRegistry::instance()->requestTouchOwnership(m_touchId, this);
458+ setStatus(Recognized);
459+ event->accept();
460+ } else {
461+ // just monitor the touch points for now.
462+ TouchRegistry::instance()->addCandidateOwnerForTouch(m_touchId, this);
463+
464+ setStatus(Undecided);
465+ // Let the item below have it. We will monitor it and grab it later if a gesture
466+ // gets recognized.
467+ event->ignore();
468+ }
469+ } else {
470+ watchPressedTouchPoints(touchPoints);
471+ event->ignore();
472+ }
361473}
362474
363475void DirectionalDragArea::touchEvent_undecided(QTouchEvent *event)
364476{
365− const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event);
477+ Q_ASSERT(event->type() == QEvent::TouchBegin);
478+ Q_ASSERT(fetchTargetTouchPoint(event) == nullptr);
366479
367− if (!touchPoint) {
368− qCritical() << "DirectionalDragArea[status=Undecided]: touch " << m_touchId
369− << "missing from QTouchEvent without first reaching state Qt::TouchPointReleased. "
370− "Considering it as released.";
371− setStatus(WaitingForTouch);
372− return;
373− }
374−
375− const QPointF &touchScenePos = touchPoint->scenePos();
376−
377− if (touchPoint->state() == Qt::TouchPointReleased) {
378− // touch has ended before recognition concluded
379− DDA_DEBUG("Touch has ended before recognition concluded");
380− setStatus(WaitingForTouch);
381− Q_EMIT tapped();
382− return;
383− }
480+ // We're not interested in new touch points. We already have our candidate (m_touchId).
481+ // But we do want to know when those new touches end for keeping the composition time
482+ // window up-to-date
483+ event->ignore();
484+ watchPressedTouchPoints(event->touchPoints());
384485
385486 if (event->touchPointStates().testFlag(Qt::TouchPointPressed) && isWithinTouchCompositionWindow()) {
386487 // multi-finger drags are not accepted
387488 DDA_DEBUG("Multi-finger drags are not accepted");
388− setStatus(WaitingForTouch);
389− return;
390− }
391489
392− m_previousDampedScenePos.setX(m_dampedScenePos.x());
393− m_previousDampedScenePos.setY(m_dampedScenePos.y());
394− m_dampedScenePos.update(touchScenePos);
395− updateVelocityCalculator(touchScenePos);
396−
397− if (!pointInsideAllowedArea()) {
398− DDA_DEBUG("Rejecting gesture because touch point is outside allowed area.");
399− setStatus(WaitingForTouch);
400− return;
401− }
490+ TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
491+ // We still wanna know when it ends for keeping the composition time window up-to-date
492+ TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
402493
403− if (!movingInRightDirection()) {
404− DDA_DEBUG("Rejecting gesture becauuse touch point is moving in the wrong direction.");
405494 setStatus(WaitingForTouch);
406− return;
407− }
408−
409− setPreviousPos(touchPoint->pos());
410− setPreviousScenePos(touchScenePos);
411−
412− if (isWithinTouchCompositionWindow()) {
413− // There's still time for some new touch to appear and ruin our party as it would be combined
414− // with our m_touchId one and therefore deny the possibility of a single-finger gesture.
415− DDA_DEBUG("Sill within composition window. Let's wait more.");
416− return;
417− }
418−
419− if (movedFarEnough(touchScenePos)) {
420− setStatus(Recognized);
421− } else {
422− DDA_DEBUG("Didn't move far enough yet. Let's wait more.");
423495 }
424496}
425497
@@ -437,11 +509,35 @@ void DirectionalDragArea::touchEvent_recognized(QTouchEvent *event)
437509 setPreviousScenePos(touchPoint->scenePos());
438510
439511 if (touchPoint->state() == Qt::TouchPointReleased) {
512+ emitSignalIfTapped();
440513 setStatus(WaitingForTouch);
441514 }
442515 }
443516}
444517
518+void DirectionalDragArea::watchPressedTouchPoints(const QList<QTouchEvent::TouchPoint> &touchPoints)
519+{
520+ for (int i = 0; i < touchPoints.count(); ++i) {
521+ const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
522+ if (touchPoint.state() == Qt::TouchPointPressed) {
523+ TouchRegistry::instance()->addTouchWatcher(touchPoint.id(), this);
524+ }
525+ }
526+}
527+
528+bool DirectionalDragArea::recognitionIsDisabled() const
529+{
530+ return distanceThreshold() <= 0 && compositionTime() <= 0;
531+}
532+
533+void DirectionalDragArea::emitSignalIfTapped()
534+{
535+ qint64 touchDuration = m_timeSource->msecsSinceReference() - m_activeTouches.touchStartTime(m_touchId);
536+ if (touchDuration <= maxTapDuration()) {
537+ Q_EMIT tapped();
538+ }
539+}
540+
445541const QTouchEvent::TouchPoint *DirectionalDragArea::fetchTargetTouchPoint(QTouchEvent *event)
446542{
447543 const QList<QTouchEvent::TouchPoint> &touchPoints = event->touchPoints();
@@ -509,12 +605,16 @@ bool DirectionalDragArea::movedFarEnough(const QPointF &point) const
509605
510606void DirectionalDragArea::checkSpeed()
511607{
608+ Q_ASSERT(m_status == Undecided);
609+
512610 if (m_velocityCalculator->numSamples() >= AxisVelocityCalculator::MIN_SAMPLES_NEEDED) {
513611 qreal speed = qFabs(m_velocityCalculator->calculate());
514612 qreal minSpeedMsecs = m_minSpeed / 1000.0;
515613
516614 if (speed < minSpeedMsecs) {
517615 DDA_DEBUG("Rejecting gesture because it's below minimum speed.");
616+ TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
617+ TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
518618 setStatus(WaitingForTouch);
519619 }
520620 }
@@ -523,7 +623,9 @@ void DirectionalDragArea::checkSpeed()
523623 m_silenceTime += m_recognitionTimer->interval();
524624
525625 if (m_silenceTime > m_maxSilenceTime) {
526− DDA_DEBUG("Rejecting gesture because it's silence time has been exceeded.");
626+ DDA_DEBUG("Rejecting gesture because its silence time has been exceeded.");
627+ TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
628+ TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
527629 setStatus(WaitingForTouch);
528630 }
529631 } else {
@@ -532,10 +634,19 @@ void DirectionalDragArea::checkSpeed()
532634 m_numSamplesOnLastSpeedCheck = m_velocityCalculator->numSamples();
533635}
534636
535−void DirectionalDragArea::onEnabledChanged()
637+void DirectionalDragArea::giveUpIfDisabledOrInvisible()
536638{
537− if (!isEnabled() && m_status != WaitingForTouch) {
538− setStatus(WaitingForTouch);
639+ if (!isEnabled() || !isVisible()) {
640+ if (m_status == Undecided) {
641+ TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
642+ // We still wanna know when it ends for keeping the composition time window up-to-date
643+ TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
644+ }
645+
646+ if (m_status != WaitingForTouch) {
647+ DDA_DEBUG("Resetting status because got disabled or made invisible");
648+ setStatus(WaitingForTouch);
649+ }
539650 }
540651}
541652
@@ -631,7 +742,9 @@ void DirectionalDragArea::updateVelocityCalculator(const QPointF &scenePos)
631742
632743bool DirectionalDragArea::isWithinTouchCompositionWindow()
633744{
634− return !m_activeTouches.isEmpty() &&
745+ return
746+ compositionTime() > 0 &&
747+ !m_activeTouches.isEmpty() &&
635748 m_timeSource->msecsSinceReference() <=
636749 m_activeTouches.mostRecentStartTime() + (qint64)compositionTime();
637750}
@@ -639,18 +752,17 @@ bool DirectionalDragArea::isWithinTouchCompositionWindow()
639752//************************** ActiveTouchesInfo **************************
640753
641754DirectionalDragArea::ActiveTouchesInfo::ActiveTouchesInfo(const SharedTimeSource &timeSource)
642− : m_timeSource(timeSource), m_lastUsedIndex(-1)
755+ : m_timeSource(timeSource)
643756{
644− // Estimate of the maximum number of active touches we might reach.
645− // Not a problem if it ends up being an underestimate as this is just
646− // an optimization.
647− m_vector.resize(3);
648757}
649758
650759void DirectionalDragArea::ActiveTouchesInfo::update(QTouchEvent *event)
651760{
652761 if (!(event->touchPointStates() & (Qt::TouchPointPressed | Qt::TouchPointReleased))) {
653762 // nothing to update
763+ #if ACTIVETOUCHESINFO_DEBUG
764+ qDebug("[DDA::ActiveTouchesInfo] Nothing to Update");
765+ #endif
654766 return;
655767 }
656768
@@ -658,74 +770,88 @@ void DirectionalDragArea::ActiveTouchesInfo::update(QTouchEvent *event)
658770 for (int i = 0; i < touchPoints.count(); ++i) {
659771 const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
660772 if (touchPoint.state() == Qt::TouchPointPressed) {
661− addTouchPoint(touchPoint);
773+ addTouchPoint(touchPoint.id());
662774 } else if (touchPoint.state() == Qt::TouchPointReleased) {
663− removeTouchPoint(touchPoint);
775+ removeTouchPoint(touchPoint.id());
664776 }
665777 }
666778}
667779
668−void DirectionalDragArea::ActiveTouchesInfo::addTouchPoint(const QTouchEvent::TouchPoint &touchPoint)
780+#if ACTIVETOUCHESINFO_DEBUG
781+QString DirectionalDragArea::ActiveTouchesInfo::toString()
669782{
670− ActiveTouchInfo &activeTouchInfo = getEmptySlot();
671− activeTouchInfo.id = touchPoint.id();
672− activeTouchInfo.startTime = m_timeSource->msecsSinceReference();
783+ QString string = "(";
784+
785+ {
786+ QTextStream stream(&string);
787+ m_touchInfoPool.forEach([&](Pool<ActiveTouchInfo>::Iterator &touchInfo) {
788+ stream << "(id=" << touchInfo->id << ",startTime=" << touchInfo->startTime << ")";
789+ return true;
790+ });
791+ }
792+
793+ string.append(")");
794+
795+ return string;
673796}
797+#endif // ACTIVETOUCHESINFO_DEBUG
674798
675−void DirectionalDragArea::ActiveTouchesInfo::removeTouchPoint(const QTouchEvent::TouchPoint &touchPoint)
799+void DirectionalDragArea::ActiveTouchesInfo::addTouchPoint(int touchId)
676800{
677− for (int i = 0; i <= m_lastUsedIndex; ++i) {
678− if (touchPoint.id() == m_vector.at(i).id) {
679− freeSlot(i);
680− return;
681− }
682− }
683− Q_ASSERT(false); // shouldn't reach this point
801+ ActiveTouchInfo &activeTouchInfo = m_touchInfoPool.getEmptySlot();
802+ activeTouchInfo.id = touchId;
803+ activeTouchInfo.startTime = m_timeSource->msecsSinceReference();
804+
805+ #if ACTIVETOUCHESINFO_DEBUG
806+ qDebug() << "[DDA::ActiveTouchesInfo]" << qPrintable(toString());
807+ #endif
684808}
685809
686−DirectionalDragArea::ActiveTouchInfo &DirectionalDragArea::ActiveTouchesInfo::getEmptySlot()
810+qint64 DirectionalDragArea::ActiveTouchesInfo::touchStartTime(int touchId)
687811{
688− Q_ASSERT(m_lastUsedIndex < m_vector.size());
812+ qint64 result = -1;
689813
690− // Look for an in-between vacancy first
691− for (int i = 0; i < m_lastUsedIndex; ++i) {
692− ActiveTouchInfo &activeTouchInfo = m_vector[i];
693− if (!activeTouchInfo.isValid()) {
694− return activeTouchInfo;
814+ m_touchInfoPool.forEach([&](Pool<ActiveTouchInfo>::Iterator &touchInfo) {
815+ if (touchId == touchInfo->id) {
816+ result = touchInfo->startTime;
817+ return false;
818+ } else {
819+ return true;
695820 }
696− }
697−
698− ++m_lastUsedIndex;
699− if (m_lastUsedIndex >= m_vector.size()) {
700− m_vector.resize(m_lastUsedIndex + 1);
701− }
821+ });
702822
703− return m_vector[m_lastUsedIndex];
823+ Q_ASSERT(result != -1);
824+ return result;
704825}
705826
706−void DirectionalDragArea::ActiveTouchesInfo::freeSlot(int index)
827+void DirectionalDragArea::ActiveTouchesInfo::removeTouchPoint(int touchId)
707828{
708− m_vector[index].reset();
709− if (index == m_lastUsedIndex) {
710− do {
711− --m_lastUsedIndex;
712− } while (m_lastUsedIndex >= 0 && !m_vector.at(m_lastUsedIndex).isValid());
713− }
829+ m_touchInfoPool.forEach([&](Pool<ActiveTouchInfo>::Iterator &touchInfo) {
830+ if (touchId == touchInfo->id) {
831+ m_touchInfoPool.freeSlot(touchInfo);
832+ return false;
833+ } else {
834+ return true;
835+ }
836+ });
837+
838+ #if ACTIVETOUCHESINFO_DEBUG
839+ qDebug() << "[DDA::ActiveTouchesInfo]" << qPrintable(toString());
840+ #endif
714841}
715842
716843qint64 DirectionalDragArea::ActiveTouchesInfo::mostRecentStartTime()
717844{
718− Q_ASSERT(m_lastUsedIndex >= 0);
845+ Q_ASSERT(!m_touchInfoPool.isEmpty());
846+
847+ qint64 highestStartTime = -1;
719848
720− qint64 highestStartTime = m_vector.at(0).startTime;
721− int i = 1;
722− do {
723− const ActiveTouchInfo &activeTouchInfo = m_vector.at(i);
724− if (activeTouchInfo.isValid() && activeTouchInfo.startTime > highestStartTime) {
725− highestStartTime = activeTouchInfo.startTime;
849+ m_touchInfoPool.forEach([&](Pool<ActiveTouchInfo>::Iterator &activeTouchInfo) {
850+ if (activeTouchInfo->startTime > highestStartTime) {
851+ highestStartTime = activeTouchInfo->startTime;
726852 }
727− ++i;
728− } while (i < m_lastUsedIndex);
853+ return true;
854+ });
729855
730856 return highestStartTime;
731857}
plugins/Ubuntu/Gestures/DirectionalDragArea.h+37−29
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013 Canonical, Ltd.
2+ * Copyright (C) 2013,2014 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -19,27 +19,16 @@
1919
2020#include <QtQuick/QQuickItem>
2121#include "AxisVelocityCalculator.h"
22−#include "UbuntuGesturesGlobal.h"
22+#include "UbuntuGesturesQmlGlobal.h"
2323#include "Damper.h"
2424#include "Direction.h"
2525
26−namespace UbuntuGestures {
27−/* Defines an interface for a Timer. */
28−class UBUNTUGESTURES_EXPORT AbstractTimer : public QObject {
29− Q_OBJECT
30−public:
31− AbstractTimer(QObject *parent) : QObject(parent), m_isRunning(false) {}
32− virtual int interval() const = 0;
33− virtual void setInterval(int msecs) = 0;
34− virtual void start() { m_isRunning = true; };
35− virtual void stop() { m_isRunning = false; }
36− bool isRunning() const { return m_isRunning; }
37−Q_SIGNALS:
38− void timeout();
39−private:
40− bool m_isRunning;
41−};
42−}
26+// lib UbuntuGestures
27+#include <Pool.h>
28+#include <Timer.h>
29+
30+class TouchOwnershipEvent;
31+class UnownedTouchEvent;
4332
4433/*
4534 An area that detects axis-aligned single-finger drag gestures
@@ -50,7 +39,7 @@ private:
5039
5140 See doc/DirectionalDragArea.svg
5241 */
53−class UBUNTUGESTURES_EXPORT DirectionalDragArea : public QQuickItem {
42+class UBUNTUGESTURESQML_EXPORT DirectionalDragArea : public QQuickItem {
5443 Q_OBJECT
5544
5645 // The direction in which the gesture should move in order to be recognized.
@@ -196,6 +185,12 @@ public:
196185 // Useful for testing, where a fake time source can be supplied
197186 void setTimeSource(const UbuntuGestures::SharedTimeSource &timeSource);
198187
188+ bool event(QEvent *e) override;
189+
190+ // Maximum time, in milliseconds, between a press and a release, for a touch
191+ // sequence to be considered a tap.
192+ int maxTapDuration() const { return 300; }
193+
199194Q_SIGNALS:
200195 void directionChanged(Direction::Type direction);
201196 void statusChanged(Status value);
@@ -212,6 +207,10 @@ Q_SIGNALS:
212207 void touchYChanged(qreal value);
213208 void touchSceneXChanged(qreal value);
214209 void touchSceneYChanged(qreal value);
210+
211+ // TODO: I would rather not have such signal as it has nothing to do with drag gestures.
212+ // Remove when no longer used or move its implementation to the QML code that uses it
213+ // See maxTapDuration()
215214 void tapped();
216215
217216protected:
@@ -219,7 +218,7 @@ protected:
219218
220219private Q_SLOTS:
221220 void checkSpeed();
222− void onEnabledChanged();
221+ void giveUpIfDisabledOrInvisible();
223222
224223private:
225224 void touchEvent_absent(QTouchEvent *event);
@@ -238,6 +237,12 @@ private:
238237 // returns the scalar projection between the given vector (in scene coordinates)
239238 // and m_sceneDirectionVector
240239 qreal projectOntoDirectionVector(const QPointF &sceneVector) const;
240+ void touchOwnershipEvent(TouchOwnershipEvent *event);
241+ void unownedTouchEvent(UnownedTouchEvent *event);
242+ void unownedTouchEvent_undecided(UnownedTouchEvent *unownedTouchEvent);
243+ void watchPressedTouchPoints(const QList<QTouchEvent::TouchPoint> &touchPoints);
244+ bool recognitionIsDisabled() const;
245+ void emitSignalIfTapped();
241246
242247 Status m_status;
243248
@@ -283,18 +288,21 @@ private:
283288 public:
284289 ActiveTouchesInfo(const UbuntuGestures::SharedTimeSource &timeSource);
285290 void update(QTouchEvent *event);
286− ActiveTouchInfo &touchInfo(int id);
291+ qint64 touchStartTime(int id);
292+ bool isEmpty() const { return m_touchInfoPool.isEmpty(); }
287293 qint64 mostRecentStartTime();
288294 UbuntuGestures::SharedTimeSource m_timeSource;
289− bool isEmpty() const { return m_lastUsedIndex == -1; }
290295 private:
291− void addTouchPoint(const QTouchEvent::TouchPoint &touchPoint);
292− ActiveTouchInfo &getEmptySlot();
293− void freeSlot(int index);
294− void removeTouchPoint(const QTouchEvent::TouchPoint &touchPoint);
295− QVector<struct ActiveTouchInfo> m_vector;
296− int m_lastUsedIndex;
296+ void addTouchPoint(int touchId);
297+ void removeTouchPoint(int touchId);
298+ #if ACTIVETOUCHESINFO_DEBUG
299+ QString toString();
300+ #endif
301+
302+ Pool<ActiveTouchInfo> m_touchInfoPool;
297303 } m_activeTouches;
304+
305+ friend class tst_DirectionalDragArea;
298306};
299307
300308#endif // DIRECTIONAL_DRAG_AREA_H
plugins/Ubuntu/Gestures/TimeSource.h+2−2
@@ -21,14 +21,14 @@
2121#ifndef UBUNTUGESTURES_TIMESOURCE_H
2222#define UBUNTUGESTURES_TIMESOURCE_H
2323
24−#include "UbuntuGesturesGlobal.h"
24+#include "UbuntuGesturesQmlGlobal.h"
2525#include <QSharedPointer>
2626
2727namespace UbuntuGestures {
2828/*
2929 Interface for a time source.
3030 */
31−class UBUNTUGESTURES_EXPORT TimeSource {
31+class UBUNTUGESTURESQML_EXPORT TimeSource {
3232public:
3333 virtual ~TimeSource() {}
3434 /* Returns the current time in milliseconds since some reference time in the past. */
plugins/Ubuntu/Gestures/TouchGate.cppnew file+347
@@ -0,0 +1,347 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "TouchGate.h"
18+
19+#include <QCoreApplication>
20+#include <QDebug>
21+
22+#include <TouchOwnershipEvent.h>
23+#include <TouchRegistry.h>
24+
25+#pragma GCC diagnostic push
26+#pragma GCC diagnostic ignored "-pedantic"
27+#include <private/qquickitem_p.h>
28+#pragma GCC diagnostic pop
29+
30+#if TOUCHGATE_DEBUG
31+#include <DebugHelpers.h>
32+#endif
33+
34+
35+bool TouchGate::event(QEvent *e)
36+{
37+ if (e->type() == TouchOwnershipEvent::touchOwnershipEventType()) {
38+ touchOwnershipEvent(static_cast<TouchOwnershipEvent *>(e));
39+ return true;
40+ } else {
41+ return QQuickItem::event(e);
42+ }
43+}
44+
45+void TouchGate::touchEvent(QTouchEvent *event)
46+{
47+ #if TOUCHGATE_DEBUG
48+ qDebug() << "[TouchGate] got touch event" << qPrintable(touchEventToString(event));
49+ #endif
50+ event->accept();
51+
52+ const QList<QTouchEvent::TouchPoint> &touchPoints = event->touchPoints();
53+ bool goodToGo = true;
54+ for (int i = 0; i < touchPoints.count(); ++i) {
55+ const QTouchEvent::TouchPoint &touchPoint = touchPoints[i];
56+
57+ if (touchPoint.state() == Qt::TouchPointPressed) {
58+ Q_ASSERT(!m_touchInfoMap.contains(touchPoint.id()));
59+ m_touchInfoMap[touchPoint.id()].ownership = OwnershipRequested;
60+ m_touchInfoMap[touchPoint.id()].ended = false;
61+ TouchRegistry::instance()->requestTouchOwnership(touchPoint.id(), this);
62+ }
63+
64+ goodToGo &= m_touchInfoMap.contains(touchPoint.id())
65+ && m_touchInfoMap[touchPoint.id()].ownership == OwnershipGranted;
66+
67+ if (touchPoint.state() == Qt::TouchPointReleased && m_touchInfoMap.contains(touchPoint.id())) {
68+ m_touchInfoMap[touchPoint.id()].ended = true;
69+ }
70+
71+ }
72+
73+ if (goodToGo) {
74+ if (m_storedEvents.isEmpty()) {
75+ // let it pass through
76+ dispatchTouchEventToTarget(event);
77+ } else {
78+ // Retain the event to ensure TouchGate dispatches them in order.
79+ // Otherwise the current event would come before the stored ones, which are older.
80+ #if TOUCHGATE_DEBUG
81+ qDebug("[TouchGate] Storing event because thouches %s are still pending ownership.",
82+ qPrintable(oldestPendingTouchIdsString()));
83+ #endif
84+ storeTouchEvent(event);
85+ }
86+ } else {
87+ // Retain events that have unowned touches
88+ storeTouchEvent(event);
89+ }
90+}
91+
92+void TouchGate::touchOwnershipEvent(TouchOwnershipEvent *event)
93+{
94+ // TODO: Optimization: batch those actions as TouchOwnershipEvents
95+ // might come one right after the other.
96+
97+ Q_ASSERT(m_touchInfoMap.contains(event->touchId()));
98+
99+ TouchInfo &touchInfo = m_touchInfoMap[event->touchId()];
100+
101+ if (event->gained()) {
102+ #if TOUCHGATE_DEBUG
103+ qDebug() << "[TouchGate] Got ownership of touch " << event->touchId();
104+ #endif
105+ touchInfo.ownership = OwnershipGranted;
106+ } else {
107+ #if TOUCHGATE_DEBUG
108+ qDebug() << "[TouchGate] Lost ownership of touch " << event->touchId();
109+ #endif
110+ m_touchInfoMap.remove(event->touchId());
111+ removeTouchFromStoredEvents(event->touchId());
112+ }
113+
114+ dispatchFullyOwnedEvents();
115+}
116+
117+bool TouchGate::isTouchPointOwned(int touchId) const
118+{
119+ return m_touchInfoMap[touchId].ownership == OwnershipGranted;
120+}
121+
122+void TouchGate::storeTouchEvent(const QTouchEvent *event)
123+{
124+ #if TOUCHGATE_DEBUG
125+ qDebug() << "[TouchGate] Storing" << qPrintable(touchEventToString(event));
126+ #endif
127+
128+ TouchEvent clonedEvent(event);
129+ m_storedEvents.append(std::move(clonedEvent));
130+}
131+
132+void TouchGate::removeTouchFromStoredEvents(int touchId)
133+{
134+ int i = 0;
135+ while (i < m_storedEvents.count()) {
136+ TouchEvent &event = m_storedEvents[i];
137+ bool removed = event.removeTouch(touchId);
138+
139+ if (removed && event.touchPoints.isEmpty()) {
140+ m_storedEvents.removeAt(i);
141+ } else {
142+ ++i;
143+ }
144+ }
145+}
146+
147+void TouchGate::dispatchFullyOwnedEvents()
148+{
149+ while (!m_storedEvents.isEmpty() && eventIsFullyOwned(m_storedEvents.first())) {
150+ TouchEvent event = m_storedEvents.takeFirst();
151+ dispatchTouchEventToTarget(event);
152+ }
153+}
154+
155+#if TOUCHGATE_DEBUG
156+QString TouchGate::oldestPendingTouchIdsString()
157+{
158+ Q_ASSERT(!m_storedEvents.isEmpty());
159+
160+ QString str;
161+
162+ const auto &touchPoints = m_storedEvents.first().touchPoints;
163+ for (int i = 0; i < touchPoints.count(); ++i) {
164+ if (!isTouchPointOwned(touchPoints[i].id())) {
165+ if (!str.isEmpty()) {
166+ str.append(", ");
167+ }
168+ str.append(QString::number(touchPoints[i].id()));
169+ }
170+ }
171+
172+ return str;
173+}
174+#endif
175+
176+bool TouchGate::eventIsFullyOwned(const TouchGate::TouchEvent &event) const
177+{
178+ for (int i = 0; i < event.touchPoints.count(); ++i) {
179+ if (!isTouchPointOwned(event.touchPoints[i].id())) {
180+ return false;
181+ }
182+ }
183+
184+ return true;
185+}
186+
187+void TouchGate::setTargetItem(QQuickItem *item)
188+{
189+ // TODO: changing the target item while dispatch of touch events is taking place will
190+ // create a mess
191+
192+ if (item == m_targetItem.data())
193+ return;
194+
195+ m_targetItem = item;
196+ Q_EMIT targetItemChanged(item);
197+}
198+
199+void TouchGate::dispatchTouchEventToTarget(const TouchEvent &event)
200+{
201+ dispatchTouchEventToTarget(event.eventType,
202+ event.device,
203+ event.modifiers,
204+ event.touchPoints,
205+ event.target,
206+ event.window,
207+ event.timestamp);
208+}
209+
210+void TouchGate::dispatchTouchEventToTarget(QTouchEvent* event)
211+{
212+ dispatchTouchEventToTarget(event->type(),
213+ event->device(),
214+ event->modifiers(),
215+ event->touchPoints(),
216+ event->target(),
217+ event->window(),
218+ event->timestamp());
219+}
220+
221+void TouchGate::dispatchTouchEventToTarget(QEvent::Type eventType,
222+ QTouchDevice *device,
223+ Qt::KeyboardModifiers modifiers,
224+ const QList<QTouchEvent::TouchPoint> &touchPoints,
225+ QObject *target,
226+ QWindow *window,
227+ ulong timestamp)
228+{
229+ removeTouchInfoForEndedTouches(touchPoints);
230+
231+ if (m_targetItem.isNull()) {
232+ qWarning("[TouchGate] Cannot dispatch touch event because target item is null");
233+ return;
234+ }
235+
236+ QQuickItem *targetItem = m_targetItem.data();
237+
238+ if (!targetItem->isEnabled() || !targetItem->isVisible()) {
239+ #if TOUCHGATE_DEBUG
240+ qDebug() << "[TouchGate] Cannot dispatch touch event to" << targetItem
241+ << "because it's disabled or invisible.";
242+ #endif
243+ return;
244+ }
245+
246+ // Map touch points to targetItem coordinates
247+ QList<QTouchEvent::TouchPoint> targetTouchPoints = touchPoints;
248+ transformTouchPoints(targetTouchPoints, QQuickItemPrivate::get(targetItem)->windowToItemTransform());
249+ QTouchEvent *eventForTargetItem = createQTouchEvent(eventType, device, modifiers, targetTouchPoints,
250+ target, window, timestamp);
251+
252+ #if TOUCHGATE_DEBUG
253+ qDebug() << "[TouchGate] dispatching" << qPrintable(touchEventToString(eventForTargetItem))
254+ << "to" << targetItem;
255+ #endif
256+
257+ QCoreApplication::sendEvent(targetItem, eventForTargetItem);
258+
259+ delete eventForTargetItem;
260+}
261+
262+// NB: From QQuickWindow
263+void TouchGate::transformTouchPoints(QList<QTouchEvent::TouchPoint> &touchPoints, const QTransform &transform)
264+{
265+ QMatrix4x4 transformMatrix(transform);
266+ for (int i=0; i<touchPoints.count(); i++) {
267+ QTouchEvent::TouchPoint &touchPoint = touchPoints[i];
268+ touchPoint.setRect(transform.mapRect(touchPoint.sceneRect()));
269+ touchPoint.setStartPos(transform.map(touchPoint.startScenePos()));
270+ touchPoint.setLastPos(transform.map(touchPoint.lastScenePos()));
271+ touchPoint.setVelocity(transformMatrix.mapVector(touchPoint.velocity()).toVector2D());
272+ }
273+}
274+
275+QTouchEvent *TouchGate::createQTouchEvent(QEvent::Type eventType,
276+ QTouchDevice *device,
277+ Qt::KeyboardModifiers modifiers,
278+ const QList<QTouchEvent::TouchPoint> &touchPoints,
279+ QObject *target,
280+ QWindow *window,
281+ ulong timestamp)
282+{
283+ Qt::TouchPointStates eventStates = 0;
284+ for (int i = 0; i < touchPoints.count(); i++)
285+ eventStates |= touchPoints[i].state();
286+ // if all points have the same state, set the event type accordingly
287+ switch (eventStates) {
288+ case Qt::TouchPointPressed:
289+ eventType = QEvent::TouchBegin;
290+ break;
291+ case Qt::TouchPointReleased:
292+ eventType = QEvent::TouchEnd;
293+ break;
294+ default:
295+ eventType = QEvent::TouchUpdate;
296+ break;
297+ }
298+
299+ QTouchEvent *touchEvent = new QTouchEvent(eventType);
300+ touchEvent->setWindow(window);
301+ touchEvent->setTarget(target);
302+ touchEvent->setDevice(device);
303+ touchEvent->setModifiers(modifiers);
304+ touchEvent->setTouchPoints(touchPoints);
305+ touchEvent->setTouchPointStates(eventStates);
306+ touchEvent->setTimestamp(timestamp);
307+ touchEvent->accept();
308+ return touchEvent;
309+}
310+
311+void TouchGate::removeTouchInfoForEndedTouches(const QList<QTouchEvent::TouchPoint> &touchPoints)
312+{
313+ for (int i = 0; i < touchPoints.size(); ++i) {\
314+ const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
315+
316+ if (touchPoint.state() == Qt::TouchPointReleased) {
317+ Q_ASSERT(m_touchInfoMap.contains(touchPoint.id()));
318+ Q_ASSERT(m_touchInfoMap[touchPoint.id()].ended);
319+ Q_ASSERT(m_touchInfoMap[touchPoint.id()].ownership == OwnershipGranted);
320+ m_touchInfoMap.remove(touchPoint.id());
321+ }
322+ }
323+}
324+
325+TouchGate::TouchEvent::TouchEvent(const QTouchEvent *event)
326+ : eventType(event->type())
327+ , device(event->device())
328+ , modifiers(event->modifiers())
329+ , touchPoints(event->touchPoints())
330+ , target(event->target())
331+ , window(event->window())
332+ , timestamp(event->timestamp())
333+{
334+}
335+
336+bool TouchGate::TouchEvent::removeTouch(int touchId)
337+{
338+ bool removed = false;
339+ for (int i = 0; i < touchPoints.count() && !removed; ++i) {
340+ if (touchPoints[i].id() == touchId) {
341+ touchPoints.removeAt(i);
342+ removed = true;
343+ }
344+ }
345+
346+ return removed;
347+}
plugins/Ubuntu/Gestures/TouchGate.hnew file+126
@@ -0,0 +1,126 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef UBUNTU_TOUCH_GATE_H
18+#define UBUNTU_TOUCH_GATE_H
19+
20+#include "UbuntuGesturesQmlGlobal.h"
21+
22+#include <QQuickItem>
23+#include <QList>
24+#include <QPointer>
25+#include <QMap>
26+
27+#define TOUCHGATE_DEBUG 0
28+
29+class TouchOwnershipEvent;
30+
31+/*
32+ Blocks the passage of events until ownership over the related touch points is granted.
33+
34+ Blocked touch events won't be discarded. Instead they will be buffered until ownership
35+ is granted. If ownership is given to another item, the event buffer is cleared.
36+
37+ A TouchGate is useful as a mediator for items that do not understand, or gracefully handle,
38+ touch canceling. By having a TouchGate in front of them you guarantee that only owned touches (i.e.,
39+ touches that won't be canceled later) reaches them.
40+ */
41+class UBUNTUGESTURESQML_EXPORT TouchGate : public QQuickItem {
42+ Q_OBJECT
43+
44+ // Item that's going to receive the touch events that make it through the gate.
45+ Q_PROPERTY(QQuickItem* targetItem READ targetItem WRITE setTargetItem NOTIFY targetItemChanged)
46+
47+public:
48+ bool event(QEvent *e) override;
49+
50+ QQuickItem *targetItem() { return m_targetItem; }
51+ void setTargetItem(QQuickItem *item);
52+
53+Q_SIGNALS:
54+ void targetItemChanged(QQuickItem *item);
55+
56+protected:
57+ void touchEvent(QTouchEvent *event) override;
58+private:
59+ class TouchEvent {
60+ public:
61+ TouchEvent(const QTouchEvent *event);
62+
63+ bool removeTouch(int touchId);
64+
65+ QEvent::Type eventType;
66+ QTouchDevice *device;
67+ Qt::KeyboardModifiers modifiers;
68+ QList<QTouchEvent::TouchPoint> touchPoints;
69+ QObject *target;
70+ QWindow *window;
71+ ulong timestamp;
72+ };
73+
74+ void touchOwnershipEvent(TouchOwnershipEvent *event);
75+ bool isTouchPointOwned(int touchId) const;
76+ void storeTouchEvent(const QTouchEvent *event);
77+ void removeTouchFromStoredEvents(int touchId);
78+ void dispatchFullyOwnedEvents();
79+ bool eventIsFullyOwned(const TouchEvent &event) const;
80+
81+ void dispatchTouchEventToTarget(const TouchEvent &event);
82+ void dispatchTouchEventToTarget(QTouchEvent* event);
83+ void dispatchTouchEventToTarget(
84+ QEvent::Type eventType,
85+ QTouchDevice *device,
86+ Qt::KeyboardModifiers modifiers,
87+ const QList<QTouchEvent::TouchPoint> &touchPoints,
88+ QObject *target,
89+ QWindow *window,
90+ ulong timestamp);
91+
92+ void transformTouchPoints(QList<QTouchEvent::TouchPoint> &touchPoints, const QTransform &transform);
93+ static QTouchEvent *createQTouchEvent(QEvent::Type eventType,
94+ QTouchDevice *device,
95+ Qt::KeyboardModifiers modifiers,
96+ const QList<QTouchEvent::TouchPoint> &touchPoints,
97+ QObject *target,
98+ QWindow *window,
99+ ulong timestamp);
100+ void removeTouchInfoForEndedTouches(const QList<QTouchEvent::TouchPoint> &touchPoints);
101+
102+ #if TOUCHGATE_DEBUG
103+ QString oldestPendingTouchIdsString();
104+ #endif
105+
106+ QList<TouchEvent> m_storedEvents;
107+
108+ enum {
109+ OwnershipUndefined,
110+ OwnershipRequested,
111+ OwnershipGranted,
112+ };
113+ class TouchInfo {
114+ public:
115+ TouchInfo() {ownership = OwnershipUndefined; ended = false;}
116+ int ownership;
117+ bool ended;
118+ };
119+ QMap<int, TouchInfo> m_touchInfoMap;
120+
121+ QPointer<QQuickItem> m_targetItem;
122+
123+ friend class tst_TouchGate;
124+};
125+
126+#endif // UBUNTU_TOUCH_GATE_H
plugins/Ubuntu/Gestures/UbuntuGesturesQmlGlobal.hnew file+23
@@ -0,0 +1,23 @@
1+/*
2+ * Copyright (C) 2013 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include <QtCore/QtGlobal>
18+
19+#if defined(UBUNTUGESTURESQML_LIBRARY)
20+# define UBUNTUGESTURESQML_EXPORT Q_DECL_EXPORT
21+#else
22+# define UBUNTUGESTURESQML_EXPORT Q_DECL_IMPORT
23+#endif
plugins/Ubuntu/Gestures/plugin.cpp+3−1
@@ -19,6 +19,7 @@
1919#include "Direction.h"
2020#include "DirectionalDragArea.h"
2121#include "PressedOutsideNotifier.h"
22+#include "TouchGate.h"
2223
2324#include <qqml.h>
2425
@@ -28,10 +29,11 @@ static QObject* directionSingleton(QQmlEngine* engine, QJSEngine* scriptEngine)
2829 return new Direction;
2930}
3031
31−void UbuntuGestureQmlPlugin::registerTypes(const char *uri)
32+void UbuntuGesturesQmlPlugin::registerTypes(const char *uri)
3233{
3334 qmlRegisterSingletonType<Direction>(uri, 0, 1, "Direction", directionSingleton);
3435 qmlRegisterType<DirectionalDragArea>(uri, 0, 1, "DirectionalDragArea");
3536 qmlRegisterType<AxisVelocityCalculator>(uri, 0, 1, "AxisVelocityCalculator");
3637 qmlRegisterType<PressedOutsideNotifier>(uri, 0, 1, "PressedOutsideNotifier");
38+ qmlRegisterType<TouchGate>(uri, 0, 1, "TouchGate");
3739}
plugins/Ubuntu/Gestures/plugin.h+1−1
@@ -19,7 +19,7 @@
1919
2020#include <QtQml/QQmlExtensionPlugin>
2121
22−class UbuntuGestureQmlPlugin : public QQmlExtensionPlugin
22+class UbuntuGesturesQmlPlugin : public QQmlExtensionPlugin
2323{
2424 Q_OBJECT
2525 Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface")
plugins/Ubuntu/Gestures/qmldir+1−1
@@ -1,3 +1,3 @@
11module Ubuntu.Gestures
2−plugin UbuntuGestureQml
2+plugin UbuntuGesturesQml
33typeinfo Gestures.qmltypes
qml/Components/DragHandle.qml+2−2
@@ -47,12 +47,12 @@ EdgeDragArea {
4747 id: dragArea
4848 objectName: "dragHandle"
4949
50− // Disable most of the gesture recognition parameters by default when hinting is used as
50+ // Disable gesture recognition by default when hinting is used as
5151 // it conflicts with the hinting idea.
52− // The only part we keep in this situation is that it must be a single-finger gesture.
5352 distanceThreshold: hintDisplacement > 0 ? 0 : defaultDistanceThreshold
5453 maxSilenceTime: hintDisplacement > 0 ? 60*60*1000 : defaultMaxSilenceTime
5554 maxDeviation: hintDisplacement > 0 ? 999999 : defaultMaxDeviation
55+ compositionTime: hintDisplacement > 0 ? 0 : defaultCompositionTime
5656
5757 property bool stretch: false
5858
qml/Components/EdgeDragArea.qml+2
@@ -34,6 +34,7 @@ DirectionalDragArea {
3434 distanceThreshold: defaultDistanceThreshold
3535 minSpeed: defaultMinSpeed
3636 maxSilenceTime: defaultMaxSilenceTime
37+ compositionTime: defaultCompositionTime
3738
3839 readonly property real defaultMaxDeviation: units.gu(3)
3940 readonly property real defaultWideningAngle: 50
@@ -41,4 +42,5 @@ DirectionalDragArea {
4142 // some people were getting false negatives with it enabled.
4243 readonly property real defaultMinSpeed: units.gu(0)
4344 readonly property int defaultMaxSilenceTime: 200
45+ readonly property int defaultCompositionTime: 60
4446}
qml/Components/InputMethod.qml+13
@@ -17,6 +17,7 @@
1717import QtQuick 2.0
1818import Unity.Application 0.1
1919import Ubuntu.Components 0.1
20+import Ubuntu.Gestures 0.1
2021
2122Item {
2223 id: root
@@ -25,6 +26,18 @@ Item {
2526
2627 property int transitionDuration: UbuntuAnimation.FastDuration
2728
29+ Binding { target: surface; property: "z"; value: 0 }
30+
31+ TouchGate {
32+ x: UbuntuKeyboardInfo.x
33+ y: UbuntuKeyboardInfo.y
34+ z: 1
35+ width: UbuntuKeyboardInfo.width
36+ height: UbuntuKeyboardInfo.height
37+
38+ targetItem: root.surface
39+ }
40+
2841 state: {
2942 if (surface && surface.state != MirSurfaceItem.Minimized) {
3043 return "shown";
qml/Dash/Dash.qml+29−7
@@ -207,6 +207,12 @@ Showable {
207207 }
208208 }
209209
210+ // This is to avoid the situation where a bottom-edge swipe would bring up the dash overview
211+ // (as expected) but would also cause the dash content flickable to move a bit, because
212+ // that flickable was getting the touch events while overviewDragHandle was still undecided
213+ // about whether that touch was indeed performing a directional drag gesture.
214+ forceNonInteractive: overviewDragHandle.status != DirectionalDragArea.WaitingForTouch
215+
210216 enabled: overviewController.progress == 0
211217 opacity: enabled ? 1 : 0
212218 }
@@ -363,16 +369,32 @@ Showable {
363369 height: units.gu(2)
364370
365371 onSceneDistanceChanged: {
366− if (overviewController.enableAnimation) {
367− dashContentCache.scheduleUpdate();
372+ if (status == DirectionalDragArea.Recognized && initialSceneDistance != -1) {
373+ if (overviewController.enableAnimation) {
374+ dashContentCache.scheduleUpdate();
375+ }
376+ overviewController.enableAnimation = false;
377+ var deltaDistance = sceneDistance - initialSceneDistance;
378+ overviewController.progress = Math.max(0, Math.min(1, deltaDistance / fullMovement));
368379 }
369− overviewController.enableAnimation = false;
370− overviewController.progress = Math.max(0, Math.min(1, sceneDistance / fullMovement));
371380 }
372381
373− onDraggingChanged: {
374− overviewController.enableAnimation = true;
375− overviewController.progress = (overviewController.progress > 0.7) ? 1 : 0;
382+ property int previousStatus: -1
383+ property int currentStatus: DirectionalDragArea.WaitingForTouch
384+ property real initialSceneDistance: -1
385+
386+ onStatusChanged: {
387+ previousStatus = currentStatus;
388+ currentStatus = status;
389+
390+ if (status == DirectionalDragArea.Recognized) {
391+ initialSceneDistance = sceneDistance;
392+ } else if (status == DirectionalDragArea.WaitingForTouch &&
393+ previousStatus == DirectionalDragArea.Recognized) {
394+ overviewController.enableAnimation = true;
395+ overviewController.progress = (overviewController.progress > 0.7) ? 1 : 0;
396+ initialSceneDistance = -1;
397+ }
376398 }
377399 }
378400
qml/Dash/DashContent.qml+4−1
@@ -24,6 +24,7 @@ import "../Components/Flickables" as Flickables
2424Item {
2525 id: dashContent
2626
27+ property bool forceNonInteractive: false
2728 property alias scopes: dashContentList.model
2829 readonly property alias currentIndex: dashContentList.currentIndex
2930 readonly property string currentScopeId: dashContentList.currentItem ? dashContentList.currentItem.scopeId : ""
@@ -102,7 +103,8 @@ Item {
102103 id: dashContentList
103104 objectName: "dashContentList"
104105
105− interactive: dashContent.scopes.loaded && currentItem && !currentItem.moving && !currentItem.navigationShown && !currentItem.subPageShown
106+ interactive: !dashContent.forceNonInteractive && dashContent.scopes.loaded && currentItem
107+ && !currentItem.moving && !currentItem.navigationShown && !currentItem.subPageShown
106108 anchors.fill: parent
107109 orientation: ListView.Horizontal
108110 boundsBehavior: Flickable.DragAndOvershootBounds
@@ -179,6 +181,7 @@ Item {
179181 item.paginationCount = Qt.binding(function() { return dashContentList.count } )
180182 item.paginationIndex = Qt.binding(function() { return dashContentList.currentIndex } )
181183 item.holdingList = dashContentList;
184+ item.forceNonInteractive = Qt.binding(function() { return dashContent.forceNonInteractive } )
182185 }
183186 Connections {
184187 target: isCurrent ? scope : null
qml/Dash/GenericScopeView.qml+2
@@ -26,6 +26,7 @@ FocusScope {
2626 id: scopeView
2727
2828 readonly property bool navigationShown: pageHeaderLoader.item ? pageHeaderLoader.item.bottomItem[0].openList : false
29+ property bool forceNonInteractive: false
2930 property var scope: null
3031 property SortFilterProxyModel categories: categoryFilter
3132 property bool isCurrent: false
@@ -149,6 +150,7 @@ FocusScope {
149150 ScopeListView {
150151 id: categoryView
151152 objectName: "categoryListView"
153+ interactive: !forceNonInteractive
152154
153155 x: subPageLoader.open ? -width : 0
154156 visible: x != -width
qml/Launcher/Launcher.qml+3−1
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013 Canonical, Ltd.
2+ * Copyright (C) 2013-2014 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -76,6 +76,7 @@ Item {
7676
7777 Timer {
7878 id: dismissTimer
79+ objectName: "dismissTimer"
7980 interval: 5000
8081 onTriggered: {
8182 if (!panel.preventHiding) {
@@ -218,6 +219,7 @@ Item {
218219
219220 EdgeDragArea {
220221 id: dragArea
222+ objectName: "launcherDragArea"
221223
222224 direction: Direction.Rightwards
223225
qml/Launcher/LauncherPanel.qml+1
@@ -46,6 +46,7 @@ Rectangle {
4646 }
4747
4848 Rectangle {
49+ objectName: "buttonShowDashHome"
4950 width: parent.width
5051 height: units.gu(7)
5152 color: UbuntuColors.orange
qml/Stages/ApplicationWindow.qml−18
@@ -61,14 +61,6 @@ Item {
6161 // Remove this when possible
6262 property bool surfaceInitialized: false
6363
64− function forceSurfaceActiveFocusIfReady() {
65− if (sessionContainer.surface !== null &&
66− sessionContainer.surface.focus &&
67− sessionContainer.surface.parent === sessionContainer.surfaceContainer &&
68− sessionContainer.surface.enabled) {
69− sessionContainer.surface.forceActiveFocus();
70− }
71− }
7264 }
7365
7466 Timer {
@@ -77,15 +69,6 @@ Item {
7769 onTriggered: { if (sessionContainer.surface) {d.surfaceInitialized = true;} }
7870 }
7971
80− Connections {
81− target: sessionContainer.surface
82− // FIXME: I would rather not need to do this, but currently it doesn't get
83− // active focus without it and I don't know why.
84− onFocusChanged: d.forceSurfaceActiveFocusIfReady();
85− onParentChanged: d.forceSurfaceActiveFocusIfReady();
86− onEnabledChanged: d.forceSurfaceActiveFocusIfReady();
87− }
88−
8972 Image {
9073 id: screenshotImage
9174 objectName: "screenshotImage"
@@ -132,7 +115,6 @@ Item {
132115 onSurfaceChanged: {
133116 if (sessionContainer.surface) {
134117 surfaceInitTimer.start();
135− d.forceSurfaceActiveFocusIfReady();
136118 } else {
137119 d.surfaceInitialized = false;
138120 }
qml/Stages/PhoneStage.qml+14−5
@@ -132,7 +132,7 @@ Rectangle {
132132
133133 // This indicates when the spreadView is active. That means, all the animations
134134 // are activated and tiles need to line up for the spread.
135− readonly property bool active: shiftedContentX > 0 || spreadDragArea.dragging
135+ readonly property bool active: shiftedContentX > 0 || spreadDragArea.status === DirectionalDragArea.Recognized
136136
137137 // The flickable needs to fill the screen in order to get touch events all over.
138138 // However, we don't want to the user to be able to scroll back all the way. For
@@ -419,6 +419,7 @@ Rectangle {
419419
420420 EdgeDragArea {
421421 id: spreadDragArea
422+ objectName: "spreadDragArea"
422423 direction: Direction.Leftwards
423424 enabled: spreadView.phase != 2 && root.spreadEnabled
424425
@@ -438,9 +439,10 @@ Rectangle {
438439 spreadView.phase = 0;
439440 spreadView.contentX = -spreadView.shift;
440441 }
441− if (dragging && attachedToView) {
442+ if (dragging && status == DirectionalDragArea.Recognized && attachedToView) {
442443 // Gesture recognized. Let's move the spreadView with the finger
443− spreadView.contentX = -touchX + spreadDragArea.width - spreadView.shift;
444+ var finalX = Math.min(touchX + width, width);
445+ spreadView.contentX = -finalX + spreadDragArea.width - spreadView.shift;
444446 }
445447 if (attachedToView && spreadView.shiftedContentX >= spreadView.width * spreadView.positionMarker3) {
446448 // We passed positionMarker3. Detach from spreadView and snap it.
@@ -450,7 +452,13 @@ Rectangle {
450452 gesturePoints.push(touchX);
451453 }
452454
455+ property int previousStatus: -1
456+ property int currentStatus: DirectionalDragArea.WaitingForTouch
457+
453458 onStatusChanged: {
459+ previousStatus = currentStatus;
460+ currentStatus = status;
461+
454462 if (status == DirectionalDragArea.Recognized) {
455463 attachedToView = true;
456464 }
@@ -458,7 +466,7 @@ Rectangle {
458466
459467 onDraggingChanged: {
460468 if (dragging) {
461− // Gesture recognized. Start recording this gesture
469+ // A potential edge-drag gesture has started. Start recording it
462470 gesturePoints = [];
463471 return;
464472 }
@@ -475,7 +483,8 @@ Rectangle {
475483 }
476484 gesturePoints = [];
477485
478− if (oneWayFlick && spreadView.shiftedContentX > units.gu(2) &&
486+ if (previousStatus == DirectionalDragArea.Recognized &&
487+ oneWayFlick && spreadView.shiftedContentX > units.gu(2) &&
479488 spreadView.shiftedContentX < spreadView.positionMarker1 * spreadView.width) {
480489 // If it was a short one-way movement, do the Alt+Tab switch
481490 // no matter if we didn't cross positionMarker1 yet.
qml/Stages/SessionContainer.qml+1−13
@@ -23,7 +23,7 @@ Item {
2323 property QtObject session
2424 readonly property var childSessions: session ? session.childSessions : null
2525 readonly property alias surface: _surfaceContainer.surface
26− property bool interactive: true
26+ property alias interactive: _surfaceContainer.interactive
2727 property int orientation
2828
2929 readonly property alias surfaceContainer: _surfaceContainer
@@ -34,18 +34,6 @@ Item {
3434 orientation: root.orientation
3535 }
3636
37− Binding {
38− target: surfaceContainer.surface
39− when: surfaceContainer.surface
40− property: "enabled"
41− value: root.interactive
42− }
43− Binding {
44− target: surfaceContainer.surface
45− when: surfaceContainer.surface
46− property: "focus"
47− value: root.interactive
48− }
4937
5038 Repeater {
5139 model: root.childSessions
qml/Stages/SurfaceContainer.qml+37−6
@@ -16,6 +16,7 @@
1616
1717import QtQuick 2.0
1818import Ubuntu.Components 1.1
19+import Ubuntu.Gestures 0.1 // For TouchGate
1920
2021Item {
2122 id: root
@@ -23,21 +24,51 @@ Item {
2324 property Item surface: null
2425 property bool hadSurface: false
2526 property int orientation
27+ property bool interactive
2628
2729 onSurfaceChanged: {
2830 if (surface) {
2931 surface.parent = root;
32+ d.forceSurfaceActiveFocusIfReady();
3033 } else {
3134 hadSurface = true;
3235 }
3336 }
34− Binding {
35− target: surface
36− property: "anchors.fill"; value: root
37+ Binding { target: surface; property: "anchors.fill"; value: root }
38+ Binding { target: surface; property: "orientation"; value: root.orientation }
39+ Binding { target: surface; property: "z"; value: 1 }
40+ Binding { target: surface; property: "enabled"; value: root.interactive; when: surface }
41+ Binding { target: surface; property: "focus"; value: root.interactive; when: surface }
42+
43+ TouchGate {
44+ targetItem: surface
45+ anchors.fill: root
46+ enabled: root.surface ? root.surface.enabled : false
47+ z: 2
48+ }
49+
50+ Connections {
51+ target: root.surface
52+ // FIXME: I would rather not need to do this, but currently it doesn't get
53+ // active focus without it and I don't know why.
54+ // Possibly because if an item get focus=true before it has a parent, once
55+ // it gets a parent QQuickWindow won't check its focus and update its activeFocus
56+ // accordingly. Unlike when you focus=true after the item already has a parent.
57+ onFocusChanged: d.forceSurfaceActiveFocusIfReady();
58+ onParentChanged: d.forceSurfaceActiveFocusIfReady();
59+ onEnabledChanged: d.forceSurfaceActiveFocusIfReady();
3760 }
38− Binding {
39− target: surface
40− property: "orientation"; value: root.orientation
61+
62+ QtObject {
63+ id: d
64+ function forceSurfaceActiveFocusIfReady() {
65+ if (root.surface !== null &&
66+ root.surface.focus &&
67+ root.surface.parent === root &&
68+ root.surface.enabled) {
69+ root.surface.forceActiveFocus();
70+ }
71+ }
4172 }
4273
4374 states: [
src/CMakeLists.txt+6
@@ -1,5 +1,6 @@
11include_directories(
22 ${Qt5Gui_PRIVATE_INCLUDE_DIRS}
3+ ${CMAKE_SOURCE_DIR}/libs/UbuntuGestures
34)
45
56file(GLOB_RECURSE QML_FILES
@@ -29,6 +30,11 @@ if (NOT "${XCB_INCLUDE_DIRS}" STREQUAL "")
2930 target_link_libraries(${SHELL_APP} ${XCB_LDFLAGS})
3031endif()
3132
33+target_link_libraries(${SHELL_APP} UbuntuGestures)
34+
35+# For it to find libUbuntuGestures.so
36+set_target_properties(${SHELL_APP} PROPERTIES INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/${SHELL_PRIVATE_LIBDIR}")
37+
3238# install binaries
3339install(TARGETS ${SHELL_APP}
3440 RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
src/Dash/CMakeLists.txt+5
@@ -9,6 +9,11 @@ add_executable(unity8-dash ${DASH_SRCS})
99
1010qt5_use_modules(unity8-dash Gui Qml Quick Test)
1111
12+# For it to find libUbuntuGestures.so, needed by Ubuntu.Gestures QML module.
13+set_target_properties(unity8-dash PROPERTIES INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/${SHELL_PRIVATE_LIBDIR}")
14+
15+target_link_libraries(unity8-dash UbuntuGestures)
16+
1217set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fvisibility=hidden")
1318
1419# install binaries
src/Dash/main.cpp+6
@@ -31,6 +31,9 @@
3131#include "../ApplicationArguments.h"
3232#include "../CachingNetworkManagerFactory.h"
3333
34+// Ubuntu Gestures
35+#include <TouchRegistry.h>
36+
3437int main(int argc, const char *argv[])
3538{
3639 QGuiApplication *application = new QGuiApplication(argc, (char**)argv);
@@ -88,6 +91,9 @@ int main(int argc, const char *argv[])
8891 view->setTitle("Scopes");
8992 view->rootContext()->setContextProperty("applicationArguments", &qmlArgs);
9093
94+ TouchRegistry touchRegistry;
95+ view->installEventFilter(&touchRegistry);
96+
9197 // You will need this if you want to interact with touch-only components using a mouse
9298 // Needed only when manually testing on a desktop.
9399 MouseTouchAdaptor *mouseTouchAdaptor = 0;
src/main.cpp+5
@@ -34,6 +34,9 @@
3434#include "ApplicationArguments.h"
3535#include "CachingNetworkManagerFactory.h"
3636
37+// Ubuntu Gestures
38+#include <TouchRegistry.h>
39+
3740int main(int argc, const char *argv[])
3841{
3942 bool isMirServer = false;
@@ -120,6 +123,8 @@ Load the testability driver");
120123 if (parser.isSet(framelessOption)) {
121124 view->setFlags(Qt::FramelessWindowHint);
122125 }
126+ TouchRegistry touchRegistry;
127+ view->installEventFilter(&touchRegistry);
123128
124129 // You will need this if you want to interact with touch-only components using a mouse
125130 // Needed only when manually testing on a desktop.
tests/CMakeLists.txt+1
@@ -7,6 +7,7 @@ add_custom_target(qmluitests)
77add_custom_target(qmltests)
88add_dependencies(qmltests qmlunittests qmluitests)
99
10+add_subdirectory(libs)
1011add_subdirectory(qmltests)
1112add_subdirectory(mocks)
1213add_subdirectory(whitespace)
tests/libs/CMakeLists.txtnew file+1
@@ -0,0 +1 @@
1+add_subdirectory(UbuntuGestures)
tests/libs/UbuntuGestures/CMakeLists.txtnew file+20
@@ -0,0 +1,20 @@
1+include_directories(
2+ ${CMAKE_SOURCE_DIR}/libs/UbuntuGestures
3+ ${CMAKE_CURRENT_BINARY_DIR}
4+ )
5+
6+macro(add_gesture_test CLASSNAME)
7+ set(testCommand
8+ ${CLASSNAME}TestExec -o ${CMAKE_BINARY_DIR}/test${CLASSNAME}.xml,xunitxml -o -,txt)
9+
10+ add_test(NAME test${CLASSNAME} COMMAND ${testCommand})
11+
12+ set_tests_properties(test${CLASSNAME} PROPERTIES
13+ ENVIRONMENT LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/libs/UbuntuGestures)
14+
15+ add_executable(${CLASSNAME}TestExec tst_${CLASSNAME}.cpp)
16+ qt5_use_modules(${CLASSNAME}TestExec Test Core Gui Quick)
17+ target_link_libraries(${CLASSNAME}TestExec UbuntuGestures)
18+endmacro(add_gesture_test)
19+
20+add_gesture_test(TouchRegistry)
tests/libs/UbuntuGestures/tst_TouchRegistry.cppnew file+803
@@ -0,0 +1,803 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include <QtTest>
18+#include <QSet>
19+#include <QTouchEvent>
20+
21+#include <Timer.h>
22+#include <TouchOwnershipEvent.h>
23+#include <TouchRegistry.h>
24+#include <UnownedTouchEvent.h>
25+
26+using namespace UbuntuGestures;
27+
28+class TouchMemento {
29+public:
30+ TouchMemento(const QTouchEvent *touchEvent);
31+ Qt::TouchPointStates touchPointStates;
32+ QList<QTouchEvent::TouchPoint> touchPoints;
33+
34+ bool containsTouchWithId(int touchId) const;
35+};
36+
37+class DummyCandidate : public QQuickItem
38+{
39+ Q_OBJECT
40+public:
41+ bool event(QEvent *e) override;
42+ QSet<int> ownedTouches;
43+ QSet<int> lostTouches;
44+ QList<TouchMemento> unownedTouchEvents;
45+};
46+
47+class tst_TouchRegistry : public QObject
48+{
49+ Q_OBJECT
50+public:
51+ tst_TouchRegistry() : QObject(0) { }
52+private Q_SLOTS:
53+ void initTestCase() {} // will be called before the first test function is executed
54+ void cleanupTestCase() {} // will be called after the last test function was executed.
55+
56+ void init() {} // called right before each and every test function is executed
57+ void cleanup() {} // called right after each and every test function is executed
58+
59+ void requestWithNoCandidates();
60+ void lateCandidateRequestGetsNothing();
61+ void lateCandidateGestOwnershipOnceEarlyCandidateQuits();
62+ void dispatchesTouchEventsToCandidates();
63+ void dispatchesTouchEventsToWatchers();
64+ void keepDispatchingToWatchersAfterLastCandidateGivesUp();
65+ void candidatesAndWatchers_1();
66+ void candidatesAndWatchers_2();
67+ void rejectingTouchfterItsEnd();
68+ void removeOldUndecidedCandidates();
69+};
70+
71+void tst_TouchRegistry::requestWithNoCandidates()
72+{
73+ TouchRegistry touchRegistry;
74+ DummyCandidate candidate;
75+
76+ {
77+ QList<QTouchEvent::TouchPoint> touchPoints;
78+ touchPoints.append(QTouchEvent::TouchPoint(0));
79+ touchPoints[0].setState(Qt::TouchPointPressed);
80+ QTouchEvent touchEvent(QEvent::TouchBegin,
81+ 0 /* device */,
82+ Qt::NoModifier,
83+ Qt::TouchPointPressed,
84+ touchPoints);
85+ touchRegistry.update(&touchEvent);
86+ }
87+
88+ touchRegistry.requestTouchOwnership(0, &candidate);
89+
90+ QVERIFY(candidate.ownedTouches.contains(0));
91+}
92+
93+void tst_TouchRegistry::lateCandidateRequestGetsNothing()
94+{
95+ TouchRegistry touchRegistry;
96+ DummyCandidate earlyCandidate;
97+ earlyCandidate.setObjectName("early");
98+ DummyCandidate lateCandidate;
99+ lateCandidate.setObjectName("late");
100+
101+ {
102+ QList<QTouchEvent::TouchPoint> touchPoints;
103+ touchPoints.append(QTouchEvent::TouchPoint(0));
104+ touchPoints[0].setState(Qt::TouchPointPressed);
105+ QTouchEvent touchEvent(QEvent::TouchBegin,
106+ 0 /* device */,
107+ Qt::NoModifier,
108+ Qt::TouchPointPressed,
109+ touchPoints);
110+ touchRegistry.update(&touchEvent);
111+ }
112+
113+ touchRegistry.addCandidateOwnerForTouch(0, &earlyCandidate);
114+ touchRegistry.addCandidateOwnerForTouch(0, &lateCandidate);
115+
116+ QVERIFY(earlyCandidate.ownedTouches.isEmpty());
117+ QVERIFY(lateCandidate.ownedTouches.isEmpty());
118+
119+ touchRegistry.requestTouchOwnership(0, &lateCandidate);
120+
121+ QVERIFY(earlyCandidate.ownedTouches.isEmpty());
122+ QVERIFY(lateCandidate.ownedTouches.isEmpty());
123+
124+ touchRegistry.requestTouchOwnership(0, &earlyCandidate);
125+
126+ QVERIFY(earlyCandidate.ownedTouches.contains(0));
127+ QVERIFY(lateCandidate.ownedTouches.isEmpty());
128+}
129+
130+void tst_TouchRegistry::lateCandidateGestOwnershipOnceEarlyCandidateQuits()
131+{
132+ TouchRegistry touchRegistry;
133+ DummyCandidate earlyCandidate;
134+ DummyCandidate lateCandidate;
135+
136+ {
137+ QList<QTouchEvent::TouchPoint> touchPoints;
138+ touchPoints.append(QTouchEvent::TouchPoint(0));
139+ touchPoints[0].setState(Qt::TouchPointPressed);
140+ QTouchEvent touchEvent(QEvent::TouchBegin,
141+ 0 /* device */,
142+ Qt::NoModifier,
143+ Qt::TouchPointPressed,
144+ touchPoints);
145+ touchRegistry.update(&touchEvent);
146+ }
147+
148+ touchRegistry.addCandidateOwnerForTouch(0, &earlyCandidate);
149+ touchRegistry.addCandidateOwnerForTouch(0, &lateCandidate);
150+
151+ QVERIFY(earlyCandidate.ownedTouches.isEmpty());
152+ QVERIFY(lateCandidate.ownedTouches.isEmpty());
153+
154+ touchRegistry.requestTouchOwnership(0, &lateCandidate);
155+
156+ QVERIFY(earlyCandidate.ownedTouches.isEmpty());
157+ QVERIFY(lateCandidate.ownedTouches.isEmpty());
158+
159+ touchRegistry.removeCandidateOwnerForTouch(0, &earlyCandidate);
160+
161+ QVERIFY(earlyCandidate.ownedTouches.isEmpty());
162+ QVERIFY(lateCandidate.ownedTouches.contains(0));
163+}
164+
165+void tst_TouchRegistry::dispatchesTouchEventsToCandidates()
166+{
167+ TouchRegistry touchRegistry;
168+ QQuickItem rootItem;
169+
170+ DummyCandidate candidate0;
171+ candidate0.setObjectName("0");
172+ candidate0.setParentItem(&rootItem);
173+ candidate0.setX(1);
174+ candidate0.setY(2);
175+
176+ DummyCandidate candidate1;
177+ candidate1.setObjectName("1");
178+ candidate0.setParentItem(&rootItem);
179+ candidate1.setX(3);
180+ candidate1.setY(4);
181+
182+ {
183+ QList<QTouchEvent::TouchPoint> touchPoints;
184+ touchPoints.append(QTouchEvent::TouchPoint(0));
185+ touchPoints[0].setState(Qt::TouchPointPressed);
186+ touchPoints[0].setRect(QRect(10, 10, 0, 0));
187+ QTouchEvent touchEvent(QEvent::TouchBegin,
188+ 0 /* device */,
189+ Qt::NoModifier,
190+ Qt::TouchPointPressed,
191+ touchPoints);
192+ touchRegistry.update(&touchEvent);
193+ }
194+
195+ touchRegistry.addCandidateOwnerForTouch(0, &candidate0);
196+
197+ {
198+ QList<QTouchEvent::TouchPoint> touchPoints;
199+ touchPoints.append(QTouchEvent::TouchPoint(0));
200+ touchPoints[0].setState(Qt::TouchPointMoved);
201+ touchPoints[0].setRect(QRect(11, 11, 0, 0));
202+ touchPoints.append(QTouchEvent::TouchPoint(1));
203+ touchPoints[1].setState(Qt::TouchPointPressed);
204+ touchPoints[1].setRect(QRect(20, 20, 0, 0));
205+ QTouchEvent touchEvent(QEvent::TouchUpdate,
206+ 0 /* device */,
207+ Qt::NoModifier,
208+ Qt::TouchPointPressed | Qt::TouchPointMoved,
209+ touchPoints);
210+ touchRegistry.update(&touchEvent);
211+ }
212+
213+ // candidate0 should have received an update on the touch he's interested on.
214+ QCOMPARE(candidate0.unownedTouchEvents.count(), 1);
215+ // make sure only touch 0 is there (i.e. no mention of touch 1)
216+ QCOMPARE(candidate0.unownedTouchEvents[0].touchPoints.count(), 1);
217+ QCOMPARE(candidate0.unownedTouchEvents[0].touchPoints[0].id(), 0);
218+ // Check that the points local coordinates have been mapped to candidate0's coordinate system.
219+ QCOMPARE(candidate0.unownedTouchEvents[0].touchPoints[0].rect().x(), 11 - candidate0.x());
220+ QCOMPARE(candidate0.unownedTouchEvents[0].touchPoints[0].rect().y(), 11 - candidate0.y());
221+
222+ touchRegistry.addCandidateOwnerForTouch(1, &candidate1);
223+
224+ {
225+ QList<QTouchEvent::TouchPoint> touchPoints;
226+ touchPoints.append(QTouchEvent::TouchPoint(0));
227+ touchPoints[0].setState(Qt::TouchPointMoved);
228+ touchPoints[0].setRect(QRect(12, 12, 0, 0));
229+ touchPoints.append(QTouchEvent::TouchPoint(1));
230+ touchPoints[1].setState(Qt::TouchPointMoved);
231+ touchPoints[1].setRect(QRect(21, 21, 0, 0));
232+ QTouchEvent touchEvent(QEvent::TouchUpdate,
233+ 0 /* device */,
234+ Qt::NoModifier,
235+ Qt::TouchPointPressed | Qt::TouchPointMoved,
236+ touchPoints);
237+ touchRegistry.update(&touchEvent);
238+ }
239+
240+ // candidate0 gets updates only for touch 0 and
241+ // candidate1 gets updates only for touch 1
242+
243+ QCOMPARE(candidate0.unownedTouchEvents.count(), 2);
244+ QCOMPARE(candidate0.unownedTouchEvents[1].touchPoints.count(), 1);
245+ QCOMPARE(candidate0.unownedTouchEvents[1].touchPoints[0].id(), 0);
246+
247+ QCOMPARE(candidate1.unownedTouchEvents.count(), 1);
248+ QCOMPARE(candidate1.unownedTouchEvents[0].touchPoints.count(), 1);
249+ QCOMPARE(candidate1.unownedTouchEvents[0].touchPoints[0].id(), 1);
250+}
251+
252+void tst_TouchRegistry::dispatchesTouchEventsToWatchers()
253+{
254+ TouchRegistry touchRegistry;
255+ QQuickItem rootItem;
256+
257+ DummyCandidate watcher;
258+ watcher.setObjectName("watcher");
259+ watcher.setParentItem(&rootItem);
260+ watcher.setX(1);
261+ watcher.setY(2);
262+
263+ {
264+ QList<QTouchEvent::TouchPoint> touchPoints;
265+ touchPoints.append(QTouchEvent::TouchPoint(0));
266+ touchPoints[0].setState(Qt::TouchPointPressed);
267+ touchPoints[0].setRect(QRect(10, 10, 0, 0));
268+ QTouchEvent touchEvent(QEvent::TouchBegin,
269+ 0 /* device */,
270+ Qt::NoModifier,
271+ Qt::TouchPointPressed,
272+ touchPoints);
273+ touchRegistry.update(&touchEvent);
274+ }
275+
276+ touchRegistry.addTouchWatcher(0, &watcher);
277+
278+ {
279+ QList<QTouchEvent::TouchPoint> touchPoints;
280+ touchPoints.append(QTouchEvent::TouchPoint(0));
281+ touchPoints[0].setState(Qt::TouchPointMoved);
282+ touchPoints[0].setRect(QRect(11, 11, 0, 0));
283+ touchPoints.append(QTouchEvent::TouchPoint(1));
284+ touchPoints[1].setState(Qt::TouchPointPressed);
285+ touchPoints[1].setRect(QRect(20, 20, 0, 0));
286+ QTouchEvent touchEvent(QEvent::TouchUpdate,
287+ 0 /* device */,
288+ Qt::NoModifier,
289+ Qt::TouchPointPressed | Qt::TouchPointMoved,
290+ touchPoints);
291+ touchRegistry.update(&touchEvent);
292+ }
293+
294+ // watcher should have received an update on the touch he's interested on.
295+ QCOMPARE(watcher.unownedTouchEvents.count(), 1);
296+ // make sure only touch 0 is there (i.e. no mention of touch 1)
297+ QCOMPARE(watcher.unownedTouchEvents[0].touchPoints.count(), 1);
298+ QCOMPARE(watcher.unownedTouchEvents[0].touchPoints[0].id(), 0);
299+ // Check that the points local coordinates have been mapped to watcher's coordinate system.
300+ QCOMPARE(watcher.unownedTouchEvents[0].touchPoints[0].rect().x(), 11 - watcher.x());
301+ QCOMPARE(watcher.unownedTouchEvents[0].touchPoints[0].rect().y(), 11 - watcher.y());
302+
303+ {
304+ QList<QTouchEvent::TouchPoint> touchPoints;
305+ touchPoints.append(QTouchEvent::TouchPoint(0));
306+ touchPoints[0].setState(Qt::TouchPointMoved);
307+ touchPoints[0].setRect(QRect(12, 12, 0, 0));
308+ touchPoints.append(QTouchEvent::TouchPoint(1));
309+ touchPoints[1].setState(Qt::TouchPointMoved);
310+ touchPoints[1].setRect(QRect(21, 21, 0, 0));
311+ QTouchEvent touchEvent(QEvent::TouchUpdate,
312+ 0 /* device */,
313+ Qt::NoModifier,
314+ Qt::TouchPointPressed | Qt::TouchPointMoved,
315+ touchPoints);
316+ touchRegistry.update(&touchEvent);
317+ }
318+
319+ // watcher gets updates only for touch 0
320+
321+ QCOMPARE(watcher.unownedTouchEvents.count(), 2);
322+ QCOMPARE(watcher.unownedTouchEvents[1].touchPoints.count(), 1);
323+ QCOMPARE(watcher.unownedTouchEvents[1].touchPoints[0].id(), 0);
324+}
325+
326+void tst_TouchRegistry::keepDispatchingToWatchersAfterLastCandidateGivesUp()
327+{
328+ TouchRegistry touchRegistry;
329+ DummyCandidate item;
330+
331+ {
332+ QList<QTouchEvent::TouchPoint> touchPoints;
333+ touchPoints.append(QTouchEvent::TouchPoint(0));
334+ touchPoints[0].setState(Qt::TouchPointPressed);
335+ QTouchEvent touchEvent(QEvent::TouchBegin,
336+ 0 /* device */,
337+ Qt::NoModifier,
338+ Qt::TouchPointPressed,
339+ touchPoints);
340+ touchRegistry.update(&touchEvent);
341+ }
342+
343+ touchRegistry.addCandidateOwnerForTouch(0, &item);
344+
345+ {
346+ QList<QTouchEvent::TouchPoint> touchPoints;
347+ touchPoints.append(QTouchEvent::TouchPoint(0));
348+ touchPoints[0].setState(Qt::TouchPointMoved);
349+ touchPoints.append(QTouchEvent::TouchPoint(1));
350+ touchPoints[1].setState(Qt::TouchPointPressed);
351+ QTouchEvent touchEvent(QEvent::TouchUpdate,
352+ 0 /* device */,
353+ Qt::NoModifier,
354+ Qt::TouchPointPressed | Qt::TouchPointMoved,
355+ touchPoints);
356+ touchRegistry.update(&touchEvent);
357+ }
358+
359+ QCOMPARE(item.unownedTouchEvents.count(), 1);
360+ QCOMPARE(item.unownedTouchEvents[0].touchPoints.count(), 1);
361+ QCOMPARE(item.unownedTouchEvents[0].touchPoints[0].id(), 0);
362+ item.unownedTouchEvents.clear();
363+
364+ touchRegistry.addTouchWatcher(1, &item);
365+
366+ {
367+ QList<QTouchEvent::TouchPoint> touchPoints;
368+ touchPoints.append(QTouchEvent::TouchPoint(0));
369+ touchPoints[0].setState(Qt::TouchPointMoved);
370+ touchPoints.append(QTouchEvent::TouchPoint(1));
371+ touchPoints[1].setState(Qt::TouchPointMoved);
372+ QTouchEvent touchEvent(QEvent::TouchUpdate,
373+ 0 /* device */,
374+ Qt::NoModifier,
375+ Qt::TouchPointMoved,
376+ touchPoints);
377+ touchRegistry.update(&touchEvent);
378+ }
379+
380+ QCOMPARE(item.unownedTouchEvents.count(), 1);
381+ QCOMPARE(item.unownedTouchEvents[0].touchPoints.count(), 2);
382+ QVERIFY(item.unownedTouchEvents[0].containsTouchWithId(0));
383+ QVERIFY(item.unownedTouchEvents[0].containsTouchWithId(1));
384+ item.unownedTouchEvents.clear();
385+
386+ {
387+ QList<QTouchEvent::TouchPoint> touchPoints;
388+ touchPoints.append(QTouchEvent::TouchPoint(0));
389+ touchPoints[0].setState(Qt::TouchPointReleased);
390+ touchPoints.append(QTouchEvent::TouchPoint(1));
391+ touchPoints[1].setState(Qt::TouchPointMoved);
392+ QTouchEvent touchEvent(QEvent::TouchUpdate,
393+ 0 /* device */,
394+ Qt::NoModifier,
395+ Qt::TouchPointReleased | Qt::TouchPointMoved,
396+ touchPoints);
397+ touchRegistry.update(&touchEvent);
398+ }
399+
400+ QCOMPARE(item.unownedTouchEvents.count(), 1);
401+ QCOMPARE(item.unownedTouchEvents[0].touchPoints.count(), 2);
402+ QVERIFY(item.unownedTouchEvents[0].containsTouchWithId(0));
403+ QVERIFY(item.unownedTouchEvents[0].containsTouchWithId(1));
404+ item.unownedTouchEvents.clear();
405+
406+ touchRegistry.removeCandidateOwnerForTouch(0, &item);
407+
408+ {
409+ QList<QTouchEvent::TouchPoint> touchPoints;
410+ touchPoints.append(QTouchEvent::TouchPoint(1));
411+ touchPoints[0].setState(Qt::TouchPointReleased);
412+ QTouchEvent touchEvent(QEvent::TouchEnd,
413+ 0 /* device */,
414+ Qt::NoModifier,
415+ Qt::TouchPointReleased,
416+ touchPoints);
417+ touchRegistry.update(&touchEvent);
418+ }
419+
420+ QCOMPARE(item.unownedTouchEvents.count(), 1);
421+ QCOMPARE(item.unownedTouchEvents[0].touchPoints.count(), 1);
422+ QVERIFY(item.unownedTouchEvents[0].containsTouchWithId(1));
423+ item.unownedTouchEvents.clear();
424+
425+ QVERIFY(touchRegistry.m_touchInfoPool.isEmpty());
426+}
427+
428+/*
429+ Regression test that reproduces a problematic scenario that came up during manual testing.
430+ It reproduces the interaction between TouchRegistry and a DirectionalDragArea
431+ */
432+void tst_TouchRegistry::candidatesAndWatchers_1()
433+{
434+ TouchRegistry touchRegistry;
435+ DummyCandidate item;
436+
437+ {
438+ QList<QTouchEvent::TouchPoint> touchPoints;
439+ touchPoints.append(QTouchEvent::TouchPoint(0));
440+ touchPoints[0].setState(Qt::TouchPointPressed);
441+ QTouchEvent touchEvent(QEvent::TouchBegin,
442+ 0 /* device */,
443+ Qt::NoModifier,
444+ Qt::TouchPointPressed,
445+ touchPoints);
446+ touchRegistry.update(&touchEvent);
447+ }
448+
449+ touchRegistry.addCandidateOwnerForTouch(0, &item);
450+
451+ {
452+ QList<QTouchEvent::TouchPoint> touchPoints;
453+ touchPoints.append(QTouchEvent::TouchPoint(0));
454+ touchPoints[0].setState(Qt::TouchPointMoved);
455+ touchPoints.append(QTouchEvent::TouchPoint(1));
456+ touchPoints[1].setState(Qt::TouchPointPressed);
457+ QTouchEvent touchEvent(QEvent::TouchUpdate,
458+ 0 /* device */,
459+ Qt::NoModifier,
460+ Qt::TouchPointPressed | Qt::TouchPointMoved,
461+ touchPoints);
462+ touchRegistry.update(&touchEvent);
463+ }
464+
465+ touchRegistry.addTouchWatcher(1, &item);
466+
467+ touchRegistry.removeCandidateOwnerForTouch(0, &item);
468+
469+ touchRegistry.addTouchWatcher(0, &item);
470+
471+ {
472+ QList<QTouchEvent::TouchPoint> touchPoints;
473+ touchPoints.append(QTouchEvent::TouchPoint(0));
474+ touchPoints[0].setState(Qt::TouchPointReleased);
475+ touchPoints.append(QTouchEvent::TouchPoint(1));
476+ touchPoints[1].setState(Qt::TouchPointMoved);
477+ QTouchEvent touchEvent(QEvent::TouchUpdate,
478+ 0 /* device */,
479+ Qt::NoModifier,
480+ Qt::TouchPointReleased | Qt::TouchPointMoved,
481+ touchPoints);
482+ touchRegistry.update(&touchEvent);
483+ }
484+
485+ {
486+ QList<QTouchEvent::TouchPoint> touchPoints;
487+ touchPoints.append(QTouchEvent::TouchPoint(1));
488+ touchPoints[0].setState(Qt::TouchPointReleased);
489+ QTouchEvent touchEvent(QEvent::TouchEnd,
490+ 0 /* device */,
491+ Qt::NoModifier,
492+ Qt::TouchPointReleased,
493+ touchPoints);
494+ touchRegistry.update(&touchEvent);
495+ }
496+
497+ QVERIFY(touchRegistry.m_touchInfoPool.isEmpty());
498+
499+ item.unownedTouchEvents.clear();
500+
501+ {
502+ QList<QTouchEvent::TouchPoint> touchPoints;
503+ touchPoints.append(QTouchEvent::TouchPoint(0));
504+ touchPoints[0].setState(Qt::TouchPointPressed);
505+ QTouchEvent touchEvent(QEvent::TouchBegin,
506+ 0 /* device */,
507+ Qt::NoModifier,
508+ Qt::TouchPointPressed,
509+ touchPoints);
510+ touchRegistry.update(&touchEvent);
511+ }
512+
513+ // Haven't made any subscription for that new touch 0.
514+ QCOMPARE(item.unownedTouchEvents.count(), 0);
515+}
516+
517+/*
518+ Regression test that reproduces a problematic scenario that came up during manual testing.
519+ It reproduces the interaction between TouchRegistry, DirectionalDragArea and a TouchGate.
520+ */
521+void tst_TouchRegistry::candidatesAndWatchers_2()
522+{
523+ TouchRegistry touchRegistry;
524+
525+ DummyCandidate directionalDragArea;
526+ directionalDragArea.setObjectName("DirectionalDragArea");
527+
528+ DummyCandidate touchGate;
529+ touchGate.setObjectName("TouchGate");
530+
531+ // [DDA] 1298 TouchBegin (id:0, state:pressed, scenePos:(147,1023))
532+ {
533+ QList<QTouchEvent::TouchPoint> touchPoints;
534+ touchPoints.append(QTouchEvent::TouchPoint(0));
535+ touchPoints[0].setState(Qt::TouchPointPressed);
536+ QTouchEvent touchEvent(QEvent::TouchBegin,
537+ 0 /* device */,
538+ Qt::NoModifier,
539+ Qt::TouchPointPressed,
540+ touchPoints);
541+ touchRegistry.update(&touchEvent);
542+ }
543+
544+ // [TouchRegistry] addCandidateOwnerForTouch id 0 candidate DirectionalDragArea
545+ touchRegistry.addCandidateOwnerForTouch(0, &directionalDragArea);
546+
547+ // [TouchRegistry] requestTouchOwnership id 0 candidate TouchGate
548+ touchRegistry.requestTouchOwnership(0, &touchGate);
549+
550+ //[TouchRegistry] got TouchUpdate (id:0, state:moved, scenePos:(147,1023)) (id:1, state:pressed, scenePos:(327,792))
551+ {
552+ QList<QTouchEvent::TouchPoint> touchPoints;
553+ touchPoints.append(QTouchEvent::TouchPoint(0));
554+ touchPoints[0].setState(Qt::TouchPointMoved);
555+ touchPoints.append(QTouchEvent::TouchPoint(1));
556+ touchPoints[1].setState(Qt::TouchPointPressed);
557+ QTouchEvent touchEvent(QEvent::TouchUpdate,
558+ 0 /* device */,
559+ Qt::NoModifier,
560+ Qt::TouchPointPressed | Qt::TouchPointMoved,
561+ touchPoints);
562+ touchRegistry.update(&touchEvent);
563+ }
564+
565+ // [TouchRegistry] addTouchWatcher id 1 watcher DirectionalDragArea
566+ touchRegistry.addTouchWatcher(1, &directionalDragArea);
567+
568+ // [TouchRegistry] removeCandidateOwnerForTouch id 0 candidate DirectionalDragArea
569+ touchRegistry.removeCandidateOwnerForTouch(0, &directionalDragArea);
570+
571+ //[TouchRegistry] sending TouchWonershipEvent(id = 0 gained) to candidate TouchGate
572+ QCOMPARE(touchGate.ownedTouches.count(), 1);
573+ QVERIFY(touchGate.ownedTouches.contains(0));
574+
575+ // [TouchRegistry] addTouchWatcher id 0 watcher DirectionalDragArea
576+ touchRegistry.addTouchWatcher(0, &directionalDragArea);
577+
578+ // [TouchRegistry] requestTouchOwnership id 1 candidate TouchGate
579+ touchRegistry.requestTouchOwnership(1, &touchGate);
580+
581+ //[TouchRegistry] sending TouchWonershipEvent(id = 1 gained) to candidate TouchGate
582+ QCOMPARE(touchGate.ownedTouches.count(), 2);
583+ QVERIFY(touchGate.ownedTouches.contains(1));
584+
585+ directionalDragArea.unownedTouchEvents.clear();
586+ touchGate.unownedTouchEvents.clear();
587+
588+ //[TouchRegistry] got TouchUpdate (id:0, state:moved, scenePos:(148,1025)) (id:1, state:moved, scenePos:(329,795))
589+ {
590+ QList<QTouchEvent::TouchPoint> touchPoints;
591+ touchPoints.append(QTouchEvent::TouchPoint(0));
592+ touchPoints[0].setState(Qt::TouchPointMoved);
593+ touchPoints.append(QTouchEvent::TouchPoint(1));
594+ touchPoints[1].setState(Qt::TouchPointMoved);
595+ QTouchEvent touchEvent(QEvent::TouchUpdate,
596+ 0 /* device */,
597+ Qt::NoModifier,
598+ Qt::TouchPointMoved,
599+ touchPoints);
600+ touchRegistry.update(&touchEvent);
601+ }
602+
603+ //vvvvv DDA Watchers are being ignored from now on vvvvvvv
604+
605+ QCOMPARE(directionalDragArea.unownedTouchEvents.count(), 1);
606+ QCOMPARE(directionalDragArea.unownedTouchEvents.first().touchPoints.count(), 2);
607+ QVERIFY(directionalDragArea.unownedTouchEvents.first().containsTouchWithId(0));
608+ QVERIFY(directionalDragArea.unownedTouchEvents.first().containsTouchWithId(1));
609+
610+ QVERIFY(touchGate.unownedTouchEvents.isEmpty());
611+
612+ directionalDragArea.unownedTouchEvents.clear();
613+
614+ //[TouchRegistry] got TouchUpdate (id:0, state:moved, scenePos:(151,1029)) (id:1, state:released, scenePos:(329,795))
615+ {
616+ QList<QTouchEvent::TouchPoint> touchPoints;
617+ touchPoints.append(QTouchEvent::TouchPoint(0));
618+ touchPoints[0].setState(Qt::TouchPointMoved);
619+ touchPoints.append(QTouchEvent::TouchPoint(1));
620+ touchPoints[1].setState(Qt::TouchPointReleased);
621+ QTouchEvent touchEvent(QEvent::TouchUpdate,
622+ 0 /* device */,
623+ Qt::NoModifier,
624+ Qt::TouchPointMoved | Qt::TouchPointReleased,
625+ touchPoints);
626+ touchRegistry.update(&touchEvent);
627+ }
628+
629+ QCOMPARE(directionalDragArea.unownedTouchEvents.count(), 1);
630+ QCOMPARE(directionalDragArea.unownedTouchEvents.first().touchPoints.count(), 2);
631+ QVERIFY(directionalDragArea.unownedTouchEvents.first().containsTouchWithId(0));
632+ QVERIFY(directionalDragArea.unownedTouchEvents.first().containsTouchWithId(1));
633+
634+ QVERIFY(touchGate.unownedTouchEvents.isEmpty());
635+
636+ directionalDragArea.unownedTouchEvents.clear();
637+
638+ //[TouchRegistry] got TouchEnd (id:0, state:released, scenePos:(157,1034))
639+ {
640+ QList<QTouchEvent::TouchPoint> touchPoints;
641+ touchPoints.append(QTouchEvent::TouchPoint(0));
642+ touchPoints[0].setState(Qt::TouchPointReleased);
643+ QTouchEvent touchEvent(QEvent::TouchEnd,
644+ 0 /* device */,
645+ Qt::NoModifier,
646+ Qt::TouchPointReleased,
647+ touchPoints);
648+ touchRegistry.update(&touchEvent);
649+ }
650+
651+ QCOMPARE(directionalDragArea.unownedTouchEvents.count(), 1);
652+ QCOMPARE(directionalDragArea.unownedTouchEvents.first().touchPoints.count(), 1);
653+ QVERIFY(directionalDragArea.unownedTouchEvents.first().containsTouchWithId(0));
654+
655+ QVERIFY(touchGate.unownedTouchEvents.isEmpty());
656+}
657+
658+void tst_TouchRegistry::rejectingTouchfterItsEnd()
659+{
660+ TouchRegistry touchRegistry;
661+ DummyCandidate earlyCandidate;
662+ DummyCandidate lateCandidate;
663+
664+ {
665+ QList<QTouchEvent::TouchPoint> touchPoints;
666+ touchPoints.append(QTouchEvent::TouchPoint(0));
667+ touchPoints[0].setState(Qt::TouchPointPressed);
668+ QTouchEvent touchEvent(QEvent::TouchBegin,
669+ 0 /* device */,
670+ Qt::NoModifier,
671+ Qt::TouchPointPressed,
672+ touchPoints);
673+ touchRegistry.update(&touchEvent);
674+ }
675+
676+ touchRegistry.addCandidateOwnerForTouch(0, &earlyCandidate);
677+ touchRegistry.addCandidateOwnerForTouch(0, &lateCandidate);
678+
679+ QVERIFY(earlyCandidate.ownedTouches.isEmpty());
680+ QVERIFY(lateCandidate.ownedTouches.isEmpty());
681+
682+ touchRegistry.requestTouchOwnership(0, &lateCandidate);
683+
684+ QVERIFY(earlyCandidate.ownedTouches.isEmpty());
685+ QVERIFY(lateCandidate.ownedTouches.isEmpty());
686+
687+ {
688+ QList<QTouchEvent::TouchPoint> touchPoints;
689+ touchPoints.append(QTouchEvent::TouchPoint(0));
690+ touchPoints[0].setState(Qt::TouchPointReleased);
691+ QTouchEvent touchEvent(QEvent::TouchEnd,
692+ 0 /* device */,
693+ Qt::NoModifier,
694+ Qt::TouchPointPressed,
695+ touchPoints);
696+ touchRegistry.update(&touchEvent);
697+ }
698+
699+ QVERIFY(earlyCandidate.ownedTouches.isEmpty());
700+ QVERIFY(lateCandidate.ownedTouches.isEmpty());
701+
702+ touchRegistry.removeCandidateOwnerForTouch(0, &earlyCandidate);
703+
704+ QCOMPARE(lateCandidate.ownedTouches.count(), 1);
705+ QCOMPARE(lateCandidate.ownedTouches.contains(0), 1);
706+
707+ // Check that there's no trace left of touch 0 as we no longer need to keep tabs on it.
708+ QVERIFY(!touchRegistry.findTouchInfo(0));
709+}
710+
711+void tst_TouchRegistry::removeOldUndecidedCandidates()
712+{
713+ FakeTimerFactory *fakeTimerFactory = new FakeTimerFactory;
714+ TouchRegistry *touchRegistry = new TouchRegistry(nullptr, fakeTimerFactory);
715+
716+ DummyCandidate undecidedCandidate;
717+ undecidedCandidate.setObjectName("undecided");
718+
719+ DummyCandidate candidateThatWantsTouch;
720+ candidateThatWantsTouch.setObjectName("wantsTouch");
721+
722+ {
723+ QList<QTouchEvent::TouchPoint> touchPoints;
724+ touchPoints.append(QTouchEvent::TouchPoint(0));
725+ touchPoints[0].setState(Qt::TouchPointPressed);
726+ QTouchEvent touchEvent(QEvent::TouchBegin,
727+ 0 /* device */,
728+ Qt::NoModifier,
729+ Qt::TouchPointPressed,
730+ touchPoints);
731+ touchRegistry->update(&touchEvent);
732+ }
733+
734+ touchRegistry->addCandidateOwnerForTouch(0, &undecidedCandidate);
735+
736+ touchRegistry->requestTouchOwnership(0, &candidateThatWantsTouch);
737+
738+ QVERIFY(undecidedCandidate.ownedTouches.isEmpty());
739+ QVERIFY(undecidedCandidate.lostTouches.isEmpty());
740+ QVERIFY(candidateThatWantsTouch.ownedTouches.isEmpty());
741+ QVERIFY(candidateThatWantsTouch.lostTouches.isEmpty());
742+
743+ // Simulate that enough time has passed to cause the CandidateInactivityTimer to timeout,
744+ // making TouchRegistry consider that undecidedCantidate defaulted.
745+ fakeTimerFactory->makeRunningTimersTimeout();
746+
747+ QVERIFY(undecidedCandidate.ownedTouches.isEmpty());
748+ QVERIFY(undecidedCandidate.lostTouches.contains(0));
749+ QVERIFY(candidateThatWantsTouch.ownedTouches.contains(0));
750+ QVERIFY(candidateThatWantsTouch.lostTouches.isEmpty());
751+
752+ delete touchRegistry;
753+}
754+
755+////////////// TouchMemento //////////
756+
757+TouchMemento::TouchMemento(const QTouchEvent *touchEvent)
758+ : touchPointStates(touchEvent->touchPointStates()), touchPoints(touchEvent->touchPoints())
759+{
760+}
761+
762+bool TouchMemento::containsTouchWithId(int touchId) const
763+{
764+ for (int i = 0; i < touchPoints.count(); ++i) {
765+ if (touchPoints.at(i).id() == touchId) {
766+ return true;
767+ }
768+ }
769+ return false;
770+}
771+
772+////////////// DummyCandidate //////////
773+
774+bool DummyCandidate::event(QEvent *e)
775+{
776+ if (e->type() == TouchOwnershipEvent::touchOwnershipEventType()) {
777+ TouchOwnershipEvent *touchOwnershipEvent = static_cast<TouchOwnershipEvent *>(e);
778+
779+ // NB: Cannot use QVERIFY here because the macro doesn't return a boolean and is
780+ // meant for use only directly in the body of a test function
781+ if (ownedTouches.contains(touchOwnershipEvent->touchId()))
782+ qFatal("Sent ownership event for a touch that is already owned.");
783+ if (lostTouches.contains(touchOwnershipEvent->touchId()))
784+ qFatal("Sent ownership event for a touch that has already been lost.");
785+
786+ if (touchOwnershipEvent->gained()) {
787+ ownedTouches.insert(touchOwnershipEvent->touchId());
788+ } else {
789+ lostTouches.insert(touchOwnershipEvent->touchId());
790+ }
791+ return true;
792+ } else if (e->type() == UnownedTouchEvent::unownedTouchEventType()) {
793+ UnownedTouchEvent *unownedTouchEvent = static_cast<UnownedTouchEvent *>(e);
794+ unownedTouchEvents.append(TouchMemento(unownedTouchEvent->touchEvent()));
795+ return true;
796+ } else {
797+ return QObject::event(e);
798+ }
799+}
800+
801+QTEST_GUILESS_MAIN(tst_TouchRegistry)
802+
803+#include "tst_TouchRegistry.moc"
tests/mocks/Unity/Application/CMakeLists.txt+1
@@ -13,6 +13,7 @@ set(FakeUnityApplicationQml_SOURCES
1313 SessionManager.cpp
1414 SurfaceManager.cpp
1515 SessionModel.h
16+ UbuntuKeyboardInfo.cpp
1617 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationInfoInterface.h
1718 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationManagerInterface.h
1819)
tests/mocks/Unity/Application/MirSurfaceItem.cpp+18−22
@@ -40,6 +40,8 @@ MirSurfaceItem::MirSurfaceItem(const QString& name,
4040 , m_state(state)
4141 , m_live(true)
4242 , m_orientation(Qt::PortraitOrientation)
43+ , m_touchPressCount(0)
44+ , m_touchReleaseCount(0)
4345 , m_qmlItem(nullptr)
4446 , m_screenshotUrl(screenshot)
4547{
@@ -144,24 +146,12 @@ void MirSurfaceItem::setLive(bool live)
144146 }
145147}
146148
147−void MirSurfaceItem::onQmlWantInputMethodChanged()
148−{
149− QQmlProperty prop(m_qmlItem, "wantInputMethod");
150− bool wantInputMethod = prop.read().toBool();
151−
152− if (hasFocus() && wantInputMethod) {
153− Q_EMIT inputMethodRequested();
154− }
155−}
156−
157149void MirSurfaceItem::onFocusChanged()
158150{
159− QQmlProperty prop(m_qmlItem, "wantInputMethod");
160− bool wantInputMethod = prop.read().toBool();
161−
162− if (!hasFocus() && wantInputMethod) {
163− Q_EMIT inputMethodDismissed();
164− prop.write(QVariant::fromValue(false));
151+ if (!hasFocus()) {
152+ // Causes a crash in tst_Shell.qml, inside the mock Unity.Application itself.
153+ // Didn't have time to debug yet.
154+ //Q_EMIT inputMethodDismissed();
165155 }
166156}
167157
@@ -194,12 +184,18 @@ void MirSurfaceItem::createQmlContentItem()
194184 QQmlProperty screenshotSource(m_qmlItem, "screenshotSource");
195185 screenshotSource.write(QVariant::fromValue(m_screenshotUrl));
196186 }
187+}
197188
198− {
199− QQmlProperty prop(m_qmlItem, "wantInputMethod");
200− if (prop.type() == QQmlProperty::Property) {
201− bool ok = prop.connectNotifySignal(this, SLOT(onQmlWantInputMethodChanged()));
202− if (!ok) qCritical("MirSurfaceItem: failed to connect to wantInputMethod notify signal");
203− }
189+void MirSurfaceItem::touchEvent(QTouchEvent * event)
190+{
191+ if (event->touchPointStates() & Qt::TouchPointPressed) {
192+ ++m_touchPressCount;
193+ Q_EMIT touchPressCountChanged(m_touchPressCount);
194+ // Causes a crash in tst_Shell.qml, inside the mock Unity.Application itself.
195+ // Didn't have time to debug yet.
196+ // Q_EMIT inputMethodRequested();
197+ } else if (event->touchPointStates() & Qt::TouchPointReleased) {
198+ ++m_touchReleaseCount;
199+ Q_EMIT touchReleaseCountChanged(m_touchReleaseCount);
204200 }
205201}
tests/mocks/Unity/Application/MirSurfaceItem.h+18−1
@@ -36,6 +36,11 @@ class MirSurfaceItem : public QQuickItem
3636 Q_PROPERTY(bool live READ live NOTIFY liveChanged)
3737 Q_PROPERTY(Qt::ScreenOrientation orientation READ orientation WRITE setOrientation NOTIFY orientationChanged DESIGNABLE false)
3838
39+ Q_PROPERTY(int touchPressCount READ touchPressCount WRITE setTouchPressCount NOTIFY touchPressCountChanged
40+ DESIGNABLE false)
41+ Q_PROPERTY(int touchReleaseCount READ touchReleaseCount WRITE setTouchReleaseCount NOTIFY touchReleaseCountChanged
42+ DESIGNABLE false)
43+
3944public:
4045 enum Type {
4146 Normal,
@@ -73,6 +78,12 @@ public:
7378 void setScreenshot(const QUrl& screenshot);
7479 void setLive(bool live);
7580
81+ int touchPressCount() const { return m_touchPressCount; }
82+ void setTouchPressCount(int count) { m_touchPressCount = count; Q_EMIT touchPressCountChanged(count); }
83+
84+ int touchReleaseCount() const { return m_touchReleaseCount; }
85+ void setTouchReleaseCount(int count) { m_touchReleaseCount = count; Q_EMIT touchReleaseCountChanged(count); }
86+
7687 Q_INVOKABLE void setState(State newState);
7788 Q_INVOKABLE void release();
7889
@@ -81,6 +92,8 @@ Q_SIGNALS:
8192 void stateChanged(State);
8293 void liveChanged(bool live);
8394 void orientationChanged();
95+ void touchPressCountChanged(int count);
96+ void touchReleaseCountChanged(int count);
8497
8598 void inputMethodRequested();
8699 void inputMethodDismissed();
@@ -88,10 +101,12 @@ Q_SIGNALS:
88101 // internal mock use
89102 void deregister();
90103
104+protected:
105+ void touchEvent(QTouchEvent * event) override;
106+
91107private Q_SLOTS:
92108 void onFocusChanged();
93109 void onComponentStatusChanged(QQmlComponent::Status status);
94− void onQmlWantInputMethodChanged();
95110
96111private:
97112 explicit MirSurfaceItem(const QString& name,
@@ -110,6 +125,8 @@ private:
110125 State m_state;
111126 bool m_live;
112127 Qt::ScreenOrientation m_orientation;
128+ int m_touchPressCount;
129+ int m_touchReleaseCount;
113130
114131 QQmlComponent *m_qmlContentComponent;
115132 QQuickItem *m_qmlItem;
tests/mocks/Unity/Application/MirSurfaceItem.qml−12
@@ -17,7 +17,6 @@
1717import QtQuick 2.0
1818
1919Rectangle {
20− objectName: "fakeSurfaceQML"
2120 id: root
2221 color: "pink"
2322
@@ -38,11 +37,6 @@ Rectangle {
3837 property alias screenshotSource: screenshotImage.source
3938 property int orientation: Qt.PortraitOrientation
4039
41− property bool wantInputMethod: false
42−
43− property int touchPressCount: 0
44− property int touchReleaseCount: 0
45−
4640 Image {
4741 id: screenshotImage
4842 anchors.fill: parent
@@ -58,10 +52,4 @@ Rectangle {
5852 minimumPixelSize: 10; font.pixelSize: 200
5953 verticalAlignment: Text.AlignVCenter
6054 }
61−
62− MultiPointTouchArea {
63− anchors.fill: parent
64− onPressed: { root.wantInputMethod = true; root.touchPressCount++; }
65− onReleased: { root.touchReleaseCount++; }
66− }
6755}
tests/mocks/Unity/Application/UbuntuKeyboardInfo.cppnew file+28
@@ -0,0 +1,28 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software: you can redistribute it and/or modify it under
5+ * the terms of the GNU Lesser General Public License version 3, as published by
6+ * the Free Software Foundation.
7+ *
8+ * This program is distributed in the hope that it will be useful, but WITHOUT
9+ * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
10+ * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11+ * Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "UbuntuKeyboardInfo.h"
18+
19+UbuntuKeyboardInfo *UbuntuKeyboardInfo::m_instance = nullptr;
20+
21+UbuntuKeyboardInfo::UbuntuKeyboardInfo(QObject *parent)
22+ : QObject(parent),
23+ m_x(0),
24+ m_y(0),
25+ m_width(200),
26+ m_height(200)
27+{
28+}
tests/mocks/Unity/Application/UbuntuKeyboardInfo.hnew file+60
@@ -0,0 +1,60 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software: you can redistribute it and/or modify it under
5+ * the terms of the GNU Lesser General Public License version 3, as published by
6+ * the Free Software Foundation.
7+ *
8+ * This program is distributed in the hope that it will be useful, but WITHOUT
9+ * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
10+ * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11+ * Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef UBUNTU_KEYBOARD_INFO_H
18+#define UBUNTU_KEYBOARD_INFO_H
19+
20+#include <QLocalSocket>
21+#include <QTimer>
22+
23+class UbuntuKeyboardInfo : public QObject {
24+ Q_OBJECT
25+ Q_PROPERTY(qreal x READ x NOTIFY xChanged)
26+ Q_PROPERTY(qreal y READ y NOTIFY yChanged)
27+ Q_PROPERTY(qreal width READ width NOTIFY widthChanged)
28+ Q_PROPERTY(qreal height READ height NOTIFY heightChanged)
29+public:
30+ UbuntuKeyboardInfo(QObject *parent = 0);
31+ virtual ~UbuntuKeyboardInfo() {}
32+ qreal x() const { return m_x; }
33+ qreal y() const { return m_y; }
34+ qreal width() const { return m_width; }
35+ qreal height() const { return m_height; }
36+
37+ static UbuntuKeyboardInfo *singleton() {
38+ if (!m_instance) {
39+ m_instance = new UbuntuKeyboardInfo;
40+ }
41+ return m_instance;
42+ }
43+
44+Q_SIGNALS:
45+ void xChanged(qreal x);
46+ void yChanged(qreal y);
47+ void widthChanged(qreal width);
48+ void heightChanged(qreal height);
49+
50+private:
51+ QLocalSocket m_socket;
52+ qint32 m_x;
53+ qint32 m_y;
54+ qint32 m_width;
55+ qint32 m_height;
56+
57+ static UbuntuKeyboardInfo *m_instance;
58+};
59+
60+#endif // UBUNTU_KEYBOARD_INFO_H
tests/mocks/Unity/Application/plugin.cpp+7
@@ -24,6 +24,7 @@
2424#include "SurfaceManager.h"
2525#include "SessionManager.h"
2626#include "ApplicationTestInterface.h"
27+#include "UbuntuKeyboardInfo.h"
2728
2829#include <qqml.h>
2930#include <QQmlEngine>
@@ -46,6 +47,11 @@ static QObject* sessionManagerSingleton(QQmlEngine* engine, QJSEngine* scriptEng
4647 return SessionManager::singleton();
4748}
4849
50+static QObject* ubuntuKeyboardInfoSingleton(QQmlEngine*, QJSEngine*) {
51+ return UbuntuKeyboardInfo::singleton();
52+}
53+
54+
4955ApplicationTestInterface* s_appTestInterface = nullptr;
5056
5157static QObject* applicationTestInterface(QQmlEngine* engine, QJSEngine* scriptEngine) {
@@ -76,6 +82,7 @@ void FakeUnityApplicationQmlPlugin::registerTypes(const char *uri)
7682 qmlRegisterSingletonType<SurfaceManager>(uri, 0, 1, "SurfaceManager", surfaceManagerSingleton);
7783 qmlRegisterSingletonType<SessionManager>(uri, 0, 1, "SessionManager", sessionManagerSingleton);
7884 qmlRegisterSingletonType<ApplicationTestInterface>(uri, 0, 1, "ApplicationTest", applicationTestInterface);
85+ qmlRegisterSingletonType<UbuntuKeyboardInfo>(uri, 0, 1, "UbuntuKeyboardInfo", ubuntuKeyboardInfoSingleton);
7986
8087 qRegisterMetaType<MirSurfaceItem::Type>("MirSurfaceItem::Type");
8188 qRegisterMetaType<MirSurfaceItem::State>("MirSurfaceItem::State");
tests/plugins/Ubuntu/Gestures/CMakeLists.txt+9−2
@@ -1,7 +1,13 @@
11include(QmlTest)
22
3+# DANGER! DANGER! Using Qt's private API!
4+include_directories(
5+ ${Qt5Quick_PRIVATE_INCLUDE_DIRS}
6+)
7+
38include_directories(
49 ${CMAKE_SOURCE_DIR}/plugins/Ubuntu/Gestures
10+ ${CMAKE_SOURCE_DIR}/libs/UbuntuGestures
511 ${CMAKE_CURRENT_BINARY_DIR}
612 ${Qt5Gui_PRIVATE_INCLUDE_DIRS}
713 )
@@ -17,7 +23,7 @@ add_definitions(-DUBUNTU_GESTURES_PLUGIN_DIR="${CMAKE_BINARY_DIR}/plugins")
1723macro(add_gesture_ui_test CLASSNAME)
1824 add_executable(${CLASSNAME}TestExec tst_${CLASSNAME}.cpp GestureTest.cpp)
1925 qt5_use_modules(${CLASSNAME}TestExec Test Core Qml Gui Quick)
20− target_link_libraries(${CLASSNAME}TestExec UbuntuGestureQml)
26+ target_link_libraries(${CLASSNAME}TestExec UbuntuGesturesQml UbuntuGestures)
2127
2228 add_binary_qml_test(${CLASSNAME} ${CMAKE_BINARY_DIR}/plugins/Ubuntu/Gestures UbuntuGesturesTestQmlFiles "")
2329 add_manual_qml_test(. ${CLASSNAME} IMPORT_PATHS ${CMAKE_BINARY_DIR}/plugins)
@@ -34,10 +40,11 @@ macro(add_gesture_test CLASSNAME)
3440
3541 add_executable(${CLASSNAME}TestExec tst_${CLASSNAME}.cpp)
3642 qt5_use_modules(${CLASSNAME}TestExec Test Core)
37− target_link_libraries(${CLASSNAME}TestExec UbuntuGestureQml)
43+ target_link_libraries(${CLASSNAME}TestExec UbuntuGesturesQml)
3844endmacro(add_gesture_test)
3945
4046add_gesture_ui_test(DirectionalDragArea)
4147add_gesture_ui_test(PressedOutsideNotifier)
48+add_gesture_ui_test(TouchGate)
4249add_gesture_test(Damper)
4350add_gesture_test(AxisVelocityCalculator)
tests/plugins/Ubuntu/Gestures/RightwardsLauncher.qml+3
@@ -46,6 +46,9 @@ Item {
4646 id: dragArea
4747 objectName: "hpDragArea"
4848
49+ // give some room for items to be dynamically stacked right behind him
50+ z: 10.0
51+
4952 width: units.gu(5)
5053
5154 direction: Direction.Rightwards
tests/plugins/Ubuntu/Gestures/touchGateExample.qmlnew file+27
@@ -0,0 +1,27 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.0
18+import Ubuntu.Gestures 0.1
19+
20+Rectangle {
21+ width: 720; height: 720
22+
23+ TouchGate {
24+ anchors.fill: parent
25+ objectName: "touchGate"
26+ }
27+}
tests/plugins/Ubuntu/Gestures/tst_DirectionalDragArea.cpp+491−14
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013 Canonical, Ltd.
2+ * Copyright (C) 2013-2014 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -19,37 +19,63 @@
1919#include <QtQuick/QQuickView>
2020#include <QtQml/QQmlEngine>
2121#include <QPointer>
22+#include <private/qquickmousearea_p.h>
23+
24+// C++ std lib
25+#include <functional>
2226
2327#include <DirectionalDragArea.h>
28+#include <TouchRegistry.h>
2429
2530#include "GestureTest.h"
2631
2732using namespace UbuntuGestures;
2833
29−class FakeTimer : public AbstractTimer
34+class TouchMemento {
35+public:
36+ TouchMemento(const QTouchEvent *touchEvent);
37+ Qt::TouchPointStates touchPointStates;
38+ QList<QTouchEvent::TouchPoint> touchPoints;
39+
40+ bool containsTouchWithId(int touchId) const;
41+};
42+
43+class DummyItem : public QQuickItem
3044{
3145 Q_OBJECT
3246public:
33− FakeTimer(const SharedTimeSource &timeSource, QObject *parent = 0)
34− : UbuntuGestures::AbstractTimer(parent),
47+ DummyItem(QQuickItem *parent = 0);
48+
49+ QList<TouchMemento> touchEvents;
50+ std::function<void(QTouchEvent*)> touchEventHandler;
51+protected:
52+ void touchEvent(QTouchEvent *event) override;
53+private:
54+ static void defaultTouchEventHandler(QTouchEvent *event);
55+};
56+
57+class ComplexFakeTimer : public FakeTimer
58+{
59+ Q_OBJECT
60+public:
61+ ComplexFakeTimer(const SharedTimeSource &timeSource, QObject *parent = 0)
62+ : FakeTimer(parent),
3563 m_timeSource(timeSource)
3664 {}
3765
38− int interval() const override { return m_interval; }
39− void setInterval(int msecs) override { m_interval = msecs; }
4066 void start() override {
4167 AbstractTimer::start();
42− m_nextTimeoutTime = m_timeSource->msecsSinceReference() + (qint64)m_interval;
68+ m_nextTimeoutTime = m_timeSource->msecsSinceReference() + (qint64)interval();
4369 }
4470
4571 void emitTimeout() {
46− m_nextTimeoutTime += m_interval;
72+ m_nextTimeoutTime += interval();
4773 Q_EMIT timeout();
4874 }
4975
5076 qint64 nextTimeoutTime() const { return m_nextTimeoutTime; }
77+
5178private:
52− int m_interval;
5379 SharedTimeSource m_timeSource;
5480 qint64 m_nextTimeoutTime;
5581};
@@ -62,6 +88,30 @@ public:
6288 qint64 m_msecsSinceReference;
6389};
6490
91+/*
92+ QQuickMouseArea::canceled() signal is not registered in the meta object system.
93+ So using a QSignalSpy to track it won't work. Thus the only way to connect to it
94+ is using its method address directly.
95+ */
96+class MouseAreaSpy : public QObject
97+{
98+ Q_OBJECT
99+public:
100+ MouseAreaSpy(QQuickMouseArea *mouseArea)
101+ : canceledCount(0)
102+ {
103+ connect(mouseArea, &QQuickMouseArea::canceled,
104+ this, &MouseAreaSpy::onMouseAreaCanceled);
105+ }
106+
107+ int canceledCount;
108+
109+private Q_SLOTS:
110+ void onMouseAreaCanceled() {
111+ ++canceledCount;
112+ }
113+};
114+
65115class tst_DirectionalDragArea: public GestureTest
66116{
67117 Q_OBJECT
@@ -87,10 +137,20 @@ private Q_SLOTS:
87137 void sceneDistance();
88138 void sceneDistance_data();
89139 void disabledWhileDragging();
140+ void oneFingerDownFollowedByLateSecondFingerDown();
141+ void givesUpWhenLosesTouch();
142+ void threeFingerDrag();
143+ void immediateRecognitionWhenConstraintsDisabled();
144+ void withdrawTouchOwnershipCandidacyIfDisabledDuringRecognition();
145+ void withdrawTouchOwnershipCandidacyIfDisabledDuringRecognition_data();
146+ void tappedSignal();
147+ void tappedSignal_data();
148+ void gettingTouchOwnershipMakesMouseAreaBehindGetCanceled();
90149
91150private:
92− void passTime(qint64 timeSpan);
93− FakeTimer *fakeTimer;
151+ void passTime(qint64 timeSpanMs);
152+ TouchRegistry *touchRegistry;
153+ ComplexFakeTimer *fakeTimer;
94154 QSharedPointer<FakeTimeSource> fakeTimeSource;
95155};
96156
@@ -111,12 +171,19 @@ void tst_DirectionalDragArea::init()
111171 QTRY_COMPARE(m_view->width(), (int)m_view->rootObject()->width());
112172 QTRY_COMPARE(m_view->height(), (int)m_view->rootObject()->height());
113173
174+ touchRegistry = new TouchRegistry;
175+ m_view->installEventFilter(touchRegistry);
176+
114177 fakeTimeSource.reset(new FakeTimeSource);
115− fakeTimer = new FakeTimer(fakeTimeSource);
178+ fakeTimer = new ComplexFakeTimer(fakeTimeSource);
116179}
117180
118181void tst_DirectionalDragArea::cleanup()
119182{
183+ m_view->removeEventFilter(touchRegistry);
184+ delete touchRegistry;
185+ touchRegistry = nullptr;
186+
120187 delete fakeTimer;
121188 fakeTimer = 0;
122189
@@ -125,9 +192,9 @@ void tst_DirectionalDragArea::cleanup()
125192 GestureTest::cleanup();
126193}
127194
128−void tst_DirectionalDragArea::passTime(qint64 timeSpan)
195+void tst_DirectionalDragArea::passTime(qint64 timeSpanMs)
129196{
130− qint64 finalTime = fakeTimeSource->m_msecsSinceReference + timeSpan;
197+ qint64 finalTime = fakeTimeSource->m_msecsSinceReference + timeSpanMs;
131198
132199 if (fakeTimer->isRunning() && finalTime >= fakeTimer->nextTimeoutTime()) {
133200 fakeTimeSource->m_msecsSinceReference = fakeTimer->nextTimeoutTime();
@@ -849,6 +916,416 @@ void tst_DirectionalDragArea::disabledWhileDragging()
849916 QTest::touchEvent(m_view, m_device).release(0, touchPoint.toPoint());
850917}
851918
919+void tst_DirectionalDragArea::oneFingerDownFollowedByLateSecondFingerDown()
920+{
921+ DirectionalDragArea *edgeDragArea =
922+ m_view->rootObject()->findChild<DirectionalDragArea*>("hpDragArea");
923+ Q_ASSERT(edgeDragArea != 0);
924+ edgeDragArea->setRecognitionTimer(fakeTimer);
925+ edgeDragArea->setTimeSource(fakeTimeSource);
926+
927+ // Disable some constraints we're not interested in
928+ edgeDragArea->setMaxSilenceTime(60 * 1000);
929+ edgeDragArea->setMinSpeed(0);
930+
931+ // And ensure others have the values we want
932+ edgeDragArea->setCompositionTime(60);
933+
934+ // Put an item right behind edgeDragArea to receive the touches ignored by it
935+ DummyItem *dummyItem = new DummyItem;
936+ dummyItem->setParentItem(edgeDragArea->parentItem());
937+ dummyItem->setX(edgeDragArea->x());
938+ dummyItem->setY(edgeDragArea->y());
939+ dummyItem->setZ(edgeDragArea->z() - 1.0);
940+ dummyItem->setWidth(edgeDragArea->width());
941+ dummyItem->setHeight(edgeDragArea->height());
942+
943+ // Make touches evenly spaced along the edgeDragArea
944+ QPoint touch0Pos(edgeDragArea->width()/2.0f, m_view->height()*0.33f);
945+ QPoint touch1Pos(edgeDragArea->width()/2.0f, m_view->height()*0.66f);
946+
947+ QTest::touchEvent(m_view, m_device).press(0, touch0Pos);
948+
949+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Undecided);
950+
951+ // We are now going to be way beyond compositionTime
952+ passTime(edgeDragArea->compositionTime()*3);
953+
954+ QTest::touchEvent(m_view, m_device)
955+ .move(0, touch0Pos)
956+ .press(1, touch1Pos);
957+
958+ // A new touch has come, but as it can't be composed with touch 0, it should be
959+ // ignored/rejected by the DirectionalDragArea
960+ // Therefore the last event received by dummyItem must have both touch points (0 and 1)
961+ {
962+ TouchMemento &touchMemento = dummyItem->touchEvents.last();
963+ QCOMPARE(touchMemento.touchPoints.count(), 2);
964+ QVERIFY(touchMemento.containsTouchWithId(0));
965+ QVERIFY(touchMemento.containsTouchWithId(1));
966+ }
967+
968+ passTime(30);
969+
970+ QTest::touchEvent(m_view, m_device)
971+ .move(0, touch0Pos)
972+ .move(1, touch1Pos);
973+
974+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Undecided);
975+
976+ passTime(5);
977+
978+ QTest::touchEvent(m_view, m_device)
979+ .release(0, touch0Pos)
980+ .move(1, touch1Pos);
981+
982+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch);
983+
984+ passTime(5);
985+
986+ QTest::touchEvent(m_view, m_device)
987+ .release(1, touch1Pos);
988+
989+ // Shouldn't be keepping info about touches that no longer exist or interest us
990+ QVERIFY(edgeDragArea->m_activeTouches.isEmpty());
991+
992+ delete dummyItem;
993+}
994+
995+void tst_DirectionalDragArea::givesUpWhenLosesTouch()
996+{
997+ DirectionalDragArea *edgeDragArea =
998+ m_view->rootObject()->findChild<DirectionalDragArea*>("hpDragArea");
999+ Q_ASSERT(edgeDragArea != 0);
1000+ edgeDragArea->setRecognitionTimer(fakeTimer);
1001+ edgeDragArea->setTimeSource(fakeTimeSource);
1002+
1003+ // Disable some constraints we're not interested in
1004+ edgeDragArea->setMaxSilenceTime(60 * 1000);
1005+ edgeDragArea->setMinSpeed(0);
1006+
1007+ // Put an item right in front of edgeDragArea
1008+ DummyItem *dummyItem = new DummyItem(edgeDragArea->parentItem());
1009+ dummyItem->setX(edgeDragArea->x());
1010+ dummyItem->setY(edgeDragArea->y());
1011+ dummyItem->setZ(edgeDragArea->z() + 1.0);
1012+ dummyItem->setWidth(edgeDragArea->width());
1013+ dummyItem->setHeight(edgeDragArea->height());
1014+
1015+ QPoint touchPos(edgeDragArea->width()/2.0f, m_view->height()/2.0f);
1016+
1017+ dummyItem->touchEventHandler = [&](QTouchEvent *event) {
1018+ touchRegistry->addCandidateOwnerForTouch(0, dummyItem);
1019+ event->ignore();
1020+ };
1021+
1022+ QTest::touchEvent(m_view, m_device).press(0, touchPos);
1023+
1024+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Undecided);
1025+
1026+ touchRegistry->requestTouchOwnership(0, dummyItem);
1027+
1028+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch);
1029+
1030+ dummyItem->grabTouchPoints({0});
1031+ dummyItem->touchEventHandler = [&](QTouchEvent *event) { event->accept(); };
1032+
1033+ passTime(5);
1034+ QTest::touchEvent(m_view, m_device).release(0, touchPos);
1035+
1036+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch);
1037+
1038+ QVERIFY(edgeDragArea->m_activeTouches.isEmpty());
1039+}
1040+
1041+void tst_DirectionalDragArea::threeFingerDrag()
1042+{
1043+ DirectionalDragArea *edgeDragArea =
1044+ m_view->rootObject()->findChild<DirectionalDragArea*>("hpDragArea");
1045+ Q_ASSERT(edgeDragArea != 0);
1046+ edgeDragArea->setRecognitionTimer(fakeTimer);
1047+ edgeDragArea->setTimeSource(fakeTimeSource);
1048+
1049+ // Disable some constraints we're not interested in
1050+ edgeDragArea->setMaxSilenceTime(60 * 1000);
1051+ edgeDragArea->setMinSpeed(0);
1052+
1053+ // And ensure others have the values we want
1054+ edgeDragArea->setCompositionTime(60);
1055+
1056+ // Make touches evenly spaced along the edgeDragArea
1057+ QPoint touch0Pos(edgeDragArea->width()/2.0f, m_view->height()*0.25f);
1058+ QPoint touch1Pos(edgeDragArea->width()/2.0f, m_view->height()*0.50f);
1059+ QPoint touch2Pos(edgeDragArea->width()/2.0f, m_view->height()*0.75f);
1060+
1061+ QTest::touchEvent(m_view, m_device)
1062+ .press(0, touch0Pos);
1063+
1064+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Undecided);
1065+
1066+ passTime(5);
1067+ QTest::touchEvent(m_view, m_device)
1068+ .move(0, touch0Pos)
1069+ .press(1, touch1Pos);
1070+
1071+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch);
1072+
1073+ passTime(5);
1074+ QTest::touchEvent(m_view, m_device)
1075+ .move(0, touch0Pos)
1076+ .move(1, touch1Pos)
1077+ .press(2, touch2Pos);
1078+
1079+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch);
1080+
1081+ passTime(10);
1082+ QTest::touchEvent(m_view, m_device)
1083+ .move(0, touch0Pos)
1084+ .move(1, touch1Pos)
1085+ .move(2, touch2Pos);
1086+
1087+ passTime(200);
1088+ QTest::touchEvent(m_view, m_device)
1089+ .move(0, touch0Pos)
1090+ .release(1, touch1Pos)
1091+ .move(2, touch2Pos);
1092+
1093+ passTime(10);
1094+ QTest::touchEvent(m_view, m_device)
1095+ .move(0, touch0Pos)
1096+ .release(2, touch2Pos);
1097+
1098+ passTime(5);
1099+ QTest::touchEvent(m_view, m_device)
1100+ .release(0, touch0Pos);
1101+
1102+ // Shouldn't be keepping info about touches that no longer exist or interest us
1103+ QVERIFY(edgeDragArea->m_activeTouches.isEmpty());
1104+}
1105+
1106+/*
1107+ If all the relevant gesture recognition constraints/parameters have been disabled,
1108+ it means that the gesture recognition itself has been disabled and DirectionalDragArea
1109+ will therefore work like a simple touch area, merely reporting touch movement.
1110+ */
1111+void tst_DirectionalDragArea::immediateRecognitionWhenConstraintsDisabled()
1112+{
1113+ DirectionalDragArea *edgeDragArea =
1114+ m_view->rootObject()->findChild<DirectionalDragArea*>("hpDragArea");
1115+ Q_ASSERT(edgeDragArea != 0);
1116+ edgeDragArea->setRecognitionTimer(fakeTimer);
1117+ edgeDragArea->setTimeSource(fakeTimeSource);
1118+
1119+ // Disable the minimum amount of constraints to ensure immediate recognition
1120+ edgeDragArea->setDistanceThreshold(0);
1121+ edgeDragArea->setCompositionTime(0);
1122+
1123+ // Put an item right behind edgeDragArea to receive the touches ignored by it
1124+ DummyItem *dummyItem = new DummyItem;
1125+ dummyItem->setParentItem(edgeDragArea->parentItem());
1126+ dummyItem->setX(edgeDragArea->x());
1127+ dummyItem->setY(edgeDragArea->y());
1128+ dummyItem->setZ(edgeDragArea->z() - 1.0);
1129+ dummyItem->setWidth(edgeDragArea->width());
1130+ dummyItem->setHeight(edgeDragArea->height());
1131+
1132+ QPoint touch0Pos(edgeDragArea->width()/2.0f, m_view->height()/2.0f);
1133+
1134+ QTest::touchEvent(m_view, m_device).press(0, touch0Pos);
1135+
1136+ // check for immediate recognition
1137+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Recognized);
1138+
1139+ // and therefore it should have immediately grabbed the touch point,
1140+ // not letting it leak to items behind him.
1141+ QCOMPARE(dummyItem->touchEvents.count(), 0);
1142+}
1143+
1144+void tst_DirectionalDragArea::withdrawTouchOwnershipCandidacyIfDisabledDuringRecognition()
1145+{
1146+ DirectionalDragArea *edgeDragArea =
1147+ m_view->rootObject()->findChild<DirectionalDragArea*>("hpDragArea");
1148+ Q_ASSERT(edgeDragArea != 0);
1149+ edgeDragArea->setRecognitionTimer(fakeTimer);
1150+ edgeDragArea->setTimeSource(fakeTimeSource);
1151+
1152+ QPointF touchPoint = calculateInitialTouchPos(edgeDragArea);
1153+
1154+ // Move less than the minimum needed for the drag gesture recognition
1155+ qreal desiredDragDistance = edgeDragArea->distanceThreshold()*0.5f;
1156+ QPointF dragDirectionVector(1., 0.); // horizontal positive
1157+
1158+ qreal movementStepDistance = edgeDragArea->distanceThreshold() * 0.1f;
1159+ QPointF touchMovement = dragDirectionVector * movementStepDistance;
1160+ int totalMovementSteps = qCeil(desiredDragDistance / movementStepDistance);
1161+ int movementTimeStepMs = (edgeDragArea->compositionTime() * 0.8f) / totalMovementSteps;
1162+
1163+ QTest::touchEvent(m_view, m_device).press(0, touchPoint.toPoint());
1164+
1165+ for (int i = 0; i < totalMovementSteps; ++i) {
1166+ touchPoint += touchMovement;
1167+ passTime(movementTimeStepMs);
1168+ QTest::touchEvent(m_view, m_device).move(0, touchPoint.toPoint());
1169+ }
1170+
1171+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Undecided);
1172+
1173+ // edgeDragArea should be an undecided candidate
1174+ {
1175+ auto touchInfo = touchRegistry->findTouchInfo(0);
1176+ QCOMPARE(touchInfo->candidates.size(), 1);
1177+ QCOMPARE(touchInfo->candidates.at(0).item.data(), edgeDragArea);
1178+ QCOMPARE(touchInfo->candidates.at(0).undecided, true);
1179+ }
1180+
1181+ // disable the dragArea while it's still recognizing a possible drag gesture.
1182+ QFETCH(bool, disable);
1183+ if (disable) {
1184+ edgeDragArea->setEnabled(false);
1185+ } else {
1186+ edgeDragArea->setVisible(false);
1187+ }
1188+
1189+ // edgeDragArea should no longer be a candidate
1190+ {
1191+ auto touchInfo = touchRegistry->findTouchInfo(0);
1192+ QCOMPARE(touchInfo->candidates.size(), 0);
1193+ }
1194+
1195+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch);
1196+
1197+ QTest::touchEvent(m_view, m_device).release(0, touchPoint.toPoint());
1198+}
1199+
1200+void tst_DirectionalDragArea::withdrawTouchOwnershipCandidacyIfDisabledDuringRecognition_data()
1201+{
1202+ QTest::addColumn<bool>("disable");
1203+
1204+ QTest::newRow("disabled") << true;
1205+ QTest::newRow("invisible") << false;
1206+}
1207+
1208+void tst_DirectionalDragArea::tappedSignal()
1209+{
1210+ DirectionalDragArea *edgeDragArea =
1211+ m_view->rootObject()->findChild<DirectionalDragArea*>("hpDragArea");
1212+ QVERIFY(edgeDragArea != 0);
1213+ edgeDragArea->setRecognitionTimer(fakeTimer);
1214+ edgeDragArea->setTimeSource(fakeTimeSource);
1215+
1216+ QFETCH(bool, immediateGestureRecognition);
1217+ if (immediateGestureRecognition) {
1218+ // Disable the minimum amount of constraints to ensure immediate recognition
1219+ edgeDragArea->setDistanceThreshold(0);
1220+ edgeDragArea->setCompositionTime(0);
1221+ }
1222+
1223+ QPoint touch0Pos(edgeDragArea->width()/2.0f, m_view->height()/2.0f);
1224+
1225+ QSignalSpy tappedSpy(edgeDragArea, SIGNAL(tapped()));
1226+
1227+ QTest::touchEvent(m_view, m_device).press(0, touch0Pos);
1228+ passTime(edgeDragArea->maxTapDuration() / 2);
1229+ QTest::touchEvent(m_view, m_device).release(0, touch0Pos);
1230+
1231+ QCOMPARE(tappedSpy.count(), 1);
1232+}
1233+
1234+void tst_DirectionalDragArea::tappedSignal_data()
1235+{
1236+ QTest::addColumn<bool>("immediateGestureRecognition");
1237+
1238+ QTest::newRow("immediate gesture recognition") << true;
1239+ QTest::newRow("default gesture recognition") << false;
1240+}
1241+
1242+void tst_DirectionalDragArea::gettingTouchOwnershipMakesMouseAreaBehindGetCanceled()
1243+{
1244+ DirectionalDragArea *edgeDragArea =
1245+ m_view->rootObject()->findChild<DirectionalDragArea*>("hpDragArea");
1246+ QVERIFY(edgeDragArea != nullptr);
1247+ edgeDragArea->setRecognitionTimer(fakeTimer);
1248+ edgeDragArea->setTimeSource(fakeTimeSource);
1249+
1250+ QQuickMouseArea *mouseArea =
1251+ m_view->rootObject()->findChild<QQuickMouseArea*>("mouseArea");
1252+ QVERIFY(mouseArea != nullptr);
1253+
1254+ MouseAreaSpy mouseAreaSpy(mouseArea);
1255+
1256+ QPointF initialTouchPos = calculateInitialTouchPos(edgeDragArea);
1257+ QPointF touchPoint = initialTouchPos;
1258+
1259+ qreal desiredDragDistance = edgeDragArea->distanceThreshold()*2;
1260+ QPointF dragDirectionVector(1.0f, 0.0f); // rightwards
1261+ qreal movementStepDistance = edgeDragArea->distanceThreshold() * 0.1f;
1262+ QPointF touchMovement = dragDirectionVector * movementStepDistance;
1263+ int totalMovementSteps = qCeil(desiredDragDistance / movementStepDistance);
1264+ int movementTimeStepMs = (edgeDragArea->compositionTime() * 1.5f) / totalMovementSteps;
1265+
1266+ QCOMPARE(mouseArea->pressed(), false);
1267+
1268+ QTest::touchEvent(m_view, m_device).press(0, touchPoint.toPoint());
1269+
1270+ // The TouchBegin passes through the DirectionalDragArea and reaches the MouseArea behind it,
1271+ // where it's converted to a MouseEvent by QQuickWindow and sent to the MouseArea which then
1272+ // accepts it. Making it get pressed.
1273+ QCOMPARE(mouseArea->pressed(), true);
1274+ QCOMPARE(mouseAreaSpy.canceledCount, 0);
1275+
1276+ for (int i = 0; i < totalMovementSteps; ++i) {
1277+ touchPoint += touchMovement;
1278+ passTime(movementTimeStepMs);
1279+ QTest::touchEvent(m_view, m_device).move(0, touchPoint.toPoint());
1280+ }
1281+
1282+ // As the DirectionalDragArea recognizes the gesture, it grabs the touch from the MouseArea,
1283+ // which should make the MouseArea get a cancelation event, which will then cause it to
1284+ // reset its state (going back to "unpressed"/"released").
1285+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Recognized);
1286+ QCOMPARE(mouseArea->pressed(), false);
1287+ QCOMPARE(mouseAreaSpy.canceledCount, 1);
1288+
1289+ QTest::touchEvent(m_view, m_device).release(0, touchPoint.toPoint());
1290+}
1291+
1292+////////////////////////// TouchMemento /////////////////////////////
1293+
1294+TouchMemento::TouchMemento(const QTouchEvent *touchEvent)
1295+ : touchPointStates(touchEvent->touchPointStates()), touchPoints(touchEvent->touchPoints())
1296+{
1297+
1298+}
1299+
1300+bool TouchMemento::containsTouchWithId(int touchId) const
1301+{
1302+ for (int i = 0; i < touchPoints.count(); ++i) {
1303+ if (touchPoints.at(i).id() == touchId) {
1304+ return true;
1305+ }
1306+ }
1307+ return false;
1308+}
1309+
1310+////////////////////////// DummyItem /////////////////////////////
1311+
1312+DummyItem::DummyItem(QQuickItem *parent)
1313+ : QQuickItem(parent)
1314+{
1315+ touchEventHandler = defaultTouchEventHandler;
1316+}
1317+
1318+void DummyItem::touchEvent(QTouchEvent *event)
1319+{
1320+ touchEvents.append(TouchMemento(event));
1321+ touchEventHandler(event);
1322+}
1323+
1324+void DummyItem::defaultTouchEventHandler(QTouchEvent *event)
1325+{
1326+ event->accept();
1327+}
1328+
8521329QTEST_MAIN(tst_DirectionalDragArea)
8531330
8541331#include "tst_DirectionalDragArea.moc"
tests/plugins/Ubuntu/Gestures/tst_DirectionalDragArea.qml+1
@@ -24,6 +24,7 @@ Rectangle {
2424
2525 MouseArea {
2626 id: mouseArea
27+ objectName: "mouseArea"
2728 anchors.fill: parent
2829 onClicked: {
2930 hpLauncher.reset()
tests/plugins/Ubuntu/Gestures/tst_TouchGate.cppnew file+345
@@ -0,0 +1,345 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include <qpa/qwindowsysteminterface.h>
18+#include <QtTest>
19+#include <QQmlEngine>
20+#include <QQuickView>
21+#include <QSharedPointer>
22+
23+// C++ std lib
24+#include <functional>
25+
26+#include <TouchGate.h>
27+#include <TouchRegistry.h>
28+
29+class TestItem : public QQuickItem
30+{
31+ Q_OBJECT
32+
33+public:
34+ QList<QSharedPointer<QTouchEvent>> touchEventsReceived;
35+
36+protected:
37+ void touchEvent(QTouchEvent *event) override;
38+};
39+
40+class CandidateItem : public QQuickItem
41+{
42+ Q_OBJECT
43+
44+public:
45+ std::function<void(QTouchEvent*)> touchEventHandler;
46+
47+protected:
48+ void touchEvent(QTouchEvent *event) override;
49+};
50+
51+class tst_TouchGate : public QObject
52+{
53+ Q_OBJECT
54+public:
55+ tst_TouchGate(QObject *parent = nullptr);
56+private Q_SLOTS:
57+ void initTestCase(); // will be called before the first test function is executed
58+ void cleanupTestCase(); // will be called after the last test function was executed.
59+
60+ void init(); // called right before each and every test function is executed
61+ void cleanup(); // called right after each and every test function is executed
62+
63+ void onlyCandidate_passThrough();
64+ void holdsEventsUntilGainsOwnership();
65+ void holdsEventsUntilGainsOwnership_data();
66+ void holdsEventsUntilGainsOwnership_repeatingTouchId();
67+ void dontDispacthToDisabledOrInvisibleTarget();
68+ void dontDispacthToDisabledOrInvisibleTarget_data();
69+
70+private:
71+ QQuickView *createView();
72+ TouchRegistry *touchRegistry;
73+ QQuickView *view;
74+ QTouchDevice *device;
75+};
76+
77+tst_TouchGate::tst_TouchGate(QObject *parent)
78+ : QObject(parent)
79+ , touchRegistry(nullptr)
80+ , view(nullptr)
81+ , device(nullptr)
82+{
83+}
84+
85+void tst_TouchGate::initTestCase()
86+{
87+ if (!device) {
88+ device = new QTouchDevice;
89+ device->setType(QTouchDevice::TouchScreen);
90+ QWindowSystemInterface::registerTouchDevice(device);
91+ }
92+}
93+
94+void tst_TouchGate::cleanupTestCase()
95+{
96+}
97+
98+void tst_TouchGate::init()
99+{
100+ touchRegistry = new TouchRegistry;
101+
102+ view = createView();
103+ view->installEventFilter(touchRegistry);
104+ view->setSource(QUrl::fromLocalFile("touchGateExample.qml"));
105+ view->show();
106+ QVERIFY(QTest::qWaitForWindowExposed(view));
107+ QVERIFY(view->rootObject() != 0);
108+ qApp->processEvents();
109+}
110+
111+void tst_TouchGate::cleanup()
112+{
113+ delete view;
114+ view = 0;
115+
116+ delete touchRegistry;
117+ touchRegistry = nullptr;
118+}
119+
120+QQuickView *tst_TouchGate::createView()
121+{
122+ QQuickView *window = new QQuickView(0);
123+ window->setResizeMode(QQuickView::SizeRootObjectToView);
124+ window->resize(720, 720);
125+ window->engine()->addImportPath(QLatin1String(UBUNTU_GESTURES_PLUGIN_DIR));
126+
127+ return window;
128+}
129+
130+/*
131+ Simplest case. TouchGate is the only candidate for the touch point, thus it
132+ will instantly gain ownership causing the event to pass through it directly to
133+ its child.
134+ */
135+void tst_TouchGate::onlyCandidate_passThrough()
136+{
137+ TouchGate *touchGate = view->rootObject()->findChild<TouchGate*>("touchGate");
138+ Q_ASSERT(touchGate);
139+
140+ TestItem *testItem = new TestItem;
141+ testItem->setWidth(touchGate->width());
142+ testItem->setHeight(touchGate->height());
143+ testItem->setParentItem(view->rootObject());
144+ testItem->setZ(0.0);
145+
146+ touchGate->setZ(1.0);
147+ touchGate->setTargetItem(testItem);
148+
149+ QTest::touchEvent(view, device)
150+ .press(0, QPoint(100,100));
151+
152+ QCOMPARE(testItem->touchEventsReceived.count(), 1);
153+ QVERIFY(touchGate->m_storedEvents.isEmpty());
154+}
155+
156+void tst_TouchGate::holdsEventsUntilGainsOwnership()
157+{
158+ TouchGate *touchGate = view->rootObject()->findChild<TouchGate*>("touchGate");
159+ Q_ASSERT(touchGate);
160+ QFETCH(bool, ownershipAfterTouchEnd);
161+
162+ TestItem *testItem = new TestItem;
163+ testItem->setWidth(touchGate->width());
164+ testItem->setHeight(touchGate->height());
165+ testItem->setParentItem(view->rootObject());
166+ testItem->setZ(0.0);
167+
168+ touchGate->setZ(1.0);
169+ touchGate->setTargetItem(testItem);
170+
171+ // Put it in front of touchGate
172+ CandidateItem *candidateItem = new CandidateItem;
173+ candidateItem->setWidth(touchGate->width());
174+ candidateItem->setHeight(touchGate->height());
175+ candidateItem->setParentItem(view->rootObject());
176+ candidateItem->setZ(2.0);
177+
178+ candidateItem->touchEventHandler = [&](QTouchEvent* event) {
179+ touchRegistry->addCandidateOwnerForTouch(0, candidateItem);
180+ event->ignore();
181+ };
182+
183+ QTest::touchEvent(view, device)
184+ .press(0, QPoint(100,100));
185+
186+ QCOMPARE(testItem->touchEventsReceived.count(), 0);
187+
188+ QTest::touchEvent(view, device)
189+ .move(0, QPoint(101,101));
190+
191+ QCOMPARE(testItem->touchEventsReceived.count(), 0);
192+
193+ if (!ownershipAfterTouchEnd) {
194+ touchRegistry->removeCandidateOwnerForTouch(0, candidateItem);
195+ // TouchGate should now open its flood gates and let testItem get all
196+ // events from touch 0 produced so far
197+ QCOMPARE(testItem->touchEventsReceived.count(), 2);
198+ }
199+
200+ QTest::touchEvent(view, device)
201+ .release(0, QPoint(101,101));
202+
203+ if (ownershipAfterTouchEnd) {
204+ QCOMPARE(testItem->touchEventsReceived.count(), 0);
205+ touchRegistry->removeCandidateOwnerForTouch(0, candidateItem);
206+ // TouchGate should now open its flood gates and let testItem get all
207+ // events from touch 0 produced so far
208+ }
209+
210+ QCOMPARE(testItem->touchEventsReceived.count(), 3);
211+
212+ QVERIFY(touchGate->m_storedEvents.isEmpty());
213+ QVERIFY(touchGate->m_touchInfoMap.isEmpty());
214+}
215+
216+void tst_TouchGate::holdsEventsUntilGainsOwnership_data()
217+{
218+ QTest::addColumn<bool>("ownershipAfterTouchEnd");
219+
220+ QTest::newRow("touch end after touch ownership") << false;
221+ QTest::newRow("touch ownership after touch end") << true;
222+}
223+
224+/*
225+ A touch point of id 0 starts and ends. Then the next touch point is also given
226+ id 0. Check that TouchGate behaves correcly recognizing them as two distinct touch
227+ points.
228+
229+ Note that this won't be a problem with Mir as it assigns unique ids for the touch
230+ points it produces.
231+ */
232+void tst_TouchGate::holdsEventsUntilGainsOwnership_repeatingTouchId()
233+{
234+ TouchGate *touchGate = view->rootObject()->findChild<TouchGate*>("touchGate");
235+ Q_ASSERT(touchGate);
236+
237+ TestItem *testItem = new TestItem;
238+ testItem->setWidth(touchGate->width());
239+ testItem->setHeight(touchGate->height());
240+ testItem->setParentItem(view->rootObject());
241+ testItem->setZ(0.0);
242+
243+ touchGate->setZ(1.0);
244+ touchGate->setTargetItem(testItem);
245+
246+ QTest::touchEvent(view, device)
247+ .press(0, QPoint(100,100));
248+ QTest::touchEvent(view, device)
249+ .move(0, QPoint(101,101));
250+ QTest::touchEvent(view, device)
251+ .release(0, QPoint(101,101));
252+
253+ QCOMPARE(testItem->touchEventsReceived.count(), 3);
254+ testItem->touchEventsReceived.clear();
255+
256+ // Put it in front of touchGate
257+ CandidateItem *candidateItem = new CandidateItem;
258+ candidateItem->setWidth(touchGate->width());
259+ candidateItem->setHeight(touchGate->height());
260+ candidateItem->setParentItem(view->rootObject());
261+ candidateItem->setZ(2.0);
262+
263+ candidateItem->touchEventHandler = [&](QTouchEvent* event) {
264+ touchRegistry->addCandidateOwnerForTouch(0, candidateItem);
265+ event->ignore();
266+ };
267+
268+ // A brand new touch, but with the same id as the previous one
269+ QTest::touchEvent(view, device)
270+ .press(0, QPoint(200,200));
271+ QTest::touchEvent(view, device)
272+ .move(0, QPoint(201,201));
273+ QTest::touchEvent(view, device)
274+ .release(0, QPoint(201,201));
275+
276+ QCOMPARE(testItem->touchEventsReceived.count(), 0);
277+
278+ touchRegistry->requestTouchOwnership(0, candidateItem);
279+
280+ QCOMPARE(testItem->touchEventsReceived.count(), 0);
281+
282+ QVERIFY(touchGate->m_storedEvents.isEmpty());
283+}
284+
285+void tst_TouchGate::dontDispacthToDisabledOrInvisibleTarget()
286+{
287+ TouchGate *touchGate = view->rootObject()->findChild<TouchGate*>("touchGate");
288+ Q_ASSERT(touchGate);
289+
290+ TestItem *testItem = new TestItem;
291+ testItem->setWidth(touchGate->width());
292+ testItem->setHeight(touchGate->height());
293+ testItem->setParentItem(view->rootObject());
294+ testItem->setZ(0.0);
295+
296+ QFETCH(bool, isEnabled);
297+ testItem->setEnabled(isEnabled);
298+
299+ QFETCH(bool, isVisible);
300+ testItem->setVisible(isVisible);
301+
302+ touchGate->setZ(1.0);
303+ touchGate->setTargetItem(testItem);
304+
305+ QTest::touchEvent(view, device)
306+ .press(0, QPoint(100,100));
307+
308+ QCOMPARE(testItem->touchEventsReceived.count(), 0);
309+ QVERIFY(touchGate->m_storedEvents.isEmpty());
310+}
311+
312+void tst_TouchGate::dontDispacthToDisabledOrInvisibleTarget_data()
313+{
314+ QTest::addColumn<bool>("isEnabled");
315+ QTest::addColumn<bool>("isVisible");
316+
317+ QTest::newRow("disabled visible") << false << true;
318+ QTest::newRow("enabled invisible") << true << false;
319+ QTest::newRow("disabled invisible") << false << false;
320+}
321+
322+///////////// TestItem /////////////////////////////////////////////////////////////
323+
324+void TestItem::touchEvent(QTouchEvent *event)
325+{
326+ QTouchEvent *clonedEvent = new QTouchEvent(event->type(),
327+ event->device(),
328+ event->modifiers(),
329+ event->touchPointStates(),
330+ event->touchPoints());
331+
332+ touchEventsReceived.append(QSharedPointer<QTouchEvent>(clonedEvent));
333+}
334+
335+///////////// CandidateItem /////////////////////////////////////////////////////////////
336+
337+void CandidateItem::touchEvent(QTouchEvent *event)
338+{
339+ touchEventHandler(event);
340+}
341+
342+
343+QTEST_MAIN(tst_TouchGate)
344+
345+#include "tst_TouchGate.moc"
tests/qmltests/Components/CMakeLists.txt+2−1
@@ -2,13 +2,14 @@
22
33include_directories(
44 ${CMAKE_SOURCE_DIR}/plugins/Ubuntu/Gestures
5+ ${CMAKE_SOURCE_DIR}/libs/UbuntuGestures
56 ${CMAKE_CURRENT_BINARY_DIR}
67 ${Qt5Gui_PRIVATE_INCLUDE_DIRS}
78 )
89
910add_executable(DragHandleTestExec tst_DragHandle.cpp)
1011qt5_use_modules(DragHandleTestExec Test Core Qml Gui Quick)
11−target_link_libraries(DragHandleTestExec UbuntuGestureQml)
12+target_link_libraries(DragHandleTestExec UbuntuGesturesQml UbuntuGestures)
1213
1314add_binary_qml_test(DragHandle ${CMAKE_BINARY_DIR}/plugins/Ubuntu/Gestures "" "")
1415add_manual_qml_test(. DragHandle IMPORT_PATHS ${CMAKE_BINARY_DIR}/plugins)
tests/qmltests/Components/tst_DragHandle.cpp+12−15
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013 Canonical, Ltd.
2+ * Copyright (C) 2013-2014 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -22,22 +22,12 @@
2222#include <QtQml/QQmlEngine>
2323
2424#include <DirectionalDragArea.h>
25+#include <Timer.h>
26+#include <TouchRegistry.h>
2527
26−class FakeTimer : public UbuntuGestures::AbstractTimer
27−{
28− Q_OBJECT
29−public:
30− FakeTimer(QObject *parent = 0)
31− : UbuntuGestures::AbstractTimer(parent)
32− {}
33−
34− virtual int interval() const { return m_duration; }
35− virtual void setInterval(int msecs) { m_duration = msecs; }
36−private:
37− int m_duration;
38−};
28+using namespace UbuntuGestures;
3929
40−class FakeTimeSource : public UbuntuGestures::TimeSource
30+class FakeTimeSource : public TimeSource
4131{
4232public:
4333 FakeTimeSource() { m_msecsSinceReference = 0; }
@@ -77,6 +67,7 @@ private:
7767
7868 QQuickView *createView();
7969 QQuickView *m_view;
70+ TouchRegistry *m_touchRegistry;
8071 QTouchDevice *m_device;
8172 FakeTimer *m_fakeTimer;
8273 QSharedPointer<FakeTimeSource> m_fakeTimeSource;
@@ -100,7 +91,10 @@ void tst_DragHandle::cleanupTestCase()
10091
10192void tst_DragHandle::init()
10293{
94+ m_touchRegistry = new TouchRegistry;
95+
10396 m_view = createView();
97+ m_view->installEventFilter(m_touchRegistry);
10498 m_view->setSource(QUrl::fromLocalFile(TEST_DIR"/tst_DragHandle.qml"));
10599 m_view->show();
106100 QVERIFY(QTest::qWaitForWindowExposed(m_view));
@@ -116,6 +110,9 @@ void tst_DragHandle::cleanup()
116110 delete m_view;
117111 m_view = 0;
118112
113+ delete m_touchRegistry;
114+ m_touchRegistry = 0;
115+
119116 delete m_fakeTimer;
120117 m_fakeTimer = 0;
121118
tests/qmltests/Greeter/tst_SingleGreeter.qml+1−1
@@ -87,7 +87,7 @@ Item {
8787
8888 function test_teasingArea(data) {
8989 teaseSpy.clear()
90− mouseClick(greeter, data.posX, greeter.height - units.gu(1))
90+ tap(greeter, data.posX, greeter.height - units.gu(1))
9191 teaseSpy.wait()
9292 tryCompare(teaseSpy, "count", 1)
9393 }
tests/qmltests/Launcher/tst_Launcher.qml+117−101
@@ -1,5 +1,5 @@
11/*
2− * Copyright 2013 Canonical Ltd.
2+ * Copyright 2013-2014 Canonical Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -28,25 +28,49 @@ Item {
2828 width: units.gu(50)
2929 height: units.gu(55)
3030
31− Launcher {
32− id: launcher
33− x: 0
34− y: 0
35− width: units.gu(40)
36− height: parent.height
31+ Loader {
32+ id: launcherLoader
33+ anchors.fill: parent
34+ property bool itemDestroyed: false
35+ sourceComponent: Component {
36+ Launcher {
37+ id: launcher
38+ x: 0
39+ y: 0
40+ width: units.gu(40)
41+ height: root.height
42+
43+ property string lastSelectedApplication
44+
45+ onLauncherApplicationSelected: {
46+ lastSelectedApplication = appId
47+ }
3748
38− property string lastSelectedApplication
49+ property int showDashHome_count: 0
50+ onShowDashHome: {
51+ showDashHome_count++;
52+ }
3953
40− onLauncherApplicationSelected: {
41− lastSelectedApplication = appId
42− }
54+ property int maxPanelX: 0
4355
44− property int showDashHome_count: 0
45− onShowDashHome: {
46− showDashHome_count++;
47− }
56+ Connections {
57+ target: testCase.findChild(launcher, "launcherPanel")
58+
59+ onXChanged: {
60+ if (target.x > launcher.maxPanelX) {
61+ launcher.maxPanelX = target.x;
62+ }
63+ }
64+ }
4865
49− property int maxPanelX: 0
66+ Component.onCompleted: {
67+ launcherLoader.itemDestroyed = false;
68+ }
69+ Component.onDestruction: {
70+ launcherLoader.itemDestroyed = true;
71+ }
72+ }
73+ }
5074 }
5175
5276 SignalSpy {
@@ -54,19 +78,45 @@ Item {
5478 target: LauncherModel
5579 }
5680
57− Connections {
58− target: testCase.findChild(launcher, "launcherPanel")
81+ UT.UnityTestCase {
82+ id: testCase
83+ name: "Launcher"
84+ when: windowShown
5985
60− onXChanged: {
61− if (target.x > launcher.maxPanelX) {
62− launcher.maxPanelX = target.x;
63− }
86+ property Item launcher: launcherLoader.status === Loader.Ready ? launcherLoader.item : null
87+ function cleanup() {
88+ launcherLoader.active = false;
89+ // Loader.status might be Loader.Null and Loader.item might be null but the Loader
90+ // item might still be alive. So if we set Loader.active back to true
91+ // again right now we will get the very same Shell instance back. So no reload
92+ // actually took place. Likely because Loader waits until the next event loop
93+ // iteration to do its work. So to ensure the reload, we will wait until the
94+ // Shell instance gets destroyed.
95+ tryCompare(launcherLoader, "itemDestroyed", true);
96+ launcherLoader.active = true;
6497 }
65− }
98+ function init() {
99+ var listView = findChild(launcher, "launcherListView");
100+ // wait for it to settle before doing the flick. Otherwise the flick
101+ // might fail.
102+ // On startup that listView is already moving. maybe because of its contents
103+ // growing while populating it with icons etc.
104+ tryCompare(listView, "flicking", false);
66105
67− UT.UnityTestCase {
68− id: revealer
69− when: windowShown
106+ // Make sure noone changed the height of the window. The issue this test case
107+ // is verifying only happens on certain heights of the Launcher
108+ compare(root.height, units.gu(55));
109+
110+ compare(listView.contentY, -listView.topMargin, "Launcher did not start up with first item unfolded");
111+
112+ // Now do check that snapping is in fact enabled
113+ compare(listView.snapMode, ListView.SnapToItem, "Snapping is not enabled");
114+
115+ // Tests can be run in a reaaaaally slow environment or machine. Thus ensure
116+ // the dismissTimer doesn't time out inadvertently.
117+ var dismissTimer = findInvisibleChild(launcher, "dismissTimer");
118+ dismissTimer.interval = 60 * 60 * 1000; // one hour
119+ }
70120
71121 function dragLauncherIntoView() {
72122 var startX = launcher.dragAreaWidth/2;
@@ -87,22 +137,11 @@ Item {
87137 var panel = findChild(launcher, "launcherPanel");
88138 tryCompare(panel, "x", -panel.width, 1000);
89139 }
90− }
91−
92− UT.UnityTestCase {
93− id: testCase
94− name: "Launcher"
95− when: windowShown && initTestCase.completed
96−
97− function cleanup() {
98− mouseClick(root, root.width / 2, root.height / 2);
99− revealer.waitUntilLauncherDisappears();
100− }
101140
102141 // Drag from the left edge of the screen rightwards and check that the launcher
103142 // appears (as if being dragged by the finger/pointer)
104143 function test_dragLeftEdgeToRevealLauncherAndTapCenterToDismiss() {
105− revealer.waitUntilLauncherDisappears();
144+ waitUntilLauncherDisappears();
106145
107146 var panel = findChild(launcher, "launcherPanel")
108147 verify(panel != undefined)
@@ -110,7 +149,7 @@ Item {
110149 // it starts out hidden just left of the left launcher edge
111150 compare(panel.x, -panel.width)
112151
113− revealer.dragLauncherIntoView()
152+ dragLauncherIntoView()
114153
115154 // tapping on the center of the screen should dismiss the launcher
116155 mouseClick(launcher, launcher.width/2, launcher.height/2)
@@ -124,7 +163,7 @@ Item {
124163 corresponding desktop file. E.g. clicking on phone icon should yield
125164 launcherApplicationSelected("[...]dialer-app.desktop") */
126165 function test_clickingOnAppIconCausesSignalEmission() {
127− revealer.dragLauncherIntoView();
166+ dragLauncherIntoView();
128167 launcher.lastSelectedApplication = ""
129168
130169 var listView = findChild(launcher, "launcherListView");
@@ -140,7 +179,7 @@ Item {
140179 "dialer-app")
141180
142181 // Tapping on an application icon also dismisses the launcher
143− revealer.waitUntilLauncherDisappears()
182+ waitUntilLauncherDisappears()
144183 }
145184
146185 /* If I click on the dash icon on the launcher
@@ -148,7 +187,7 @@ Item {
148187 function test_clickingOnDashIconCausesSignalEmission() {
149188 launcher.showDashHome_count = 0
150189
151− revealer.dragLauncherIntoView()
190+ dragLauncherIntoView()
152191
153192 var dashIcon = findChild(launcher, "dashItem")
154193 verify(dashIcon != undefined)
@@ -158,7 +197,7 @@ Item {
158197 tryCompare(launcher, "showDashHome_count", 1)
159198
160199 // Tapping on the dash icon also dismisses the launcher
161− revealer.waitUntilLauncherDisappears()
200+ waitUntilLauncherDisappears()
162201 }
163202
164203 function test_teaseLauncher_data() {
@@ -185,12 +224,12 @@ Item {
185224 wait(100)
186225 compare(launcher.maxPanelX, -launcher.panelWidth, "Launcher moved even if it shouldn't")
187226 }
188− revealer.waitUntilLauncherDisappears();
227+ waitUntilLauncherDisappears();
189228 launcher.available = true;
190229 }
191230
192231 function test_countEmblems() {
193− revealer.dragLauncherIntoView();
232+ dragLauncherIntoView();
194233 var launcherListView = findChild(launcher, "launcherListView");
195234 for (var i = 0; i < launcherListView.count; ++i) {
196235 var delegate = findChild(launcherListView, "launcherDelegate" + i)
@@ -201,7 +240,7 @@ Item {
201240 }
202241
203242 function test_progressOverlays() {
204− revealer.dragLauncherIntoView();
243+ dragLauncherIntoView();
205244 var launcherListView = findChild(launcher, "launcherListView");
206245 for (var i = 0; i < launcherListView.count; ++i) {
207246 var delegate = findChild(launcherListView, "launcherDelegate" + i)
@@ -210,35 +249,49 @@ Item {
210249 }
211250
212251 function test_focusedHighlight() {
213− revealer.dragLauncherIntoView();
252+ dragLauncherIntoView();
214253 var launcherListView = findChild(launcher, "launcherListView");
215254 for (var i = 0; i < launcherListView.count; ++i) {
216255 var delegate = findChild(launcherListView, "launcherDelegate" + i)
217256 compare(findChild(delegate, "focusedHighlight").visible, LauncherModel.get(i).focused)
218257 }
219258 }
220− }
221−
222− UT.UnityTestCase {
223− id: clickFlickTestCase
224− when: windowShown && testCase.completed
225259
226260 function test_clickFlick_data() {
227261 var listView = findChild(launcher, "launcherListView");
228262 return [
229− {tag: "unfolded top", flickSpeed: units.gu(200), clickY: listView.topMargin + units.gu(2), expectFlick: false},
230− {tag: "folded top", flickSpeed: -units.gu(200), clickY: listView.topMargin + units.gu(2), expectFlick: true},
231− {tag: "unfolded bottom", flickSpeed: -units.gu(200), clickY: listView.height - listView.topMargin - units.gu(1), expectFlick: false},
232− {tag: "folded bottom", flickSpeed: units.gu(200), clickY: listView.height - listView.topMargin - units.gu(1), expectFlick: true},
263+ {tag: "unfolded top", positionViewAtBeginning: false,
264+ clickY: listView.topMargin + units.gu(2),
265+ expectFlick: false},
266+
267+ {tag: "folded top", positionViewAtBeginning: true,
268+ clickY: listView.topMargin + units.gu(2),
269+ expectFlick: true},
270+
271+ {tag: "unfolded bottom", positionViewAtBeginning: true,
272+ clickY: listView.height - listView.topMargin - units.gu(1),
273+ expectFlick: false},
274+
275+ {tag: "folded bottom", positionViewAtBeginning: false,
276+ clickY: listView.height - listView.topMargin - units.gu(1),
277+ expectFlick: true},
233278 ];
234279 }
235280
236281 function test_clickFlick(data) {
237282 launcher.lastSelectedApplication = "";
238− revealer.dragLauncherIntoView();
283+ dragLauncherIntoView();
239284 var listView = findChild(launcher, "launcherListView");
240285
241− listView.flick(0, data.flickSpeed);
286+ // flicking is unreliable. sometimes it works, sometimes the
287+ // list view moves just a tiny bit or not at all, making tests fail.
288+ // So for stability's sake we just put the listView in the position
289+ // we want to to actually start doing what this tests intends to check.
290+ if (data.positionViewAtBeginning) {
291+ listView.positionViewAtBeginning();
292+ } else {
293+ listView.positionViewAtEnd();
294+ }
242295 tryCompare(listView, "flicking", false);
243296
244297 var oldY = listView.contentY;
@@ -253,44 +306,7 @@ Item {
253306 verify(launcher.lastSelectedApplication != "");
254307 compare(listView.contentY, oldY, "Launcher was flicked even though it should only launch an app");
255308 }
256−
257− // Restore position on top
258− listView.flick(0, units.gu(200));
259− tryCompare(listView, "flicking", false)
260− // Click somewhere in the empty space to make it hide in case it isn't
261− mouseClick(launcher, launcher.width - units.gu(1), units.gu(1));
262− revealer.waitUntilLauncherDisappears();
263309 }
264− }
265−
266− UT.UnityTestCase {
267− id: initTestCase
268− name: "LauncherInit"
269− when: windowShown
270−
271− function initTestCase() {
272− var listView = findChild(launcher, "launcherListView");
273− tryCompare(listView, "flicking", false)
274− }
275−
276− function test_initFirstUnfolded() {
277−
278− // Make sure noone changed the height of the window. The issue this test case
279− // is verifying only happens on certain heights of the Launcher
280− compare(root.height, units.gu(55));
281−
282− var listView = findChild(launcher, "launcherListView");
283− compare(listView.contentY, -listView.topMargin, "Launcher did not start up with first item unfolded");
284−
285− // Now do check that snapping is in fact enabled
286− compare(listView.snapMode, ListView.SnapToItem, "Snapping is not enabled");
287− }
288− }
289−
290− UT.UnityTestCase {
291− id: dndTestCase
292− name: "Drag and Drop"
293− when: windowShown && initTestCase.completed
294310
295311 function test_dragndrop_data() {
296312 return [
@@ -301,7 +317,7 @@ Item {
301317 }
302318
303319 function test_dragndrop(data) {
304− revealer.dragLauncherIntoView();
320+ dragLauncherIntoView();
305321 var draggedItem = findChild(launcher, "launcherDelegate4")
306322 var item0 = findChild(launcher, "launcherDelegate0")
307323 var fakeDragItem = findChild(launcher, "fakeDragItem")
@@ -366,11 +382,11 @@ Item {
366382
367383 // Click somewhere in the empty space to make it hide in case it isn't
368384 mouseClick(launcher, launcher.width - units.gu(1), units.gu(1));
369− revealer.waitUntilLauncherDisappears();
385+ waitUntilLauncherDisappears();
370386 }
371387
372388 function test_quicklist_dismiss() {
373− revealer.dragLauncherIntoView();
389+ dragLauncherIntoView();
374390 var draggedItem = findChild(launcher, "launcherDelegate5")
375391 var item0 = findChild(launcher, "launcherDelegate0")
376392 var fakeDragItem = findChild(launcher, "fakeDragItem")
@@ -401,7 +417,7 @@ Item {
401417 }
402418
403419 function test_quicklist_positioning(data) {
404− revealer.dragLauncherIntoView();
420+ dragLauncherIntoView();
405421 var quickList = findChild(launcher, "quickList")
406422 var draggedItem = findChild(launcher, "launcherDelegate" + data.itemIndex)
407423 var quickListShape = findChild(launcher, "quickListShape")
@@ -428,7 +444,7 @@ Item {
428444
429445 // Click somewhere in the empty space to dismiss the launcher
430446 mouseClick(launcher, launcher.width - units.gu(1), units.gu(1));
431− revealer.waitUntilLauncherDisappears()
447+ waitUntilLauncherDisappears()
432448 }
433449
434450 function test_quicklist_click_data() {
@@ -439,7 +455,7 @@ Item {
439455 }
440456
441457 function test_quicklist_click(data) {
442− revealer.dragLauncherIntoView();
458+ dragLauncherIntoView();
443459 var clickedItem = findChild(launcher, "launcherDelegate5")
444460 var quickList = findChild(launcher, "quickList")
445461 var quickListShape = findChild(launcher, "quickListShape")
tests/qmltests/Stages/tst_ApplicationWindow.qml+2−2
@@ -51,6 +51,7 @@ Rectangle {
5151 anchors.fill: parent
5252 application: fakeApplication
5353 orientation: Qt.PortraitOrientation
54+ interactive: true
5455 }
5556 }
5657 Loader {
@@ -375,8 +376,7 @@ Rectangle {
375376
376377 waitUntilTransitionsEnd();
377378
378− // Because doing stuff in C++ is a PITA we keep the counter in the interal qml impl.
379− var fakeSurface = findChild(fakeSession.surface, "fakeSurfaceQML");
379+ var fakeSurface = fakeSession.surface;
380380 fakeSurface.touchPressCount = 0;
381381 fakeSurface.touchReleaseCount = 0;
382382
tests/qmltests/Stages/tst_PhoneStage.qml+2−1
@@ -178,7 +178,8 @@ Item {
178178 var startX = phoneStage.width - 2;
179179 var startY = phoneStage.height / 2;
180180 var endY = startY;
181− var endX = spreadView.width - (spreadView.width * spreadView[data.positionMarker]) - data.offset;
181+ var endX = spreadView.width - (spreadView.width * spreadView[data.positionMarker]) - data.offset
182+ - phoneStage.dragAreaWidth;
182183
183184 var oldFocusedApp = ApplicationManager.get(0);
184185 var newFocusedApp = ApplicationManager.get(data.newFocusedIndex);
tests/qmltests/Stages/tst_SurfaceContainer.qml+26−1
@@ -99,6 +99,9 @@ Rectangle {
9999 name: "SurfaceContainer"
100100 when: windowShown
101101
102+ property var surfaceContainer:
103+ surfaceContainerLoader.status === Loader.Ready ? surfaceContainerLoader.item : null
104+
102105 function cleanup() {
103106 // reload our test subject to get it in a fresh state once again
104107 surfaceContainerLoader.active = false;
@@ -126,7 +129,6 @@ Rectangle {
126129
127130 function test_animateRemoval() {
128131 surfaceCheckbox.checked = true;
129− var surfaceContainer = surfaceContainerLoader.item;
130132
131133 verify(surfaceContainer.surface !== null);
132134
@@ -135,5 +137,28 @@ Rectangle {
135137 compare(surfaceContainer.state, "zombie");
136138 tryCompare(surfaceContainer, "surface", null);
137139 }
140+
141+ function test_surfaceGetsNoTouchesWhenContainerNotInteractive() {
142+ surfaceCheckbox.checked = true;
143+ verify(surfaceContainer.surface !== null);
144+
145+ surfaceContainer.surface.touchPressCount = 0;
146+ surfaceContainer.surface.touchReleaseCount = 0;
147+
148+ surfaceContainer.interactive = true;
149+ tap(surfaceContainer, surfaceContainer.width / 2, surfaceContainer.height / 2);
150+
151+ // surface got touches as the surfaceContainer is interactive
152+ compare(surfaceContainer.surface.touchPressCount, 1)
153+ compare(surfaceContainer.surface.touchReleaseCount, 1);
154+
155+ surfaceContainer.interactive = false;
156+ tap(surfaceContainer, surfaceContainer.width / 2, surfaceContainer.height / 2);
157+
158+ // surface shouldn't get the touches from the second tap as the surfaceContainer
159+ // was *not* interactive when it happened.
160+ compare(surfaceContainer.surface.touchPressCount, 1)
161+ compare(surfaceContainer.surface.touchReleaseCount, 1);
162+ }
138163 }
139164}
tests/qmltests/tst_Shell.qml+118−3
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013 Canonical, Ltd.
2+ * Copyright (C) 2013-2014 Canonical, Ltd.
33 *
44 * Authors:
55 * Daniel d'Andrada <daniel.dandrada@canonical.com>
@@ -98,6 +98,11 @@ Row {
9898 }
9999 }
100100
101+ SignalSpy {
102+ id: launcherShowDashHomeSpy
103+ signalName: "showDashHome"
104+ }
105+
101106 SignalSpy {
102107 id: sessionSpy
103108 signalName: "sessionStarted"
@@ -131,9 +136,14 @@ Row {
131136
132137 sessionSpy.target = findChild(shell, "greeter")
133138 dashCommunicatorSpy.target = findInvisibleChild(shell, "dashCommunicator");
139+
140+ var launcher = findChild(shell, "launcher");
141+ launcherShowDashHomeSpy.target = launcher;
134142 }
135143
136144 function cleanup() {
145+ launcherShowDashHomeSpy.target = null;
146+
137147 shellLoader.itemDestroyed = false;
138148
139149 shellLoader.active = false;
@@ -404,8 +414,7 @@ Row {
404414 touchFlick(launcherPanel, touchStartX, touchStartY, touchStartX, 0);
405415 tryCompare(launcherPanel, "moving", false);
406416
407− // NB tapping (i.e., using touch events) doesn't activate the icon... go figure...
408− mouseClick(appIcon, appIcon.width / 2, appIcon.height / 2);
417+ tap(appIcon, appIcon.width / 2, appIcon.height / 2);
409418 }
410419
411420 function showIndicators() {
@@ -572,5 +581,111 @@ Row {
572581
573582 tryCompare(greeter, "showProgress", data.unlocked ? 0 : 1);
574583 }
584+
585+ function test_tapOnRightEdgeReachesApplicationSurface() {
586+ var topmostSpreadDelegate = findChild(shell, "appDelegate0");
587+ var topmostSurface = findChild(topmostSpreadDelegate, "surfaceContainer").surface;
588+ var rightEdgeDragArea = findChild(shell, "spreadDragArea");
589+
590+ topmostSurface.touchPressCount = 0;
591+ topmostSurface.touchReleaseCount = 0;
592+
593+ var tapPoint = rightEdgeDragArea.mapToItem(shell, rightEdgeDragArea.width / 2,
594+ rightEdgeDragArea.height / 2);
595+
596+ tap(shell, tapPoint.x, tapPoint.y);
597+
598+ tryCompare(topmostSurface, "touchPressCount", 1);
599+ tryCompare(topmostSurface, "touchReleaseCount", 1);
600+ }
601+
602+ /*
603+ Perform a right edge drag over an application surface and check
604+ that no touch event was sent to it (ie, they were all consumed
605+ by the right-edge drag area)
606+ */
607+ function test_rightEdgeDragDoesNotReachApplicationSurface() {
608+ var topmostSpreadDelegate = findChild(shell, "appDelegate0");
609+ var topmostSurface = findChild(topmostSpreadDelegate, "surfaceContainer").surface;
610+ var rightEdgeDragArea = findChild(shell, "spreadDragArea");
611+
612+ topmostSurface.touchPressCount = 0;
613+ topmostSurface.touchReleaseCount = 0;
614+
615+ var gestureStartPoint = rightEdgeDragArea.mapToItem(shell, rightEdgeDragArea.width / 2,
616+ rightEdgeDragArea.height / 2);
617+
618+ touchFlick(shell,
619+ gestureStartPoint.x /* fromX */, gestureStartPoint.y /* fromY */,
620+ units.gu(1) /* toX */, gestureStartPoint.y /* toY */);
621+
622+ tryCompare(topmostSurface, "touchPressCount", 0);
623+ tryCompare(topmostSurface, "touchReleaseCount", 0);
624+ }
625+
626+ function waitUntilFocusedApplicationIsShowingItsSurface()
627+ {
628+ var spreadDelegate = findChild(shell, "appDelegate0");
629+ var appState = findInvisibleChild(spreadDelegate, "applicationWindowStateGroup");
630+ tryCompare(appState, "state", "surface");
631+ var transitions = appState.transitions;
632+ for (var i = 0; i < transitions.length; ++i) {
633+ var transition = transitions[i];
634+ tryCompare(transition, "running", false, 2000);
635+ }
636+ }
637+
638+ function swipeFromRightEdgeToShowAppSpread()
639+ {
640+ // perform a right-edge drag to show the spread
641+ var touchStartX = shell.width - (shell.edgeSize / 2)
642+ var touchStartY = shell.height / 2;
643+ touchFlick(shell, touchStartX, touchStartY, units.gu(1) /* endX */, touchStartY /* endY */);
644+
645+ // check if it's indeed showing the spread
646+ var stage = findChild(shell, "stage");
647+ var spreadView = findChild(stage, "spreadView");
648+ tryCompare(spreadView, "phase", 2);
649+ }
650+
651+ function test_tapUbuntuIconInLauncherOverAppSpread() {
652+
653+ waitUntilFocusedApplicationIsShowingItsSurface();
654+
655+ swipeFromRightEdgeToShowAppSpread();
656+
657+ var launcher = findChild(shell, "launcher");
658+
659+ // ensure the launcher dimissal timer never gets triggered during the test run
660+ var dismissTimer = findInvisibleChild(launcher, "dismissTimer");
661+ dismissTimer.interval = 60 * 60 * 1000;
662+
663+ dragLauncherIntoView();
664+
665+ // Emulate a tap with a finger, where the touch position drifts during the tap.
666+ {
667+ var buttonShowDashHome = findChild(launcher, "buttonShowDashHome");
668+ var startPos = buttonShowDashHome.mapToItem(shell,
669+ buttonShowDashHome.width * 0.2,
670+ buttonShowDashHome.height * 0.2);
671+ var endPos = buttonShowDashHome.mapToItem(shell,
672+ buttonShowDashHome.width * 0.8,
673+ buttonShowDashHome.height * 0.8);
674+ touchFlick(shell, startPos.x, startPos.y, endPos.x, endPos.y);
675+ }
676+
677+ compare(launcherShowDashHomeSpy.count, 1);
678+
679+ // check that the stage has left spread mode.
680+ {
681+ var stage = findChild(shell, "stage");
682+ var spreadView = findChild(stage, "spreadView");
683+ tryCompare(spreadView, "phase", 0);
684+ }
685+
686+ // check that the launcher got dismissed
687+ var launcherPanel = findChild(shell, "launcherPanel");
688+ tryCompare(launcherPanel, "x", -launcherPanel.width);
689+ }
575690 }
576691}
tests/uqmlscene/CMakeLists.txt+2
@@ -10,6 +10,7 @@ pkg_check_modules(XCB REQUIRED xcb)
1010include_directories(
1111 ${CMAKE_SOURCE_DIR}/src
1212 ${Qt5Gui_PRIVATE_INCLUDE_DIRS}
13+ ${CMAKE_SOURCE_DIR}/libs/UbuntuGestures
1314)
1415
1516if (NOT "${XCB_INCLUDE_DIRS}" STREQUAL "")
@@ -20,4 +21,5 @@ target_link_libraries(uqmlscene
2021 ${Qt5Qml_LIBRARIES}
2122 ${Qt5Quick_LIBRARIES}
2223 ${XCB_LDFLAGS}
24+ UbuntuGestures
2325 )
tests/uqmlscene/README+3−1
@@ -1 +1,3 @@
1−Qt's qmlscene tool modified to use MouseTouchAdaptor. That way we can interact with touch-only components
1+Qt's qmlscene tool with the following modifications:
2+ - Added MouseTouchAdaptor. That way we can interact with touch-only components
3+ - Added TouchRegistry. So that DirectionalDragArea, TouchGate and others can negotiate touch ownership etc.
tests/uqmlscene/main.cpp+9
@@ -65,6 +65,9 @@
6565
6666#include <MouseTouchAdaptor.h>
6767
68+// UbuntuGestures lib
69+#include <TouchRegistry.h>
70+
6871#ifdef QML_RUNTIME_TESTING
6972class RenderStatistics
7073{
@@ -496,6 +499,12 @@ int main(int argc, char ** argv)
496499 QQuickItem *contentItem = qobject_cast<QQuickItem *>(topLevel);
497500 if (contentItem) {
498501 qxView = new QQuickView(&engine, nullptr);
502+ if (TouchRegistry::instance() == nullptr) {
503+ new TouchRegistry(qxView);
504+ } else {
505+ TouchRegistry::instance()->setParent(qxView);
506+ }
507+ qxView->installEventFilter(TouchRegistry::instance());
499508 window = qxView;
500509 // Set window default properties; the qml can still override them
501510 QString oname = contentItem->objectName();
tests/utils/modules/Unity/Test/CMakeLists.txt+2
@@ -4,6 +4,7 @@ add_definitions(-DQT_NO_KEYWORDS)
44
55include_directories(
66 ${Qt5Gui_PRIVATE_INCLUDE_DIRS}
7+ ${CMAKE_SOURCE_DIR}/libs/UbuntuGestures
78)
89
910set(UnityTestQML_SOURCES
@@ -13,6 +14,7 @@ set(UnityTestQML_SOURCES
1314)
1415
1516add_library(UnityTestQml MODULE ${UnityTestQML_SOURCES})
17+target_link_libraries(UnityTestQml UbuntuGestures)
1618
1719qt5_use_modules(UnityTestQml Core Quick Test)
1820
tests/utils/modules/Unity/Test/UnityTestCase.qml+2
@@ -344,6 +344,8 @@ TestCase {
344344 }
345345
346346 Component.onCompleted: {
347+ UT.Util.ensureTouchRegistryInstalled();
348+
347349 var rootItem = parent;
348350 while (rootItem.parent != undefined) {
349351 rootItem = rootItem.parent;
tests/utils/modules/Unity/Test/testutil.cpp+41−2
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2012, 2013 Canonical, Ltd.
2+ * Copyright (C) 2012, 2013, 2014 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -19,11 +19,19 @@
1919
2020#include <qpa/qwindowsysteminterface.h>
2121#include <QtGui/QGuiApplication>
22+#include <QQuickView>
23+
24+// UbuntuGestures lib
25+#include <TouchRegistry.h>
26+#include <Timer.h>
27+
28+using namespace UbuntuGestures;
2229
2330TestUtil::TestUtil(QObject *parent)
2431 : QObject(parent)
2532 , m_targetWindow(0)
2633 , m_touchDevice(0)
34+ , m_installedTouchRegistry(false)
2735{
2836}
2937
@@ -52,6 +60,7 @@ TouchEventSequenceWrapper *TestUtil::touchEvent()
5260{
5361 ensureTargetWindow();
5462 ensureTouchDevice();
63+ ensureTouchRegistryInstalled();
5564
5665 return new TouchEventSequenceWrapper(
5766 QTest::touchEvent(m_targetWindow, m_touchDevice, /* autoCommit */ false));
@@ -59,7 +68,7 @@ TouchEventSequenceWrapper *TestUtil::touchEvent()
5968
6069void TestUtil::ensureTargetWindow()
6170{
62− if (!m_targetWindow)
71+ if (!m_targetWindow && !QGuiApplication::topLevelWindows().isEmpty())
6372 m_targetWindow = QGuiApplication::topLevelWindows()[0];
6473}
6574
@@ -71,3 +80,33 @@ void TestUtil::ensureTouchDevice()
7180 QWindowSystemInterface::registerTouchDevice(m_touchDevice);
7281 }
7382}
83+
84+void TestUtil::ensureTouchRegistryInstalled()
85+{
86+ if (m_installedTouchRegistry)
87+ return;
88+
89+ TouchRegistry *touchRegistry;
90+ if (TouchRegistry::instance() == nullptr) {
91+ // Tests can be *very* slow to run and we don't want things timing out because
92+ // of that. So give it fake timers to use (they will never time out)
93+ touchRegistry = new TouchRegistry(this, new FakeTimerFactory);
94+ } else {
95+ touchRegistry = TouchRegistry::instance();
96+ if (touchRegistry->parent() != this) {
97+ // someone else created it. leave it alone
98+ m_installedTouchRegistry = true;
99+ }
100+ }
101+
102+ ensureTargetWindow();
103+
104+ if (m_targetWindow) {
105+ QQuickView *view = qobject_cast<QQuickView*>(m_targetWindow);
106+ if (view) {
107+ view->installEventFilter(touchRegistry);
108+ touchRegistry->setParent(view);
109+ m_installedTouchRegistry = true;
110+ }
111+ }
112+}
tests/utils/modules/Unity/Test/testutil.h+3−1
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2012, 2013 Canonical, Ltd.
2+ * Copyright (C) 2012, 2013, 2014 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -34,6 +34,7 @@ public:
3434
3535 Q_INVOKABLE bool isInstanceOf(QObject*, QString);
3636 Q_INVOKABLE TouchEventSequenceWrapper *touchEvent();
37+ Q_INVOKABLE void ensureTouchRegistryInstalled();
3738
3839private:
3940 void ensureTargetWindow();
@@ -41,6 +42,7 @@ private:
4142
4243 QWindow *m_targetWindow;
4344 QTouchDevice *m_touchDevice;
45+ bool m_installedTouchRegistry;
4446};
4547
4648QML_DECLARE_TYPE(TestUtil)