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

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

commit3f3f8543851ddda295056db5b96242e97d72ce73 authorDaniel d'Andrada <daniel.dandrada@canonical.com> dateWed 1 Oct 2014, 10:20 (12 years ago) parent6ed5fb3
65 files changed+4108−347
CMakeLists.txt+2 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+183 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+299−189 plugins/Ubuntu/Gestures/DirectionalDragArea.h+37−29 plugins/Ubuntu/Gestures/TimeSource.h+2−2 plugins/Ubuntu/Gestures/TouchGate.cpp+322 plugins/Ubuntu/Gestures/TouchGate.h+97 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/Stages/ApplicationWindow.qml−17 qml/Stages/PhoneStage.qml+1 qml/Stages/SessionContainer.qml+1−13 qml/Stages/SurfaceContainer.qml+33−6 src/CMakeLists.txt+6 src/Dash/CMakeLists.txt+3 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/MirSurfaceItem.cpp+18−22 tests/mocks/Unity/Application/MirSurfaceItem.h+18−1 tests/mocks/Unity/Application/MirSurfaceItem.qml−12 tests/plugins/Ubuntu/Gestures/CMakeLists.txt+4−2 tests/plugins/Ubuntu/Gestures/RightwardsLauncher.qml+3 tests/plugins/Ubuntu/Gestures/touchGateExample.qml+27 tests/plugins/Ubuntu/Gestures/tst_DirectionalDragArea.cpp+415−14 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/Stages/tst_ApplicationWindow.qml+1−2 tests/qmltests/Stages/tst_SurfaceContainer.qml+26−1 tests/qmltests/tst_Shell.qml+42−1 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+37−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 "")
@@ -106,6 +107,7 @@ include_directories(
106107
107108# add subdirectories to build
108109add_subdirectory(include)
110+add_subdirectory(libs)
109111add_subdirectory(src)
110112add_subdirectory(tools)
111113add_subdirectory(qml)
debian/unity8-private.install+1
@@ -8,5 +8,6 @@ usr/lib/*/unity8/qml/SessionBroadcast
88usr/lib/*/unity8/qml/Ubuntu
99usr/lib/*/unity8/qml/Unity
1010usr/lib/*/unity8/qml/Utils
11+usr/lib/*/unity8/libUbuntuGestures*
1112usr/share/accountsservice/interfaces
1213usr/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+183
@@ -0,0 +1,183 @@
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+
96+ virtual ~TouchRegistry();
97+
98+ // Returns a pointer to the application's TouchRegistry instance.
99+ // If no instance has been allocated, null is returned.
100+ static TouchRegistry *instance() { return m_instance; }
101+
102+ void update(const QTouchEvent *event);
103+
104+ // Calls update() if the given event is a QTouchEvent
105+ bool eventFilter(QObject *watched, QEvent *event) override;
106+
107+ // An item that might later request ownership over the given touch point.
108+ // He will be kept informed about that touch point through UnownedTouchEvents
109+ // All candidates must eventually decide whether they want to own the touch point
110+ // or not. That decision is informed through requestTouchOwnership() or
111+ // removeCandidateOwnerForTouch()
112+ void addCandidateOwnerForTouch(int id, QQuickItem *candidate);
113+
114+ // The same as rejecting ownership of a touch
115+ void removeCandidateOwnerForTouch(int id, QQuickItem *candidate);
116+
117+ // The candidate object wants to be the owner of the touch with the given id.
118+ // If he's currently the oldest/top-most candidate, he will get an ownership
119+ // event immediately. If not, he will get ownership if (or once) he becomes the
120+ // top-most candidate.
121+ void requestTouchOwnership(int id, QQuickItem *candidate);
122+
123+ // An item that has no interest (effective or potential) in owning a touch point
124+ // but would nonetheless like to be kept up-to-date on its state.
125+ void addTouchWatcher(int touchId, QQuickItem *watcherItem);
126+
127+private Q_SLOTS:
128+ void rejectCandidateOwnerForTouch(int id, QQuickItem *candidate);
129+
130+private:
131+ // Useful for tests, where you should feed a fake timer
132+ TouchRegistry(QObject *parent, UbuntuGestures::AbstractTimerFactory *timerFactory);
133+
134+ class CandidateInfo {
135+ public:
136+ bool undecided;
137+ // TODO: Prune candidates that become null and resolve ownership accordingly.
138+ QPointer<QQuickItem> item;
139+ QPointer<UbuntuGestures::CandidateInactivityTimer> inactivityTimer;
140+ };
141+
142+ class TouchInfo {
143+ public:
144+ TouchInfo() : id(-1) {}
145+ TouchInfo(int id);
146+ bool isValid() const { return id >= 0; }
147+ void reset();
148+ void init(int id);
149+ int id;
150+ bool physicallyEnded;
151+ bool isOwned() const;
152+ bool ended() const;
153+ void notifyCandidatesOfOwnershipResolution();
154+
155+ // TODO optimize storage (s/QList/Pool)
156+ QList<CandidateInfo> candidates;
157+ QList<QPointer<QQuickItem>> watchers;
158+ };
159+
160+ Pool<TouchInfo>::Iterator findTouchInfo(int id);
161+
162+ void deliverTouchUpdatesToUndecidedCandidatesAndWatchers(const QTouchEvent *event);
163+
164+ static void translateTouchPointFromScreenToWindowCoords(QTouchEvent::TouchPoint &touchPoint);
165+
166+ static void dispatchPointsToItem(const QTouchEvent *event, const QList<int> &touchIds,
167+ QQuickItem *item);
168+ void freeEndedTouchInfos();
169+
170+ Pool<TouchInfo> m_touchInfoPool;
171+
172+ // the singleton instance
173+ static TouchRegistry *m_instance;
174+
175+ bool m_inDispatchLoop;
176+
177+ UbuntuGestures::AbstractTimerFactory *m_timerFactory;
178+
179+ friend class tst_TouchRegistry;
180+ friend class tst_DirectionalDragArea;
181+};
182+
183+#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+299−189
@@ -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,26 @@
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
1922#include <QtCore/qmath.h>
20−#include <QtCore/QTimer>
2123#include <QDebug>
2224
23−using namespace UbuntuGestures;
25+// local
26+#include "TouchOwnershipEvent.h"
27+#include "TouchRegistry.h"
28+#include "UnownedTouchEvent.h"
2429
25−#define DIRECTIONALDRAGAREA_DEBUG 0
30+using namespace UbuntuGestures;
2631
2732#if DIRECTIONALDRAGAREA_DEBUG
2833#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−}
34+#include "DebugHelpers.h"
7435
36+namespace {
7537const char *statusToString(DirectionalDragArea::Status status)
7638{
7739 if (status == DirectionalDragArea::WaitingForTouch) {
@@ -88,23 +50,6 @@ const char *statusToString(DirectionalDragArea::Status status)
8850#define DDA_DEBUG(msg) do{}while(0)
8951#endif // DIRECTIONALDRAGAREA_DEBUG
9052
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−};
10853
10954DirectionalDragArea::DirectionalDragArea(QQuickItem *parent)
11055 : QQuickItem(parent)
@@ -126,12 +71,14 @@ DirectionalDragArea::DirectionalDragArea(QQuickItem *parent)
12671 , m_timeSource(new RealTimeSource)
12772 , m_activeTouches(m_timeSource)
12873{
129− setRecognitionTimer(new RecognitionTimer(this));
74+ setRecognitionTimer(new Timer(this));
13075 m_recognitionTimer->setInterval(60);
76+ m_recognitionTimer->setSingleShot(false);
13177
13278 m_velocityCalculator = new AxisVelocityCalculator(this);
13379
134− connect(this, &QQuickItem::enabledChanged, this, &DirectionalDragArea::onEnabledChanged);
80+ connect(this, &QQuickItem::enabledChanged, this, &DirectionalDragArea::giveUpIfDisabledOrInvisible);
81+ connect(this, &QQuickItem::visibleChanged, this, &DirectionalDragArea::giveUpIfDisabledOrInvisible);
13582}
13683
13784Direction::Type DirectionalDragArea::direction() const
@@ -214,6 +161,7 @@ void DirectionalDragArea::setRecognitionTimer(UbuntuGestures::AbstractTimer *tim
214161{
215162 int interval = 0;
216163 bool timerWasRunning = false;
164+ bool wasSingleShot = false;
217165
218166 // can be null when called from the constructor
219167 if (m_recognitionTimer) {
@@ -226,6 +174,7 @@ void DirectionalDragArea::setRecognitionTimer(UbuntuGestures::AbstractTimer *tim
226174
227175 m_recognitionTimer = timer;
228176 timer->setInterval(interval);
177+ timer->setSingleShot(wasSingleShot);
229178 connect(timer, &UbuntuGestures::AbstractTimer::timeout,
230179 this, &DirectionalDragArea::checkSpeed);
231180 if (timerWasRunning) {
@@ -280,8 +229,131 @@ qreal DirectionalDragArea::touchSceneY() const
280229 return m_previousScenePos.y();
281230}
282231
232+bool DirectionalDragArea::event(QEvent *event)
233+{
234+ if (event->type() == TouchOwnershipEvent::touchOwnershipEventType()) {
235+ touchOwnershipEvent(static_cast<TouchOwnershipEvent *>(event));
236+ return true;
237+ } else if (event->type() == UnownedTouchEvent::unownedTouchEventType()) {
238+ unownedTouchEvent(static_cast<UnownedTouchEvent *>(event));
239+ return true;
240+ } else {
241+ return QQuickItem::event(event);
242+ }
243+}
244+
245+void DirectionalDragArea::touchOwnershipEvent(TouchOwnershipEvent *event)
246+{
247+ if (event->gained()) {
248+ QVector<int> ids;
249+ ids.append(event->touchId());
250+ grabTouchPoints(ids);
251+ } else {
252+ // We still wanna know when it ends for keeping the composition time window up-to-date
253+ TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
254+
255+ setStatus(WaitingForTouch);
256+ }
257+}
258+
259+void DirectionalDragArea::unownedTouchEvent(UnownedTouchEvent *unownedTouchEvent)
260+{
261+ QTouchEvent *event = unownedTouchEvent->touchEvent();
262+
263+ Q_ASSERT(!event->touchPointStates().testFlag(Qt::TouchPointPressed));
264+
265+ #if DIRECTIONALDRAGAREA_DEBUG
266+ // TODO Consider using qCDebug() when available (Qt 5.2)
267+ qDebug() << "[DDA] Unowned" << m_timeSource->msecsSinceReference()
268+ << qPrintable(touchEventToString(event));
269+ #endif
270+
271+ switch (m_status) {
272+ case WaitingForTouch:
273+ // do nothing
274+ break;
275+ case Undecided:
276+ Q_ASSERT(isEnabled() && isVisible());
277+ unownedTouchEvent_undecided(unownedTouchEvent);
278+ break;
279+ default: // Recognized:
280+ // do nothing
281+ break;
282+ }
283+
284+ m_activeTouches.update(event);
285+}
286+
287+void DirectionalDragArea::unownedTouchEvent_undecided(UnownedTouchEvent *unownedTouchEvent)
288+{
289+ const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(unownedTouchEvent->touchEvent());
290+ if (!touchPoint) {
291+ qCritical() << "DirectionalDragArea[status=Undecided]: touch " << m_touchId
292+ << "missing from UnownedTouchEvent without first reaching state Qt::TouchPointReleased. "
293+ "Considering it as released.";
294+
295+ TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
296+ setStatus(WaitingForTouch);
297+ return;
298+ }
299+
300+ const QPointF &touchScenePos = touchPoint->scenePos();
301+
302+ if (touchPoint->state() == Qt::TouchPointReleased) {
303+ // touch has ended before recognition concluded
304+ DDA_DEBUG("Touch has ended before recognition concluded");
305+ TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
306+ emitSignalIfTapped();
307+ setStatus(WaitingForTouch);
308+ return;
309+ }
310+
311+ m_previousDampedScenePos.setX(m_dampedScenePos.x());
312+ m_previousDampedScenePos.setY(m_dampedScenePos.y());
313+ m_dampedScenePos.update(touchScenePos);
314+ updateVelocityCalculator(touchScenePos);
315+
316+ if (!pointInsideAllowedArea()) {
317+ DDA_DEBUG("Rejecting gesture because touch point is outside allowed area.");
318+ TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
319+ // We still wanna know when it ends for keeping the composition time window up-to-date
320+ TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
321+ setStatus(WaitingForTouch);
322+ return;
323+ }
324+
325+ if (!movingInRightDirection()) {
326+ DDA_DEBUG("Rejecting gesture because touch point is moving in the wrong direction.");
327+ TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
328+ // We still wanna know when it ends for keeping the composition time window up-to-date
329+ TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
330+ setStatus(WaitingForTouch);
331+ return;
332+ }
333+
334+ setPreviousPos(touchPoint->pos());
335+ setPreviousScenePos(touchScenePos);
336+
337+ if (isWithinTouchCompositionWindow()) {
338+ // There's still time for some new touch to appear and ruin our party as it would be combined
339+ // with our m_touchId one and therefore deny the possibility of a single-finger gesture.
340+ DDA_DEBUG("Sill within composition window. Let's wait more.");
341+ return;
342+ }
343+
344+ if (movedFarEnough(touchScenePos)) {
345+ TouchRegistry::instance()->requestTouchOwnership(m_touchId, this);
346+ setStatus(Recognized);
347+ } else {
348+ DDA_DEBUG("Didn't move far enough yet. Let's wait more.");
349+ }
350+}
351+
283352void DirectionalDragArea::touchEvent(QTouchEvent *event)
284353{
354+ // TODO: Consider when more than one touch starts in the same event (although it's not possible
355+ // with Mir's android-input). Have to track them all. Consider it a plus/bonus.
356+
285357 #if DIRECTIONALDRAGAREA_DEBUG
286358 // TODO Consider using qCDebug() when available (Qt 5.2)
287359 qDebug() << "[DDA]" << m_timeSource->msecsSinceReference()
@@ -310,18 +382,23 @@ void DirectionalDragArea::touchEvent(QTouchEvent *event)
310382
311383void DirectionalDragArea::touchEvent_absent(QTouchEvent *event)
312384{
385+ // TODO: accept/reject is for the whole event, not per touch id. See how that affects us.
386+
313387 if (!event->touchPointStates().testFlag(Qt::TouchPointPressed)) {
314388 // Nothing to see here. No touch starting in this event.
315389 return;
316390 }
317391
392+ // to be proven wrong, if that's the case
393+ bool allGood = true;
394+
318395 if (isWithinTouchCompositionWindow()) {
319396 // too close to the last touch start. So we consider them as starting roughly at the same time.
320397 // Can't be a single-touch gesture.
321398 #if DIRECTIONALDRAGAREA_DEBUG
322399 qDebug("[DDA] A new touch point came in but we're still within time composition window. Ignoring it.");
323400 #endif
324− return;
401+ allGood = false;
325402 }
326403
327404 const QList<QTouchEvent::TouchPoint> &touchPoints = event->touchPoints();
@@ -332,7 +409,7 @@ void DirectionalDragArea::touchEvent_absent(QTouchEvent *event)
332409 if (touchPoint.state() == Qt::TouchPointPressed) {
333410 if (newTouchPoint) {
334411 // more than one touch starting in this QTouchEvent. Can't be a single-touch gesture
335− return;
412+ allGood = false;
336413 } else {
337414 // that's our candidate
338415 m_touchId = touchPoint.id();
@@ -341,86 +418,63 @@ void DirectionalDragArea::touchEvent_absent(QTouchEvent *event)
341418 }
342419 }
343420
344− Q_ASSERT(newTouchPoint);
421+ if (allGood) {
422+ Q_ASSERT(newTouchPoint);
345423
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);
424+ m_startPos = newTouchPoint->pos();
425+ m_startScenePos = newTouchPoint->scenePos();
426+ m_touchId = newTouchPoint->id();
427+ m_dampedScenePos.reset(m_startScenePos);
428+ m_velocityCalculator->setTrackedPosition(0.);
429+ m_velocityCalculator->reset();
430+ m_numSamplesOnLastSpeedCheck = 0;
431+ m_silenceTime = 0;
432+ setPreviousPos(m_startPos);
433+ setPreviousScenePos(m_startScenePos);
434+ updateSceneDirectionVector();
435+
436+ if (recognitionIsDisabled()) {
437+ // Behave like a dumb TouchArea
438+ TouchRegistry::instance()->requestTouchOwnership(m_touchId, this);
439+ setStatus(Recognized);
440+ event->accept();
441+ } else {
442+ // just monitor the touch points for now.
443+ TouchRegistry::instance()->addCandidateOwnerForTouch(m_touchId, this);
444+
445+ setStatus(Undecided);
446+ // Let the item below have it. We will monitor it and grab it later if a gesture
447+ // gets recognized.
448+ event->ignore();
449+ }
450+ } else {
451+ watchPressedTouchPoints(touchPoints);
452+ event->ignore();
453+ }
361454}
362455
363456void DirectionalDragArea::touchEvent_undecided(QTouchEvent *event)
364457{
365− const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event);
366−
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− }
458+ Q_ASSERT(event->type() == QEvent::TouchBegin);
459+ Q_ASSERT(fetchTargetTouchPoint(event) == nullptr);
374460
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− }
461+ // We're not interested in new touch points. We already have our candidate (m_touchId).
462+ // But we do want to know when those new touches end for keeping the composition time
463+ // window up-to-date
464+ event->ignore();
465+ watchPressedTouchPoints(event->touchPoints());
384466
385467 if (event->touchPointStates().testFlag(Qt::TouchPointPressed) && isWithinTouchCompositionWindow()) {
386468 // multi-finger drags are not accepted
387469 DDA_DEBUG("Multi-finger drags are not accepted");
388− setStatus(WaitingForTouch);
389− return;
390− }
391470
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− }
471+ TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
472+ // We still wanna know when it ends for keeping the composition time window up-to-date
473+ TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
402474
403− if (!movingInRightDirection()) {
404− DDA_DEBUG("Rejecting gesture becauuse touch point is moving in the wrong direction.");
405475 setStatus(WaitingForTouch);
406476 return;
407477 }
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.");
423− }
424478}
425479
426480void DirectionalDragArea::touchEvent_recognized(QTouchEvent *event)
@@ -437,11 +491,35 @@ void DirectionalDragArea::touchEvent_recognized(QTouchEvent *event)
437491 setPreviousScenePos(touchPoint->scenePos());
438492
439493 if (touchPoint->state() == Qt::TouchPointReleased) {
494+ emitSignalIfTapped();
440495 setStatus(WaitingForTouch);
441496 }
442497 }
443498}
444499
500+void DirectionalDragArea::watchPressedTouchPoints(const QList<QTouchEvent::TouchPoint> &touchPoints)
501+{
502+ for (int i = 0; i < touchPoints.count(); ++i) {
503+ const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
504+ if (touchPoint.state() == Qt::TouchPointPressed) {
505+ TouchRegistry::instance()->addTouchWatcher(touchPoint.id(), this);
506+ }
507+ }
508+}
509+
510+bool DirectionalDragArea::recognitionIsDisabled() const
511+{
512+ return distanceThreshold() <= 0 && compositionTime() <= 0;
513+}
514+
515+void DirectionalDragArea::emitSignalIfTapped()
516+{
517+ qint64 touchDuration = m_timeSource->msecsSinceReference() - m_activeTouches.touchStartTime(m_touchId);
518+ if (touchDuration <= maxTapDuration()) {
519+ Q_EMIT tapped();
520+ }
521+}
522+
445523const QTouchEvent::TouchPoint *DirectionalDragArea::fetchTargetTouchPoint(QTouchEvent *event)
446524{
447525 const QList<QTouchEvent::TouchPoint> &touchPoints = event->touchPoints();
@@ -509,12 +587,16 @@ bool DirectionalDragArea::movedFarEnough(const QPointF &point) const
509587
510588void DirectionalDragArea::checkSpeed()
511589{
590+ Q_ASSERT(m_status == Undecided);
591+
512592 if (m_velocityCalculator->numSamples() >= AxisVelocityCalculator::MIN_SAMPLES_NEEDED) {
513593 qreal speed = qFabs(m_velocityCalculator->calculate());
514594 qreal minSpeedMsecs = m_minSpeed / 1000.0;
515595
516596 if (speed < minSpeedMsecs) {
517597 DDA_DEBUG("Rejecting gesture because it's below minimum speed.");
598+ TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
599+ TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
518600 setStatus(WaitingForTouch);
519601 }
520602 }
@@ -523,7 +605,9 @@ void DirectionalDragArea::checkSpeed()
523605 m_silenceTime += m_recognitionTimer->interval();
524606
525607 if (m_silenceTime > m_maxSilenceTime) {
526− DDA_DEBUG("Rejecting gesture because it's silence time has been exceeded.");
608+ DDA_DEBUG("Rejecting gesture because its silence time has been exceeded.");
609+ TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
610+ TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
527611 setStatus(WaitingForTouch);
528612 }
529613 } else {
@@ -532,10 +616,19 @@ void DirectionalDragArea::checkSpeed()
532616 m_numSamplesOnLastSpeedCheck = m_velocityCalculator->numSamples();
533617}
534618
535−void DirectionalDragArea::onEnabledChanged()
619+void DirectionalDragArea::giveUpIfDisabledOrInvisible()
536620{
537− if (!isEnabled() && m_status != WaitingForTouch) {
538− setStatus(WaitingForTouch);
621+ if (!isEnabled() || !isVisible()) {
622+ if (m_status == Undecided) {
623+ TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
624+ // We still wanna know when it ends for keeping the composition time window up-to-date
625+ TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
626+ }
627+
628+ if (m_status != WaitingForTouch) {
629+ DDA_DEBUG("Resetting status because got disabled or made invisible");
630+ setStatus(WaitingForTouch);
631+ }
539632 }
540633}
541634
@@ -560,6 +653,8 @@ void DirectionalDragArea::setStatus(DirectionalDragArea::Status newStatus)
560653 switch (newStatus) {
561654 case WaitingForTouch:
562655 Q_EMIT draggingChanged(false);
656+ if (oldStatus == Undecided) {
657+ }
563658 break;
564659 case Undecided:
565660 m_recognitionTimer->start();
@@ -631,7 +726,9 @@ void DirectionalDragArea::updateVelocityCalculator(const QPointF &scenePos)
631726
632727bool DirectionalDragArea::isWithinTouchCompositionWindow()
633728{
634− return !m_activeTouches.isEmpty() &&
729+ return
730+ compositionTime() > 0 &&
731+ !m_activeTouches.isEmpty() &&
635732 m_timeSource->msecsSinceReference() <=
636733 m_activeTouches.mostRecentStartTime() + (qint64)compositionTime();
637734}
@@ -639,18 +736,17 @@ bool DirectionalDragArea::isWithinTouchCompositionWindow()
639736//************************** ActiveTouchesInfo **************************
640737
641738DirectionalDragArea::ActiveTouchesInfo::ActiveTouchesInfo(const SharedTimeSource &timeSource)
642− : m_timeSource(timeSource), m_lastUsedIndex(-1)
739+ : m_timeSource(timeSource)
643740{
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);
648741}
649742
650743void DirectionalDragArea::ActiveTouchesInfo::update(QTouchEvent *event)
651744{
652745 if (!(event->touchPointStates() & (Qt::TouchPointPressed | Qt::TouchPointReleased))) {
653746 // nothing to update
747+ #if ACTIVETOUCHESINFO_DEBUG
748+ qDebug("[DDA::ActiveTouchesInfo] Nothing to Update");
749+ #endif
654750 return;
655751 }
656752
@@ -658,74 +754,88 @@ void DirectionalDragArea::ActiveTouchesInfo::update(QTouchEvent *event)
658754 for (int i = 0; i < touchPoints.count(); ++i) {
659755 const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
660756 if (touchPoint.state() == Qt::TouchPointPressed) {
661− addTouchPoint(touchPoint);
757+ addTouchPoint(touchPoint.id());
662758 } else if (touchPoint.state() == Qt::TouchPointReleased) {
663− removeTouchPoint(touchPoint);
759+ removeTouchPoint(touchPoint.id());
664760 }
665761 }
666762}
667763
668−void DirectionalDragArea::ActiveTouchesInfo::addTouchPoint(const QTouchEvent::TouchPoint &touchPoint)
764+#if ACTIVETOUCHESINFO_DEBUG
765+QString DirectionalDragArea::ActiveTouchesInfo::toString()
669766{
670− ActiveTouchInfo &activeTouchInfo = getEmptySlot();
671− activeTouchInfo.id = touchPoint.id();
672− activeTouchInfo.startTime = m_timeSource->msecsSinceReference();
767+ QString string = "(";
768+
769+ {
770+ QTextStream stream(&string);
771+ m_touchInfoPool.forEach([&](Pool<ActiveTouchInfo>::Iterator &touchInfo) {
772+ stream << "(id=" << touchInfo->id << ",startTime=" << touchInfo->startTime << ")";
773+ return true;
774+ });
775+ }
776+
777+ string.append(")");
778+
779+ return string;
673780}
781+#endif // ACTIVETOUCHESINFO_DEBUG
674782
675−void DirectionalDragArea::ActiveTouchesInfo::removeTouchPoint(const QTouchEvent::TouchPoint &touchPoint)
783+void DirectionalDragArea::ActiveTouchesInfo::addTouchPoint(int touchId)
676784{
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
785+ ActiveTouchInfo &activeTouchInfo = m_touchInfoPool.getEmptySlot();
786+ activeTouchInfo.id = touchId;
787+ activeTouchInfo.startTime = m_timeSource->msecsSinceReference();
788+
789+ #if ACTIVETOUCHESINFO_DEBUG
790+ qDebug() << "[DDA::ActiveTouchesInfo]" << qPrintable(toString());
791+ #endif
684792}
685793
686−DirectionalDragArea::ActiveTouchInfo &DirectionalDragArea::ActiveTouchesInfo::getEmptySlot()
794+qint64 DirectionalDragArea::ActiveTouchesInfo::touchStartTime(int touchId)
687795{
688− Q_ASSERT(m_lastUsedIndex < m_vector.size());
796+ qint64 result = -1;
689797
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;
798+ m_touchInfoPool.forEach([&](Pool<ActiveTouchInfo>::Iterator &touchInfo) {
799+ if (touchId == touchInfo->id) {
800+ result = touchInfo->startTime;
801+ return false;
802+ } else {
803+ return true;
695804 }
696− }
697−
698− ++m_lastUsedIndex;
699− if (m_lastUsedIndex >= m_vector.size()) {
700− m_vector.resize(m_lastUsedIndex + 1);
701− }
805+ });
702806
703− return m_vector[m_lastUsedIndex];
807+ Q_ASSERT(result != -1);
808+ return result;
704809}
705810
706−void DirectionalDragArea::ActiveTouchesInfo::freeSlot(int index)
811+void DirectionalDragArea::ActiveTouchesInfo::removeTouchPoint(int touchId)
707812{
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− }
813+ m_touchInfoPool.forEach([&](Pool<ActiveTouchInfo>::Iterator &touchInfo) {
814+ if (touchId == touchInfo->id) {
815+ m_touchInfoPool.freeSlot(touchInfo);
816+ return false;
817+ } else {
818+ return true;
819+ }
820+ });
821+
822+ #if ACTIVETOUCHESINFO_DEBUG
823+ qDebug() << "[DDA::ActiveTouchesInfo]" << qPrintable(toString());
824+ #endif
714825}
715826
716827qint64 DirectionalDragArea::ActiveTouchesInfo::mostRecentStartTime()
717828{
718− Q_ASSERT(m_lastUsedIndex >= 0);
829+ Q_ASSERT(!m_touchInfoPool.isEmpty());
830+
831+ qint64 highestStartTime = -1;
719832
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;
833+ m_touchInfoPool.forEach([&](Pool<ActiveTouchInfo>::Iterator &activeTouchInfo) {
834+ if (activeTouchInfo->startTime > highestStartTime) {
835+ highestStartTime = activeTouchInfo->startTime;
726836 }
727− ++i;
728− } while (i < m_lastUsedIndex);
837+ return true;
838+ });
729839
730840 return highestStartTime;
731841}
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+322
@@ -0,0 +1,322 @@
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+ QTouchEvent *clonedEvent = new QTouchEvent(event->type(),
129+ event->device(),
130+ event->modifiers(),
131+ event->touchPointStates(),
132+ event->touchPoints());
133+
134+ m_storedEvents.append(clonedEvent);
135+}
136+
137+void TouchGate::removeTouchFromStoredEvents(int touchId)
138+{
139+ int i = 0;
140+ while (i < m_storedEvents.count()) {
141+ QTouchEvent *event = m_storedEvents[i];
142+ bool removed = removeTouchFromEvent(touchId, event);
143+
144+ if (removed && event->touchPoints().isEmpty()) {
145+ m_storedEvents.removeAt(i);
146+ } else {
147+ ++i;
148+ }
149+ }
150+}
151+
152+bool TouchGate::removeTouchFromEvent(int touchId, QTouchEvent *event)
153+{
154+ const QList<QTouchEvent::TouchPoint> &eventTouchPoints = event->touchPoints();
155+
156+ int indexToRemove = -1;
157+ for (int i = 0; i < eventTouchPoints.count() && indexToRemove < 0; ++i) {
158+ if (eventTouchPoints[i].id() == touchId) {
159+ indexToRemove = i;
160+ }
161+ }
162+
163+ if (indexToRemove >= 0) {
164+ QList<QTouchEvent::TouchPoint> newTouchPointsList = eventTouchPoints;
165+ newTouchPointsList.removeAt(indexToRemove);
166+ event->setTouchPoints(newTouchPointsList);
167+ return true;
168+ } else {
169+ return false;
170+ }
171+}
172+
173+void TouchGate::dispatchFullyOwnedEvents()
174+{
175+ while (!m_storedEvents.isEmpty() && eventIsFullyOwned(m_storedEvents.first())) {
176+ QTouchEvent *event = m_storedEvents.first();
177+ m_storedEvents.removeFirst();
178+ dispatchTouchEventToTarget(event);
179+ delete event;
180+ }
181+}
182+
183+#if TOUCHGATE_DEBUG
184+QString TouchGate::oldestPendingTouchIdsString()
185+{
186+ Q_ASSERT(!m_storedEvents.isEmpty());
187+
188+ QString str;
189+
190+ auto touchPoints = m_storedEvents.first()->touchPoints();
191+ for (int i = 0; i < touchPoints.count(); ++i) {
192+ if (!isTouchPointOwned(touchPoints[i].id())) {
193+ if (!str.isEmpty()) {
194+ str.append(", ");
195+ }
196+ str.append(QString::number(touchPoints[i].id()));
197+ }
198+ }
199+
200+ return str;
201+}
202+#endif
203+
204+bool TouchGate::eventIsFullyOwned(const QTouchEvent *event) const
205+{
206+ auto touchPoints = event->touchPoints();
207+ for (int i = 0; i < touchPoints.count(); ++i) {
208+ if (!isTouchPointOwned(touchPoints[i].id())) {
209+ return false;
210+ }
211+ }
212+
213+ return true;
214+}
215+
216+void TouchGate::setTargetItem(QQuickItem *item)
217+{
218+ // TODO: changing the target item while dispatch of touch events is taking place will
219+ // create a mess
220+
221+ if (item == m_targetItem.data())
222+ return;
223+
224+ m_targetItem = item;
225+ Q_EMIT targetItemChanged(item);
226+}
227+
228+void TouchGate::dispatchTouchEventToTarget(QTouchEvent* event)
229+{
230+ removeTouchInfoForEndedTouches(event);
231+
232+ if (m_targetItem.isNull()) {
233+ qWarning("[TouchGate] Cannot dispatch touch event because target item is null");
234+ return;
235+ }
236+
237+ QQuickItem *targetItem = m_targetItem.data();
238+
239+ if (!targetItem->isEnabled() || !targetItem->isVisible()) {
240+ #if TOUCHGATE_DEBUG
241+ qDebug() << "[TouchGate] Cannot dispatch touch event to" << targetItem
242+ << "because it's disabled or invisible.";
243+ #endif
244+ return;
245+ }
246+
247+ // Map touch points to targetItem coordinates
248+ QList<QTouchEvent::TouchPoint> touchPoints = event->touchPoints();
249+ transformTouchPoints(touchPoints, QQuickItemPrivate::get(targetItem)->windowToItemTransform());
250+ QTouchEvent *eventForTargetItem = touchEventWithPoints(*event, touchPoints);
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+// NB: From QQuickWindow
276+QTouchEvent *TouchGate::touchEventWithPoints(const QTouchEvent &event,
277+ const QList<QTouchEvent::TouchPoint> &newPoints)
278+{
279+ Qt::TouchPointStates eventStates = 0;
280+ for (int i=0; i<newPoints.count(); i++)
281+ eventStates |= newPoints[i].state();
282+ // if all points have the same state, set the event type accordingly
283+ QEvent::Type eventType = event.type();
284+ switch (eventStates) {
285+ case Qt::TouchPointPressed:
286+ eventType = QEvent::TouchBegin;
287+ break;
288+ case Qt::TouchPointReleased:
289+ eventType = QEvent::TouchEnd;
290+ break;
291+ default:
292+ eventType = QEvent::TouchUpdate;
293+ break;
294+ }
295+
296+ QTouchEvent *touchEvent = new QTouchEvent(eventType);
297+ touchEvent->setWindow(event.window());
298+ touchEvent->setTarget(event.target());
299+ touchEvent->setDevice(event.device());
300+ touchEvent->setModifiers(event.modifiers());
301+ touchEvent->setTouchPoints(newPoints);
302+ touchEvent->setTouchPointStates(eventStates);
303+ touchEvent->setTimestamp(event.timestamp());
304+ touchEvent->accept();
305+ return touchEvent;
306+}
307+
308+void TouchGate::removeTouchInfoForEndedTouches(QTouchEvent *event)
309+{
310+ const QList<QTouchEvent::TouchPoint> &touchPoints = event->touchPoints();
311+
312+ for (int i = 0; i < touchPoints.size(); ++i) {\
313+ const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
314+
315+ if (touchPoint.state() == Qt::TouchPointReleased) {
316+ Q_ASSERT(m_touchInfoMap.contains(touchPoint.id()));
317+ Q_ASSERT(m_touchInfoMap[touchPoint.id()].ended);
318+ Q_ASSERT(m_touchInfoMap[touchPoint.id()].ownership == OwnershipGranted);
319+ m_touchInfoMap.remove(touchPoint.id());
320+ }
321+ }
322+}
plugins/Ubuntu/Gestures/TouchGate.hnew file+97
@@ -0,0 +1,97 @@
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+ void touchOwnershipEvent(TouchOwnershipEvent *event);
60+ bool isTouchPointOwned(int touchId) const;
61+ void storeTouchEvent(const QTouchEvent *event);
62+ void removeTouchFromStoredEvents(int touchId);
63+ void dispatchFullyOwnedEvents();
64+ bool removeTouchFromEvent(int touchId, QTouchEvent *event);
65+ bool eventIsFullyOwned(const QTouchEvent *event) const;
66+ void dispatchTouchEventToTarget(QTouchEvent* event);
67+ void transformTouchPoints(QList<QTouchEvent::TouchPoint> &touchPoints, const QTransform &transform);
68+ QTouchEvent *touchEventWithPoints(const QTouchEvent &event,
69+ const QList<QTouchEvent::TouchPoint> &newPoints);
70+ void removeTouchInfoForEndedTouches(QTouchEvent *event);
71+
72+ #if TOUCHGATE_DEBUG
73+ QString oldestPendingTouchIdsString();
74+ #endif
75+
76+ // TODO: Optimize storage of buffered events
77+ QList<QTouchEvent*> m_storedEvents;
78+
79+ enum {
80+ OwnershipUndefined,
81+ OwnershipRequested,
82+ OwnershipGranted,
83+ };
84+ class TouchInfo {
85+ public:
86+ TouchInfo() {ownership = OwnershipUndefined; ended = false;}
87+ int ownership;
88+ bool ended;
89+ };
90+ QMap<int, TouchInfo> m_touchInfoMap;
91+
92+ QPointer<QQuickItem> m_targetItem;
93+
94+ friend class tst_TouchGate;
95+};
96+
97+#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/Stages/ApplicationWindow.qml−17
@@ -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"
qml/Stages/PhoneStage.qml+1
@@ -410,6 +410,7 @@ Rectangle {
410410
411411 EdgeDragArea {
412412 id: spreadDragArea
413+ objectName: "spreadDragArea"
413414 direction: Direction.Leftwards
414415 enabled: spreadView.phase != 2 && root.spreadEnabled
415416
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+33−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,6 +24,7 @@ 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) {
@@ -31,13 +33,38 @@ Item {
3133 hadSurface = true;
3234 }
3335 }
34− Binding {
35− target: surface
36− property: "anchors.fill"; value: root
36+ Binding { target: surface; property: "anchors.fill"; value: root }
37+ Binding { target: surface; property: "orientation"; value: root.orientation }
38+ Binding { target: surface; property: "z"; value: 1 }
39+ Binding { target: surface; property: "enabled"; value: root.interactive; when: surface }
40+ Binding { target: surface; property: "focus"; value: root.interactive; when: surface }
41+
42+ TouchGate {
43+ targetItem: surface
44+ anchors.fill: root
45+ enabled: surface.enabled
46+ z: 2
47+ }
48+
49+ Connections {
50+ target: root.surface
51+ // FIXME: I would rather not need to do this, but currently it doesn't get
52+ // active focus without it and I don't know why.
53+ onFocusChanged: d.forceSurfaceActiveFocusIfReady();
54+ onParentChanged: d.forceSurfaceActiveFocusIfReady();
55+ onEnabledChanged: d.forceSurfaceActiveFocusIfReady();
3756 }
38− Binding {
39− target: surface
40− property: "orientation"; value: root.orientation
57+
58+ QtObject {
59+ id: d
60+ function forceSurfaceActiveFocusIfReady() {
61+ if (root.surface !== null &&
62+ root.surface.focus &&
63+ root.surface.parent === root &&
64+ root.surface.enabled) {
65+ root.surface.forceActiveFocus();
66+ }
67+ }
4168 }
4269
4370 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+3
@@ -9,6 +9,9 @@ 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+
1215# install binaries
1316install(TARGETS ${DASH_APP}
1417 RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
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/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/plugins/Ubuntu/Gestures/CMakeLists.txt+4−2
@@ -2,6 +2,7 @@ include(QmlTest)
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 )
@@ -17,7 +18,7 @@ add_definitions(-DUBUNTU_GESTURES_PLUGIN_DIR="${CMAKE_BINARY_DIR}/plugins")
1718macro(add_gesture_ui_test CLASSNAME)
1819 add_executable(${CLASSNAME}TestExec tst_${CLASSNAME}.cpp GestureTest.cpp)
1920 qt5_use_modules(${CLASSNAME}TestExec Test Core Qml Gui Quick)
20− target_link_libraries(${CLASSNAME}TestExec UbuntuGestureQml)
21+ target_link_libraries(${CLASSNAME}TestExec UbuntuGesturesQml UbuntuGestures)
2122
2223 add_binary_qml_test(${CLASSNAME} ${CMAKE_BINARY_DIR}/plugins/Ubuntu/Gestures UbuntuGesturesTestQmlFiles)
2324 add_manual_qml_test(. ${CLASSNAME} IMPORT_PATHS ${CMAKE_BINARY_DIR}/plugins)
@@ -34,10 +35,11 @@ macro(add_gesture_test CLASSNAME)
3435
3536 add_executable(${CLASSNAME}TestExec tst_${CLASSNAME}.cpp)
3637 qt5_use_modules(${CLASSNAME}TestExec Test Core)
37− target_link_libraries(${CLASSNAME}TestExec UbuntuGestureQml)
38+ target_link_libraries(${CLASSNAME}TestExec UbuntuGesturesQml)
3839endmacro(add_gesture_test)
3940
4041add_gesture_ui_test(DirectionalDragArea)
4142add_gesture_ui_test(PressedOutsideNotifier)
43+add_gesture_ui_test(TouchGate)
4244add_gesture_test(Damper)
4345add_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+415−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
@@ -20,36 +20,61 @@
2020#include <QtQml/QQmlEngine>
2121#include <QPointer>
2222
23+// C++ std lib
24+#include <functional>
25+
2326#include <DirectionalDragArea.h>
27+#include <TouchRegistry.h>
2428
2529#include "GestureTest.h"
2630
2731using namespace UbuntuGestures;
2832
29−class FakeTimer : public AbstractTimer
33+class TouchMemento {
34+public:
35+ TouchMemento(const QTouchEvent *touchEvent);
36+ Qt::TouchPointStates touchPointStates;
37+ QList<QTouchEvent::TouchPoint> touchPoints;
38+
39+ bool containsTouchWithId(int touchId) const;
40+};
41+
42+class DummyItem : public QQuickItem
43+{
44+ Q_OBJECT
45+public:
46+ DummyItem(QQuickItem *parent = 0);
47+
48+ QList<TouchMemento> touchEvents;
49+ std::function<void(QTouchEvent*)> touchEventHandler;
50+protected:
51+ void touchEvent(QTouchEvent *event) override;
52+private:
53+ static void defaultTouchEventHandler(QTouchEvent *event);
54+};
55+
56+class ComplexFakeTimer : public FakeTimer
3057{
3158 Q_OBJECT
3259public:
33− FakeTimer(const SharedTimeSource &timeSource, QObject *parent = 0)
34− : UbuntuGestures::AbstractTimer(parent),
60+ ComplexFakeTimer(const SharedTimeSource &timeSource, QObject *parent = 0)
61+ : FakeTimer(parent),
3562 m_timeSource(timeSource)
3663 {}
3764
38− int interval() const override { return m_interval; }
39− void setInterval(int msecs) override { m_interval = msecs; }
4065 void start() override {
4166 AbstractTimer::start();
42− m_nextTimeoutTime = m_timeSource->msecsSinceReference() + (qint64)m_interval;
67+ m_nextTimeoutTime = m_timeSource->msecsSinceReference() + (qint64)interval();
4368 }
4469
4570 void emitTimeout() {
46− m_nextTimeoutTime += m_interval;
71+ m_nextTimeoutTime += interval();
4772 Q_EMIT timeout();
4873 }
4974
5075 qint64 nextTimeoutTime() const { return m_nextTimeoutTime; }
76+
5177private:
52− int m_interval;
5378 SharedTimeSource m_timeSource;
5479 qint64 m_nextTimeoutTime;
5580};
@@ -87,10 +112,19 @@ private Q_SLOTS:
87112 void sceneDistance();
88113 void sceneDistance_data();
89114 void disabledWhileDragging();
115+ void oneFingerDownFollowedByLateSecondFingerDown();
116+ void givesUpWhenLosesTouch();
117+ void threeFingerDrag();
118+ void immediateRecognitionWhenConstraintsDisabled();
119+ void withdrawTouchOwnershipCandidacyIfDisabledDuringRecognition();
120+ void withdrawTouchOwnershipCandidacyIfDisabledDuringRecognition_data();
121+ void tappedSignal();
122+ void tappedSignal_data();
90123
91124private:
92− void passTime(qint64 timeSpan);
93− FakeTimer *fakeTimer;
125+ void passTime(qint64 timeSpanMs);
126+ TouchRegistry *touchRegistry;
127+ ComplexFakeTimer *fakeTimer;
94128 QSharedPointer<FakeTimeSource> fakeTimeSource;
95129};
96130
@@ -111,12 +145,19 @@ void tst_DirectionalDragArea::init()
111145 QTRY_COMPARE(m_view->width(), (int)m_view->rootObject()->width());
112146 QTRY_COMPARE(m_view->height(), (int)m_view->rootObject()->height());
113147
148+ touchRegistry = new TouchRegistry;
149+ m_view->installEventFilter(touchRegistry);
150+
114151 fakeTimeSource.reset(new FakeTimeSource);
115− fakeTimer = new FakeTimer(fakeTimeSource);
152+ fakeTimer = new ComplexFakeTimer(fakeTimeSource);
116153}
117154
118155void tst_DirectionalDragArea::cleanup()
119156{
157+ m_view->removeEventFilter(touchRegistry);
158+ delete touchRegistry;
159+ touchRegistry = nullptr;
160+
120161 delete fakeTimer;
121162 fakeTimer = 0;
122163
@@ -125,9 +166,9 @@ void tst_DirectionalDragArea::cleanup()
125166 GestureTest::cleanup();
126167}
127168
128−void tst_DirectionalDragArea::passTime(qint64 timeSpan)
169+void tst_DirectionalDragArea::passTime(qint64 timeSpanMs)
129170{
130− qint64 finalTime = fakeTimeSource->m_msecsSinceReference + timeSpan;
171+ qint64 finalTime = fakeTimeSource->m_msecsSinceReference + timeSpanMs;
131172
132173 if (fakeTimer->isRunning() && finalTime >= fakeTimer->nextTimeoutTime()) {
133174 fakeTimeSource->m_msecsSinceReference = fakeTimer->nextTimeoutTime();
@@ -849,6 +890,366 @@ void tst_DirectionalDragArea::disabledWhileDragging()
849890 QTest::touchEvent(m_view, m_device).release(0, touchPoint.toPoint());
850891}
851892
893+void tst_DirectionalDragArea::oneFingerDownFollowedByLateSecondFingerDown()
894+{
895+ DirectionalDragArea *edgeDragArea =
896+ m_view->rootObject()->findChild<DirectionalDragArea*>("hpDragArea");
897+ Q_ASSERT(edgeDragArea != 0);
898+ edgeDragArea->setRecognitionTimer(fakeTimer);
899+ edgeDragArea->setTimeSource(fakeTimeSource);
900+
901+ // Disable some constraints we're not interested in
902+ edgeDragArea->setMaxSilenceTime(60 * 1000);
903+ edgeDragArea->setMinSpeed(0);
904+
905+ // And ensure others have the values we want
906+ edgeDragArea->setCompositionTime(60);
907+
908+ // Put an item right behind edgeDragArea to receive the touches ignored by it
909+ DummyItem *dummyItem = new DummyItem;
910+ dummyItem->setParentItem(edgeDragArea->parentItem());
911+ dummyItem->setX(edgeDragArea->x());
912+ dummyItem->setY(edgeDragArea->y());
913+ dummyItem->setZ(edgeDragArea->z() - 1.0);
914+ dummyItem->setWidth(edgeDragArea->width());
915+ dummyItem->setHeight(edgeDragArea->height());
916+
917+ // Make touches evenly spaced along the edgeDragArea
918+ QPoint touch0Pos(edgeDragArea->width()/2.0f, m_view->height()*0.33f);
919+ QPoint touch1Pos(edgeDragArea->width()/2.0f, m_view->height()*0.66f);
920+
921+ QTest::touchEvent(m_view, m_device).press(0, touch0Pos);
922+
923+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Undecided);
924+
925+ // We are now going to be way beyond compositionTime
926+ passTime(edgeDragArea->compositionTime()*3);
927+
928+ QTest::touchEvent(m_view, m_device)
929+ .move(0, touch0Pos)
930+ .press(1, touch1Pos);
931+
932+ // A new touch has come, but as it can't be composed with touch 0, it should be
933+ // ignored/rejected by the DirectionalDragArea
934+ // Therefore the last event received by dummyItem must have both touch points (0 and 1)
935+ {
936+ TouchMemento &touchMemento = dummyItem->touchEvents.last();
937+ QCOMPARE(touchMemento.touchPoints.count(), 2);
938+ QVERIFY(touchMemento.containsTouchWithId(0));
939+ QVERIFY(touchMemento.containsTouchWithId(1));
940+ }
941+
942+ passTime(30);
943+
944+ QTest::touchEvent(m_view, m_device)
945+ .move(0, touch0Pos)
946+ .move(1, touch1Pos);
947+
948+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Undecided);
949+
950+ passTime(5);
951+
952+ QTest::touchEvent(m_view, m_device)
953+ .release(0, touch0Pos)
954+ .move(1, touch1Pos);
955+
956+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch);
957+
958+ passTime(5);
959+
960+ QTest::touchEvent(m_view, m_device)
961+ .release(1, touch1Pos);
962+
963+ // Shouldn't be keepping info about touches that no longer exist or interest us
964+ QVERIFY(edgeDragArea->m_activeTouches.isEmpty());
965+
966+ delete dummyItem;
967+}
968+
969+void tst_DirectionalDragArea::givesUpWhenLosesTouch()
970+{
971+ DirectionalDragArea *edgeDragArea =
972+ m_view->rootObject()->findChild<DirectionalDragArea*>("hpDragArea");
973+ Q_ASSERT(edgeDragArea != 0);
974+ edgeDragArea->setRecognitionTimer(fakeTimer);
975+ edgeDragArea->setTimeSource(fakeTimeSource);
976+
977+ // Disable some constraints we're not interested in
978+ edgeDragArea->setMaxSilenceTime(60 * 1000);
979+ edgeDragArea->setMinSpeed(0);
980+
981+ // Put an item right in front of edgeDragArea
982+ DummyItem *dummyItem = new DummyItem(edgeDragArea->parentItem());
983+ dummyItem->setX(edgeDragArea->x());
984+ dummyItem->setY(edgeDragArea->y());
985+ dummyItem->setZ(edgeDragArea->z() + 1.0);
986+ dummyItem->setWidth(edgeDragArea->width());
987+ dummyItem->setHeight(edgeDragArea->height());
988+
989+ QPoint touchPos(edgeDragArea->width()/2.0f, m_view->height()/2.0f);
990+
991+ dummyItem->touchEventHandler = [&](QTouchEvent *event) {
992+ touchRegistry->addCandidateOwnerForTouch(0, dummyItem);
993+ event->ignore();
994+ };
995+
996+ QTest::touchEvent(m_view, m_device).press(0, touchPos);
997+
998+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Undecided);
999+
1000+ touchRegistry->requestTouchOwnership(0, dummyItem);
1001+
1002+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch);
1003+
1004+ dummyItem->grabTouchPoints({0});
1005+ dummyItem->touchEventHandler = [&](QTouchEvent *event) { event->accept(); };
1006+
1007+ passTime(5);
1008+ QTest::touchEvent(m_view, m_device).release(0, touchPos);
1009+
1010+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch);
1011+
1012+ QVERIFY(edgeDragArea->m_activeTouches.isEmpty());
1013+}
1014+
1015+void tst_DirectionalDragArea::threeFingerDrag()
1016+{
1017+ DirectionalDragArea *edgeDragArea =
1018+ m_view->rootObject()->findChild<DirectionalDragArea*>("hpDragArea");
1019+ Q_ASSERT(edgeDragArea != 0);
1020+ edgeDragArea->setRecognitionTimer(fakeTimer);
1021+ edgeDragArea->setTimeSource(fakeTimeSource);
1022+
1023+ // Disable some constraints we're not interested in
1024+ edgeDragArea->setMaxSilenceTime(60 * 1000);
1025+ edgeDragArea->setMinSpeed(0);
1026+
1027+ // And ensure others have the values we want
1028+ edgeDragArea->setCompositionTime(60);
1029+
1030+ // Make touches evenly spaced along the edgeDragArea
1031+ QPoint touch0Pos(edgeDragArea->width()/2.0f, m_view->height()*0.25f);
1032+ QPoint touch1Pos(edgeDragArea->width()/2.0f, m_view->height()*0.50f);
1033+ QPoint touch2Pos(edgeDragArea->width()/2.0f, m_view->height()*0.75f);
1034+
1035+ QTest::touchEvent(m_view, m_device)
1036+ .press(0, touch0Pos);
1037+
1038+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Undecided);
1039+
1040+ passTime(5);
1041+ QTest::touchEvent(m_view, m_device)
1042+ .move(0, touch0Pos)
1043+ .press(1, touch1Pos);
1044+
1045+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch);
1046+
1047+ passTime(5);
1048+ QTest::touchEvent(m_view, m_device)
1049+ .move(0, touch0Pos)
1050+ .move(1, touch1Pos)
1051+ .press(2, touch2Pos);
1052+
1053+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch);
1054+
1055+ passTime(10);
1056+ QTest::touchEvent(m_view, m_device)
1057+ .move(0, touch0Pos)
1058+ .move(1, touch1Pos)
1059+ .move(2, touch2Pos);
1060+
1061+ passTime(200);
1062+ QTest::touchEvent(m_view, m_device)
1063+ .move(0, touch0Pos)
1064+ .release(1, touch1Pos)
1065+ .move(2, touch2Pos);
1066+
1067+ passTime(10);
1068+ QTest::touchEvent(m_view, m_device)
1069+ .move(0, touch0Pos)
1070+ .release(2, touch2Pos);
1071+
1072+ passTime(5);
1073+ QTest::touchEvent(m_view, m_device)
1074+ .release(0, touch0Pos);
1075+
1076+ // Shouldn't be keepping info about touches that no longer exist or interest us
1077+ QVERIFY(edgeDragArea->m_activeTouches.isEmpty());
1078+}
1079+
1080+/*
1081+ If all the relevant gesture recognition constraints/parameters have been disabled,
1082+ it means that the gesture recognition itself has been disabled and DirectionalDragArea
1083+ will therefore work like a simple touch area, merely reporting touch movement.
1084+ */
1085+void tst_DirectionalDragArea::immediateRecognitionWhenConstraintsDisabled()
1086+{
1087+ DirectionalDragArea *edgeDragArea =
1088+ m_view->rootObject()->findChild<DirectionalDragArea*>("hpDragArea");
1089+ Q_ASSERT(edgeDragArea != 0);
1090+ edgeDragArea->setRecognitionTimer(fakeTimer);
1091+ edgeDragArea->setTimeSource(fakeTimeSource);
1092+
1093+ // Disable the minimum amount of constraints to ensure immediate recognition
1094+ edgeDragArea->setDistanceThreshold(0);
1095+ edgeDragArea->setCompositionTime(0);
1096+
1097+ // Put an item right behind edgeDragArea to receive the touches ignored by it
1098+ DummyItem *dummyItem = new DummyItem;
1099+ dummyItem->setParentItem(edgeDragArea->parentItem());
1100+ dummyItem->setX(edgeDragArea->x());
1101+ dummyItem->setY(edgeDragArea->y());
1102+ dummyItem->setZ(edgeDragArea->z() - 1.0);
1103+ dummyItem->setWidth(edgeDragArea->width());
1104+ dummyItem->setHeight(edgeDragArea->height());
1105+
1106+ QPoint touch0Pos(edgeDragArea->width()/2.0f, m_view->height()/2.0f);
1107+
1108+ QTest::touchEvent(m_view, m_device).press(0, touch0Pos);
1109+
1110+ // check for immediate recognition
1111+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Recognized);
1112+
1113+ // and therefore it should have immediately grabbed the touch point,
1114+ // not letting it leak to items behind him.
1115+ QCOMPARE(dummyItem->touchEvents.count(), 0);
1116+}
1117+
1118+void tst_DirectionalDragArea::withdrawTouchOwnershipCandidacyIfDisabledDuringRecognition()
1119+{
1120+ DirectionalDragArea *edgeDragArea =
1121+ m_view->rootObject()->findChild<DirectionalDragArea*>("hpDragArea");
1122+ Q_ASSERT(edgeDragArea != 0);
1123+ edgeDragArea->setRecognitionTimer(fakeTimer);
1124+ edgeDragArea->setTimeSource(fakeTimeSource);
1125+
1126+ QPointF touchPoint = calculateInitialTouchPos(edgeDragArea);
1127+
1128+ // Move less than the minimum needed for the drag gesture recognition
1129+ qreal desiredDragDistance = edgeDragArea->distanceThreshold()*0.5f;
1130+ QPointF dragDirectionVector(1., 0.); // horizontal positive
1131+
1132+ qreal movementStepDistance = edgeDragArea->distanceThreshold() * 0.1f;
1133+ QPointF touchMovement = dragDirectionVector * movementStepDistance;
1134+ int totalMovementSteps = qCeil(desiredDragDistance / movementStepDistance);
1135+ int movementTimeStepMs = (edgeDragArea->compositionTime() * 0.8f) / totalMovementSteps;
1136+
1137+ QTest::touchEvent(m_view, m_device).press(0, touchPoint.toPoint());
1138+
1139+ for (int i = 0; i < totalMovementSteps; ++i) {
1140+ touchPoint += touchMovement;
1141+ passTime(movementTimeStepMs);
1142+ QTest::touchEvent(m_view, m_device).move(0, touchPoint.toPoint());
1143+ }
1144+
1145+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Undecided);
1146+
1147+ // edgeDragArea should be an undecided candidate
1148+ {
1149+ auto touchInfo = touchRegistry->findTouchInfo(0);
1150+ QCOMPARE(touchInfo->candidates.size(), 1);
1151+ QCOMPARE(touchInfo->candidates.at(0).item.data(), edgeDragArea);
1152+ QCOMPARE(touchInfo->candidates.at(0).undecided, true);
1153+ }
1154+
1155+ // disable the dragArea while it's still recognizing a possible drag gesture.
1156+ QFETCH(bool, disable);
1157+ if (disable) {
1158+ edgeDragArea->setEnabled(false);
1159+ } else {
1160+ edgeDragArea->setVisible(false);
1161+ }
1162+
1163+ // edgeDragArea should no longer be a candidate
1164+ {
1165+ auto touchInfo = touchRegistry->findTouchInfo(0);
1166+ QCOMPARE(touchInfo->candidates.size(), 0);
1167+ }
1168+
1169+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch);
1170+
1171+ QTest::touchEvent(m_view, m_device).release(0, touchPoint.toPoint());
1172+}
1173+
1174+void tst_DirectionalDragArea::withdrawTouchOwnershipCandidacyIfDisabledDuringRecognition_data()
1175+{
1176+ QTest::addColumn<bool>("disable");
1177+
1178+ QTest::newRow("disabled") << true;
1179+ QTest::newRow("invisible") << false;
1180+}
1181+
1182+void tst_DirectionalDragArea::tappedSignal()
1183+{
1184+ DirectionalDragArea *edgeDragArea =
1185+ m_view->rootObject()->findChild<DirectionalDragArea*>("hpDragArea");
1186+ QVERIFY(edgeDragArea != 0);
1187+ edgeDragArea->setRecognitionTimer(fakeTimer);
1188+ edgeDragArea->setTimeSource(fakeTimeSource);
1189+
1190+ QFETCH(bool, immediateGestureRecognition);
1191+ if (immediateGestureRecognition) {
1192+ // Disable the minimum amount of constraints to ensure immediate recognition
1193+ edgeDragArea->setDistanceThreshold(0);
1194+ edgeDragArea->setCompositionTime(0);
1195+ }
1196+
1197+ QPoint touch0Pos(edgeDragArea->width()/2.0f, m_view->height()/2.0f);
1198+
1199+ QSignalSpy tappedSpy(edgeDragArea, SIGNAL(tapped()));
1200+
1201+ QTest::touchEvent(m_view, m_device).press(0, touch0Pos);
1202+ passTime(edgeDragArea->maxTapDuration() / 2);
1203+ QTest::touchEvent(m_view, m_device).release(0, touch0Pos);
1204+
1205+ QCOMPARE(tappedSpy.count(), 1);
1206+}
1207+
1208+void tst_DirectionalDragArea::tappedSignal_data()
1209+{
1210+ QTest::addColumn<bool>("immediateGestureRecognition");
1211+
1212+ QTest::newRow("immediate gesture recognition") << true;
1213+ QTest::newRow("default gesture recognition") << false;
1214+}
1215+
1216+////////////////////////// TouchMemento /////////////////////////////
1217+
1218+TouchMemento::TouchMemento(const QTouchEvent *touchEvent)
1219+ : touchPointStates(touchEvent->touchPointStates()), touchPoints(touchEvent->touchPoints())
1220+{
1221+
1222+}
1223+
1224+bool TouchMemento::containsTouchWithId(int touchId) const
1225+{
1226+ for (int i = 0; i < touchPoints.count(); ++i) {
1227+ if (touchPoints.at(i).id() == touchId) {
1228+ return true;
1229+ }
1230+ }
1231+ return false;
1232+}
1233+
1234+////////////////////////// DummyItem /////////////////////////////
1235+
1236+DummyItem::DummyItem(QQuickItem *parent)
1237+ : QQuickItem(parent)
1238+{
1239+ touchEventHandler = defaultTouchEventHandler;
1240+}
1241+
1242+void DummyItem::touchEvent(QTouchEvent *event)
1243+{
1244+ touchEvents.append(TouchMemento(event));
1245+ touchEventHandler(event);
1246+}
1247+
1248+void DummyItem::defaultTouchEventHandler(QTouchEvent *event)
1249+{
1250+ event->accept();
1251+}
1252+
8521253QTEST_MAIN(tst_DirectionalDragArea)
8531254
8541255#include "tst_DirectionalDragArea.moc"
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/Stages/tst_ApplicationWindow.qml+1−2
@@ -375,8 +375,7 @@ Rectangle {
375375
376376 waitUntilTransitionsEnd();
377377
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");
378+ var fakeSurface = fakeSession.surface;
380379 fakeSurface.touchPressCount = 0;
381380 fakeSurface.touchReleaseCount = 0;
382381
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+42−1
@@ -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>
@@ -567,5 +567,46 @@ Row {
567567
568568 tryCompare(greeter, "showProgress", data.unlocked ? 0 : 1);
569569 }
570+
571+ function test_tapOnRightEdgeReachesApplicationSurface() {
572+ var topmostSpreadDelegate = findChild(shell, "appDelegate0");
573+ var topmostSurface = findChild(topmostSpreadDelegate, "surfaceContainer").surface;
574+ var rightEdgeDragArea = findChild(shell, "spreadDragArea");
575+
576+ topmostSurface.touchPressCount = 0;
577+ topmostSurface.touchReleaseCount = 0;
578+
579+ var tapPoint = rightEdgeDragArea.mapToItem(shell, rightEdgeDragArea.width / 2,
580+ rightEdgeDragArea.height / 2);
581+
582+ tap(shell, tapPoint.x, tapPoint.y);
583+
584+ tryCompare(topmostSurface, "touchPressCount", 1);
585+ tryCompare(topmostSurface, "touchReleaseCount", 1);
586+ }
587+
588+ /*
589+ Perform a right edge drag over an application surface and check
590+ that no touch event was sent to it (ie, they were all consumed
591+ by the right-edge drag area)
592+ */
593+ function test_rightEdgeDragDoesNotReachApplicationSurface() {
594+ var topmostSpreadDelegate = findChild(shell, "appDelegate0");
595+ var topmostSurface = findChild(topmostSpreadDelegate, "surfaceContainer").surface;
596+ var rightEdgeDragArea = findChild(shell, "spreadDragArea");
597+
598+ topmostSurface.touchPressCount = 0;
599+ topmostSurface.touchReleaseCount = 0;
600+
601+ var gestureStartPoint = rightEdgeDragArea.mapToItem(shell, rightEdgeDragArea.width / 2,
602+ rightEdgeDragArea.height / 2);
603+
604+ touchFlick(shell,
605+ gestureStartPoint.x /* fromX */, gestureStartPoint.y /* fromY */,
606+ units.gu(1) /* toX */, gestureStartPoint.y /* toY */);
607+
608+ tryCompare(topmostSurface, "touchPressCount", 0);
609+ tryCompare(topmostSurface, "touchReleaseCount", 0);
610+ }
570611 }
571612}
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
@@ -325,6 +325,8 @@ TestCase {
325325 }
326326
327327 Component.onCompleted: {
328+ UT.Util.ensureTouchRegistryInstalled();
329+
328330 var rootItem = parent;
329331 while (rootItem.parent != undefined) {
330332 rootItem = rootItem.parent;
tests/utils/modules/Unity/Test/testutil.cpp+37−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,16 @@
1919
2020#include <qpa/qwindowsysteminterface.h>
2121#include <QtGui/QGuiApplication>
22+#include <QQuickView>
23+
24+// UbuntuGestures lib
25+#include <TouchRegistry.h>
2226
2327TestUtil::TestUtil(QObject *parent)
2428 : QObject(parent)
2529 , m_targetWindow(0)
2630 , m_touchDevice(0)
31+ , m_installedTouchRegistry(false)
2732{
2833}
2934
@@ -52,6 +57,7 @@ TouchEventSequenceWrapper *TestUtil::touchEvent()
5257{
5358 ensureTargetWindow();
5459 ensureTouchDevice();
60+ ensureTouchRegistryInstalled();
5561
5662 return new TouchEventSequenceWrapper(
5763 QTest::touchEvent(m_targetWindow, m_touchDevice, /* autoCommit */ false));
@@ -59,7 +65,7 @@ TouchEventSequenceWrapper *TestUtil::touchEvent()
5965
6066void TestUtil::ensureTargetWindow()
6167{
62− if (!m_targetWindow)
68+ if (!m_targetWindow && !QGuiApplication::topLevelWindows().isEmpty())
6369 m_targetWindow = QGuiApplication::topLevelWindows()[0];
6470}
6571
@@ -71,3 +77,32 @@ void TestUtil::ensureTouchDevice()
7177 QWindowSystemInterface::registerTouchDevice(m_touchDevice);
7278 }
7379}
80+
81+void TestUtil::ensureTouchRegistryInstalled()
82+{
83+ if (m_installedTouchRegistry)
84+ return;
85+
86+ TouchRegistry *touchRegistry;
87+ if (TouchRegistry::instance() == nullptr) {
88+ touchRegistry = new TouchRegistry;
89+ touchRegistry->setParent(this);
90+ } else {
91+ touchRegistry = TouchRegistry::instance();
92+ if (touchRegistry->parent() != this) {
93+ // someone else created it. leave it alone
94+ m_installedTouchRegistry = true;
95+ }
96+ }
97+
98+ ensureTargetWindow();
99+
100+ if (m_targetWindow) {
101+ QQuickView *view = qobject_cast<QQuickView*>(m_targetWindow);
102+ if (view) {
103+ view->installEventFilter(touchRegistry);
104+ touchRegistry->setParent(view);
105+ m_installedTouchRegistry = true;
106+ }
107+ }
108+}
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)