on xenial help sign in
merge the kraken
commit978a8178740fbb10ed8dded31e8eda2311d97d24 authorMichael Zanetti <michael.zanetti@canonical.com> dateMon 13 Oct 2014, 14:48 (12 years ago) parent1125a39ef63f10
79 files changed+4623−470
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+118−102 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
@@ -23,6 +23,7 @@ import "../Components"
2323Item {
2424 id: dashContent
2525
26+ property bool forceNonInteractive: false
2627 property alias scopes: dashContentList.model
2728 readonly property alias currentIndex: dashContentList.currentIndex
2829 readonly property string currentScopeId: dashContentList.currentItem ? dashContentList.currentItem.scopeId : ""
@@ -101,7 +102,8 @@ Item {
101102 id: dashContentList
102103 objectName: "dashContentList"
103104
104− interactive: dashContent.scopes.loaded && currentItem && !currentItem.moving && !currentItem.navigationShown && !currentItem.subPageShown
105+ interactive: !dashContent.forceNonInteractive && dashContent.scopes.loaded && currentItem
106+ && !currentItem.moving && !currentItem.navigationShown && !currentItem.subPageShown
105107 anchors.fill: parent
106108 orientation: ListView.Horizontal
107109 boundsBehavior: Flickable.DragAndOvershootBounds
@@ -180,6 +182,7 @@ Item {
180182 item.paginationCount = Qt.binding(function() { return dashContentList.count } )
181183 item.paginationIndex = Qt.binding(function() { return dashContentList.currentIndex } )
182184 item.holdingList = dashContentList;
185+ item.forceNonInteractive = Qt.binding(function() { return dashContent.forceNonInteractive } )
183186 }
184187 Connections {
185188 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
@@ -45,6 +45,7 @@ Rectangle {
4545 }
4646
4747 Rectangle {
48+ objectName: "buttonShowDashHome"
4849 width: parent.width
4950 height: units.gu(7)
5051 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
@@ -131,7 +131,7 @@ Rectangle {
131131
132132 // This indicates when the spreadView is active. That means, all the animations
133133 // are activated and tiles need to line up for the spread.
134− readonly property bool active: shiftedContentX > 0 || spreadDragArea.dragging
134+ readonly property bool active: shiftedContentX > 0 || spreadDragArea.status === DirectionalDragArea.Recognized
135135
136136 // The flickable needs to fill the screen in order to get touch events all over.
137137 // However, we don't want to the user to be able to scroll back all the way. For
@@ -418,6 +418,7 @@ Rectangle {
418418
419419 EdgeDragArea {
420420 id: spreadDragArea
421+ objectName: "spreadDragArea"
421422 direction: Direction.Leftwards
422423 enabled: spreadView.phase != 2 && root.spreadEnabled
423424
@@ -437,9 +438,10 @@ Rectangle {
437438 spreadView.phase = 0;
438439 spreadView.contentX = -spreadView.shift;
439440 }
440− if (dragging && attachedToView) {
441+ if (dragging && status == DirectionalDragArea.Recognized && attachedToView) {
441442 // Gesture recognized. Let's move the spreadView with the finger
442− spreadView.contentX = -touchX + spreadDragArea.width - spreadView.shift;
443+ var finalX = Math.min(touchX + width, width);
444+ spreadView.contentX = -finalX + spreadDragArea.width - spreadView.shift;
443445 }
444446 if (attachedToView && spreadView.shiftedContentX >= spreadView.width * spreadView.positionMarker3) {
445447 // We passed positionMarker3. Detach from spreadView and snap it.
@@ -449,7 +451,13 @@ Rectangle {
449451 gesturePoints.push(touchX);
450452 }
451453
454+ property int previousStatus: -1
455+ property int currentStatus: DirectionalDragArea.WaitingForTouch
456+
452457 onStatusChanged: {
458+ previousStatus = currentStatus;
459+ currentStatus = status;
460+
453461 if (status == DirectionalDragArea.Recognized) {
454462 attachedToView = true;
455463 }
@@ -457,7 +465,7 @@ Rectangle {
457465
458466 onDraggingChanged: {
459467 if (dragging) {
460− // Gesture recognized. Start recording this gesture
468+ // A potential edge-drag gesture has started. Start recording it
461469 gesturePoints = [];
462470 return;
463471 }
@@ -474,7 +482,8 @@ Rectangle {
474482 }
475483 gesturePoints = [];
476484
477− if (oneWayFlick && spreadView.shiftedContentX > units.gu(2) &&
485+ if (previousStatus == DirectionalDragArea.Recognized &&
486+ oneWayFlick && spreadView.shiftedContentX > units.gu(2) &&
478487 spreadView.shiftedContentX < spreadView.positionMarker1 * spreadView.width) {
479488 // If it was a short one-way movement, do the Alt+Tab switch
480489 // 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+118−102
@@ -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")
@@ -402,9 +418,9 @@ Item {
402418
403419 function test_quicklist_positioning(data) {
404420 revealer.dragLauncherIntoView();
405− var quickList = findChild(launcher, "quickList");
406− var draggedItem = findChild(launcher, "launcherDelegate" + data.itemIndex);
407− var quickListShape = findChild(launcher, "quickListShape");
421+ var quickList = findChild(launcher, "quickList")
422+ var draggedItem = findChild(launcher, "launcherDelegate" + data.itemIndex)
423+ var quickListShape = findChild(launcher, "quickListShape")
408424
409425 // Position launcher to where we need it
410426 var listView = findChild(launcher, "launcherListView");
@@ -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
@@ -335,6 +335,8 @@ TestCase {
335335 }
336336
337337 Component.onCompleted: {
338+ UT.Util.ensureTouchRegistryInstalled();
339+
338340 var rootItem = parent;
339341 while (rootItem.parent != undefined) {
340342 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)