on xenial help sign in
merge the kraken
978a817Michael Zanetti
CMakeLists.txt+2
@@ -58,6 +58,7 @@ find_package(Qt5DBus 5.2 REQUIRED)
58include(GNUInstallDirs)
58include(GNUInstallDirs)
59
59
60set(SHELL_APP_DIR ${CMAKE_INSTALL_DATADIR}/unity8)
60set(SHELL_APP_DIR ${CMAKE_INSTALL_DATADIR}/unity8)
61+set(SHELL_PRIVATE_LIBDIR ${CMAKE_INSTALL_LIBDIR}/unity8)
61
62
62execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE} --variable=plugindir_suffix unity-shell-api OUTPUT_VARIABLE SHELL_INSTALL_QML OUTPUT_STRIP_TRAILING_WHITESPACE)
63execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE} --variable=plugindir_suffix unity-shell-api OUTPUT_VARIABLE SHELL_INSTALL_QML OUTPUT_STRIP_TRAILING_WHITESPACE)
63if(SHELL_INSTALL_QML STREQUAL "")
64if(SHELL_INSTALL_QML STREQUAL "")
@@ -109,6 +110,7 @@ include_directories(
109
110
110# add subdirectories to build
111# add subdirectories to build
111add_subdirectory(include)
112add_subdirectory(include)
113+add_subdirectory(libs)
112add_subdirectory(src)
114add_subdirectory(src)
113add_subdirectory(tools)
115add_subdirectory(tools)
114add_subdirectory(qml)
116add_subdirectory(qml)
debian/control+1−1
@@ -86,7 +86,7 @@ Depends: gsettings-desktop-schemas,
86 qmenumodel-qml (>= 0.2.8),
86 qmenumodel-qml (>= 0.2.8),
87 qml-module-qtquick-xmllistmodel,
87 qml-module-qtquick-xmllistmodel,
88 qtdeclarative5-gsettings1.0,
88 qtdeclarative5-gsettings1.0,
89− qtdeclarative5-qtmir-plugin (>= 0.4.3),
89+ qtdeclarative5-qtmir-plugin (>= 0.4.4),
90 qtdeclarative5-ubuntu-telephony0.1,
90 qtdeclarative5-ubuntu-telephony0.1,
91 unity-launcher-impl-4,
91 unity-launcher-impl-4,
92 unity8-common (= ${source:Version}),
92 unity8-common (= ${source:Version}),
debian/unity8-private.install+1
@@ -9,5 +9,6 @@ usr/lib/*/unity8/qml/SessionBroadcast
9usr/lib/*/unity8/qml/Ubuntu
9usr/lib/*/unity8/qml/Ubuntu
10usr/lib/*/unity8/qml/Unity
10usr/lib/*/unity8/qml/Unity
11usr/lib/*/unity8/qml/Utils
11usr/lib/*/unity8/qml/Utils
12+usr/lib/*/unity8/libUbuntuGestures*
12usr/share/accountsservice/interfaces
13usr/share/accountsservice/interfaces
13usr/share/dbus-1/interfaces
14usr/share/dbus-1/interfaces
libs/CMakeLists.txtnew file+1
@@ -0,0 +1 @@
1+add_subdirectory(UbuntuGestures)
libs/UbuntuGestures/CMakeLists.txtnew file+40
@@ -0,0 +1,40 @@
1+# in order to include Qt's private headers
2+remove_definitions(-DQT_NO_KEYWORDS)
3+
4+set(UbuntuGestures_SOURCES
5+ CandidateInactivityTimer.cpp
6+ DebugHelpers.cpp
7+ Timer.cpp
8+ TouchOwnershipEvent.cpp
9+ TouchRegistry.cpp
10+ UnownedTouchEvent.cpp
11+)
12+
13+add_definitions(-DUBUNTUGESTURES_LIBRARY)
14+
15+add_library(UbuntuGestures SHARED ${UbuntuGestures_SOURCES})
16+
17+qt5_use_modules(UbuntuGestures Core Quick)
18+
19+# So that Foo.cpp can #include "Foo.moc"
20+include_directories(${CMAKE_CURRENT_BINARY_DIR})
21+
22+install(TARGETS UbuntuGestures
23+ DESTINATION ${SHELL_PRIVATE_LIBDIR})
24+
25+
26+# There's no cmake var for v8 include path :-/ so create one
27+LIST(GET Qt5Core_INCLUDE_DIRS 0 QtCoreDir0)
28+if(${Qt5Core_VERSION_STRING} VERSION_LESS "5.1.0")
29+ SET(Qt5V8_PRIVATE_INCLUDE_DIR ${QtCoreDir0}/../QtV8/${Qt5Core_VERSION_STRING}/QtV8)
30+else()
31+ SET(Qt5V8_PRIVATE_INCLUDE_DIR ${QtCoreDir0}/QtV8/${Qt5Core_VERSION_STRING}/QtV8)
32+endif()
33+
34+# DANGER! DANGER! Using Qt's private API!
35+include_directories(
36+ ${Qt5Qml_PRIVATE_INCLUDE_DIRS}
37+ ${Qt5Quick_INCLUDE_DIRS}
38+ ${Qt5Quick_PRIVATE_INCLUDE_DIRS}
39+ ${Qt5V8_PRIVATE_INCLUDE_DIR}
40+)
libs/UbuntuGestures/CandidateInactivityTimer.cppnew file+41
@@ -0,0 +1,41 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "CandidateInactivityTimer.h"
18+
19+namespace UbuntuGestures {
20+
21+CandidateInactivityTimer::CandidateInactivityTimer(int touchId, QQuickItem *candidate,
22+ AbstractTimerFactory &timerFactory, QObject *parent)
23+ : QObject(parent)
24+ , m_touchId(touchId)
25+ , m_candidate(candidate)
26+{
27+ m_timer = timerFactory.createTimer(this);
28+ connect(m_timer, &AbstractTimer::timeout,
29+ this, &CandidateInactivityTimer::onTimeout);
30+ m_timer->setInterval(durationMs);
31+ m_timer->setSingleShot(true);
32+ m_timer->start();
33+}
34+
35+void CandidateInactivityTimer::onTimeout()
36+{
37+ qWarning("[TouchRegistry] Candidate for touch %d defaulted!", m_touchId);
38+ Q_EMIT candidateDefaulted(m_touchId, m_candidate);
39+}
40+
41+} // namespace UbuntuGestures
libs/UbuntuGestures/CandidateInactivityTimer.hnew file+49
@@ -0,0 +1,49 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef UBUNTUGESTURES_CANDIDATE_INACTIVITY_TIMER_H
18+#define UBUNTUGESTURES_CANDIDATE_INACTIVITY_TIMER_H
19+
20+#include <QObject>
21+
22+class QQuickItem;
23+
24+#include "Timer.h"
25+
26+namespace UbuntuGestures {
27+
28+class UBUNTUGESTURES_EXPORT CandidateInactivityTimer : public QObject {
29+ Q_OBJECT
30+public:
31+ CandidateInactivityTimer(int touchId, QQuickItem *candidate,
32+ AbstractTimerFactory &timerFactory,
33+ QObject *parent = nullptr);
34+
35+ const int durationMs = 350;
36+
37+Q_SIGNALS:
38+ void candidateDefaulted(int touchId, QQuickItem *candidate);
39+private Q_SLOTS:
40+ void onTimeout();
41+private:
42+ AbstractTimer *m_timer;
43+ int m_touchId;
44+ QQuickItem *m_candidate;
45+};
46+
47+} // namespace UbuntuGestures
48+
49+#endif // UBUNTUGESTURES_CANDIDATE_INACTIVITY_TIMER_H
libs/UbuntuGestures/DebugHelpers.cppnew file+68
@@ -0,0 +1,68 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "DebugHelpers.h"
18+#include <QTouchEvent>
19+
20+QString touchPointStateToString(Qt::TouchPointState state)
21+{
22+ switch (state) {
23+ case Qt::TouchPointPressed:
24+ return QString("pressed");
25+ case Qt::TouchPointMoved:
26+ return QString("moved");
27+ case Qt::TouchPointStationary:
28+ return QString("stationary");
29+ case Qt::TouchPointReleased:
30+ return QString("released");
31+ default:
32+ return QString("INVALID_STATE");
33+ }
34+}
35+
36+QString touchEventToString(const QTouchEvent *ev)
37+{
38+ QString message;
39+
40+ switch (ev->type()) {
41+ case QEvent::TouchBegin:
42+ message.append("TouchBegin ");
43+ break;
44+ case QEvent::TouchUpdate:
45+ message.append("TouchUpdate ");
46+ break;
47+ case QEvent::TouchEnd:
48+ message.append("TouchEnd ");
49+ break;
50+ case QEvent::TouchCancel:
51+ message.append("TouchCancel ");
52+ break;
53+ default:
54+ message.append("INVALID_TOUCH_EVENT_TYPE ");
55+ }
56+
57+ foreach(const QTouchEvent::TouchPoint& touchPoint, ev->touchPoints()) {
58+ message.append(
59+ QString("(id:%1, state:%2, scenePos:(%3,%4)) ")
60+ .arg(touchPoint.id())
61+ .arg(touchPointStateToString(touchPoint.state()))
62+ .arg(touchPoint.scenePos().x())
63+ .arg(touchPoint.scenePos().y())
64+ );
65+ }
66+
67+ return message;
68+}
libs/UbuntuGestures/DebugHelpers.hnew file+29
@@ -0,0 +1,29 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef UBUNTUGESTURES_DEBUG_HELPER_H
18+#define UBUNTUGESTURES_DEBUG_HELPER_H
19+
20+#include <QString>
21+
22+#include "UbuntuGesturesGlobal.h"
23+
24+class QTouchEvent;
25+
26+UBUNTUGESTURES_EXPORT QString touchPointStateToString(Qt::TouchPointState state);
27+UBUNTUGESTURES_EXPORT QString touchEventToString(const QTouchEvent *ev);
28+
29+#endif // UBUNTUGESTURES_DEBUG_HELPER_H
libs/UbuntuGestures/Pool.hnew file+134
@@ -0,0 +1,134 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef UBUNTUGESTURES_POOL_H
18+#define UBUNTUGESTURES_POOL_H
19+
20+#include <QVector>
21+
22+#include "UbuntuGesturesGlobal.h"
23+
24+/*
25+ An object pool.
26+ Avoids unnecessary creations/initializations and deletions/destructions of items. Useful
27+ in a scenario where items are created and destroyed very frequently but the total number
28+ of items at any given time remains small. They're stored in a unordered fashion.
29+
30+ To be used in Pool, ItemType needs to have the following methods:
31+
32+ - ItemType();
33+
34+ A constructor that takes no parameters. An object contructed with it must return false if
35+ isValid() is called.
36+
37+ - bool isValid() const;
38+
39+ Returns wheter the object holds a valid , "filled" state or is empty.
40+ Used by Pool to check if the slot occupied by this object is actually available.
41+
42+ - void reset();
43+
44+ Resets the object to its initial, empty, state. After calling this method, isValid() must
45+ return false.
46+ */
47+template <class ItemType> class Pool
48+{
49+public:
50+ Pool() : m_lastUsedIndex(-1) {
51+ }
52+
53+ class Iterator {
54+ public:
55+ Iterator() : index(-1), item(nullptr) {}
56+ Iterator(int index, ItemType *item)
57+ : index(index), item(item) {}
58+
59+ ItemType *operator->() const { return item; }
60+ ItemType &operator*() const { return *item; }
61+ ItemType &value() const { return *item; }
62+
63+ Iterator &operator= (const Iterator& other) {
64+ index = other.index;
65+ item = other.item;
66+
67+ // by convention, always return *this
68+ return *this;
69+ }
70+
71+ operator bool() const { return item != nullptr; }
72+
73+ int index;
74+ ItemType *item;
75+ };
76+
77+ Iterator &end() const { return Iterator(); }
78+
79+ ItemType &getEmptySlot() {
80+ Q_ASSERT(m_lastUsedIndex < m_slots.size());
81+
82+ // Look for an in-between vacancy first
83+ for (int i = 0; i < m_lastUsedIndex; ++i) {
84+ ItemType &item = m_slots[i];
85+ if (!item.isValid()) {
86+ return item;
87+ }
88+ }
89+
90+ ++m_lastUsedIndex;
91+ if (m_lastUsedIndex >= m_slots.size()) {
92+ m_slots.resize(m_lastUsedIndex + 1);
93+ }
94+
95+ return m_slots[m_lastUsedIndex];
96+ }
97+
98+ void freeSlot(Iterator &iterator) {
99+ m_slots[iterator.index].reset();
100+ if (iterator.index == m_lastUsedIndex) {
101+ do {
102+ --m_lastUsedIndex;
103+ } while (m_lastUsedIndex >= 0 && !m_slots.at(m_lastUsedIndex).isValid());
104+ }
105+ }
106+
107+ // Iterates through all valid items (i.e. the occupied slots)
108+ // calling the given function, with the option of ending the loop early.
109+ //
110+ // bool Func(Iterator& item)
111+ //
112+ // Returning true means it wants to continue the "for" loop, false
113+ // terminates the loop.
114+ template<typename Func> void forEach(Func func) {
115+ Iterator it;
116+ for (it.index = 0; it.index <= m_lastUsedIndex; ++it.index) {
117+ it.item = &m_slots[it.index];
118+ if (!it.item->isValid())
119+ continue;
120+
121+ if (!func(it))
122+ break;
123+ }
124+ }
125+
126+ bool isEmpty() const { return m_lastUsedIndex == -1; }
127+
128+
129+private:
130+ QVector<ItemType> m_slots;
131+ int m_lastUsedIndex;
132+};
133+
134+#endif // UBUNTUGESTURES_POOL_H
libs/UbuntuGestures/Timer.cppnew file+109
@@ -0,0 +1,109 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "Timer.h"
18+
19+namespace UbuntuGestures {
20+
21+Timer::Timer(QObject *parent) : AbstractTimer(parent)
22+{
23+ m_timer.setSingleShot(false);
24+ connect(&m_timer, &QTimer::timeout, this, &AbstractTimer::timeout);
25+}
26+
27+int Timer::interval() const
28+{
29+ return m_timer.interval();
30+}
31+
32+void Timer::setInterval(int msecs)
33+{
34+ m_timer.setInterval(msecs);
35+}
36+
37+void Timer::start()
38+{
39+ m_timer.start();
40+ AbstractTimer::start();
41+}
42+
43+void Timer::stop()
44+{
45+ m_timer.stop();
46+ AbstractTimer::stop();
47+}
48+
49+bool Timer::isSingleShot() const
50+{
51+ return m_timer.isSingleShot();
52+}
53+
54+void Timer::setSingleShot(bool value)
55+{
56+ m_timer.setSingleShot(value);
57+}
58+
59+/////////////////////////////////// FakeTimer //////////////////////////////////
60+
61+FakeTimer::FakeTimer(QObject *parent)
62+ : UbuntuGestures::AbstractTimer(parent)
63+ , m_interval(0)
64+ , m_singleShot(false)
65+{
66+}
67+
68+int FakeTimer::interval() const
69+{
70+ return m_interval;
71+}
72+
73+void FakeTimer::setInterval(int msecs)
74+{
75+ m_interval = msecs;
76+}
77+
78+bool FakeTimer::isSingleShot() const
79+{
80+ return m_singleShot;
81+}
82+
83+void FakeTimer::setSingleShot(bool value)
84+{
85+ m_singleShot = value;
86+}
87+
88+/////////////////////////////////// FakeTimerFactory //////////////////////////////////
89+
90+AbstractTimer *FakeTimerFactory::createTimer(QObject *parent)
91+{
92+ FakeTimer *fakeTimer = new FakeTimer(parent);
93+
94+ timers.append(fakeTimer);
95+
96+ return fakeTimer;
97+}
98+
99+void FakeTimerFactory::makeRunningTimersTimeout()
100+{
101+ for (int i = 0; i < timers.count(); ++i) {
102+ FakeTimer *timer = timers[i].data();
103+ if (timer && timer->isRunning()) {
104+ timer->emitTimeout();
105+ }
106+ }
107+}
108+
109+} // namespace UbuntuGestures
libs/UbuntuGestures/Timer.hnew file+105
@@ -0,0 +1,105 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef UBUNTUGESTURES_TIMER_H
18+#define UBUNTUGESTURES_TIMER_H
19+
20+#include "UbuntuGesturesGlobal.h"
21+
22+#include <QObject>
23+#include <QPointer>
24+#include <QTimer>
25+
26+namespace UbuntuGestures {
27+
28+/* Defines an interface for a Timer. Useful for tests. */
29+class UBUNTUGESTURES_EXPORT AbstractTimer : public QObject
30+{
31+ Q_OBJECT
32+public:
33+ AbstractTimer(QObject *parent) : QObject(parent), m_isRunning(false) {}
34+ virtual int interval() const = 0;
35+ virtual void setInterval(int msecs) = 0;
36+ virtual void start() { m_isRunning = true; }
37+ virtual void stop() { m_isRunning = false; }
38+ bool isRunning() const { return m_isRunning; }
39+ virtual bool isSingleShot() const = 0;
40+ virtual void setSingleShot(bool value) = 0;
41+Q_SIGNALS:
42+ void timeout();
43+private:
44+ bool m_isRunning;
45+};
46+
47+/* Essentially a QTimer wrapper */
48+class UBUNTUGESTURES_EXPORT Timer : public AbstractTimer
49+{
50+ Q_OBJECT
51+public:
52+ Timer(QObject *parent = nullptr);
53+
54+ int interval() const override;
55+ void setInterval(int msecs) override;
56+ void start() override;
57+ void stop() override;
58+ bool isSingleShot() const override;
59+ void setSingleShot(bool value) override;
60+private:
61+ QTimer m_timer;
62+};
63+
64+/* For tests */
65+class UBUNTUGESTURES_EXPORT FakeTimer : public AbstractTimer
66+{
67+ Q_OBJECT
68+public:
69+ FakeTimer(QObject *parent = nullptr);
70+
71+ virtual void emitTimeout() { Q_EMIT timeout(); }
72+
73+ int interval() const override;
74+ void setInterval(int msecs) override;
75+ bool isSingleShot() const override;
76+ void setSingleShot(bool value) override;
77+private:
78+ int m_interval;
79+ bool m_singleShot;
80+};
81+
82+class UBUNTUGESTURES_EXPORT AbstractTimerFactory
83+{
84+public:
85+ virtual ~AbstractTimerFactory() {}
86+ virtual AbstractTimer *createTimer(QObject *parent = nullptr) = 0;
87+};
88+
89+class UBUNTUGESTURES_EXPORT TimerFactory : public AbstractTimerFactory
90+{
91+public:
92+ AbstractTimer *createTimer(QObject *parent = nullptr) override { return new Timer(parent); }
93+};
94+
95+class UBUNTUGESTURES_EXPORT FakeTimerFactory : public AbstractTimerFactory
96+{
97+public:
98+ AbstractTimer *createTimer(QObject *parent = nullptr) override;
99+ void makeRunningTimersTimeout();
100+ QList<QPointer<FakeTimer>> timers;
101+};
102+
103+} // namespace UbuntuGestures
104+
105+#endif // UBUNTUGESTURES_TIMER_H
libs/UbuntuGestures/TouchOwnershipEvent.cppnew file+35
@@ -0,0 +1,35 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "TouchOwnershipEvent.h"
18+
19+QEvent::Type TouchOwnershipEvent::m_touchOwnershipType = (QEvent::Type)-1;
20+
21+TouchOwnershipEvent::TouchOwnershipEvent(int touchId, bool gained)
22+ : QEvent(touchOwnershipEventType())
23+ , m_touchId(touchId)
24+ , m_gained(gained)
25+{
26+}
27+
28+QEvent::Type TouchOwnershipEvent::touchOwnershipEventType()
29+{
30+ if (m_touchOwnershipType == (QEvent::Type)-1) {
31+ m_touchOwnershipType = (QEvent::Type)registerEventType();
32+ }
33+
34+ return m_touchOwnershipType;
35+}
libs/UbuntuGestures/TouchOwnershipEvent.hnew file+50
@@ -0,0 +1,50 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef UBUNTU_TOUCHOWNERSHIPEVENT_H
18+#define UBUNTU_TOUCHOWNERSHIPEVENT_H
19+
20+#include <QEvent>
21+#include "UbuntuGesturesGlobal.h"
22+
23+/*
24+ When an item get an ownership event for a touch it can grab/steal that touch
25+ with a clean conscience.
26+ */
27+class UBUNTUGESTURES_EXPORT TouchOwnershipEvent : public QEvent
28+{
29+public:
30+ TouchOwnershipEvent(int touchId, bool gained);
31+
32+ static Type touchOwnershipEventType();
33+
34+ /*
35+ Whether ownership was gained (true) or lost (false)
36+ */
37+ bool gained() const { return m_gained; }
38+
39+ /*
40+ Id of the touch whose ownership was granted.
41+ */
42+ int touchId() const { return m_touchId; }
43+
44+private:
45+ static Type m_touchOwnershipType;
46+ int m_touchId;
47+ bool m_gained;
48+};
49+
50+#endif // UBUNTU_TOUCHOWNERSHIPEVENT_H
libs/UbuntuGestures/TouchRegistry.cppnew file+500
@@ -0,0 +1,500 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "TouchRegistry.h"
18+
19+#include <QCoreApplication>
20+#include <QDebug>
21+
22+#pragma GCC diagnostic push
23+#pragma GCC diagnostic ignored "-pedantic"
24+#include <private/qquickitem_p.h>
25+#pragma GCC diagnostic pop
26+
27+#include "CandidateInactivityTimer.h"
28+#include "Timer.h"
29+#include "TouchOwnershipEvent.h"
30+#include "UnownedTouchEvent.h"
31+
32+#define TOUCHREGISTRY_DEBUG 0
33+
34+#if TOUCHREGISTRY_DEBUG
35+ #include "DebugHelpers.h"
36+ #define UG_DEBUG qDebug() << "[TouchRegistry]"
37+#endif // TOUCHREGISTRY_DEBUG
38+
39+using namespace UbuntuGestures;
40+
41+TouchRegistry *TouchRegistry::m_instance = nullptr;
42+
43+TouchRegistry::TouchRegistry(QObject *parent)
44+ : TouchRegistry(parent, new TimerFactory)
45+{
46+}
47+
48+TouchRegistry::TouchRegistry(QObject *parent, AbstractTimerFactory *timerFactory)
49+ : QObject(parent)
50+ , m_inDispatchLoop(false)
51+ , m_timerFactory(timerFactory)
52+{
53+ if (m_instance == nullptr) {
54+ m_instance = this;
55+ } else {
56+ qFatal("Cannot have more than one instance of TouchRegistry. It must be a singleton.");
57+ }
58+}
59+
60+TouchRegistry::~TouchRegistry()
61+{
62+ Q_ASSERT(m_instance != nullptr);
63+ m_instance = nullptr;
64+ delete m_timerFactory;
65+}
66+
67+void TouchRegistry::update(const QTouchEvent *event)
68+{
69+ #if TOUCHREGISTRY_DEBUG
70+ UG_DEBUG << "got" << qPrintable(touchEventToString(event));
71+ #endif
72+
73+ const QList<QTouchEvent::TouchPoint> &touchPoints = event->touchPoints();
74+ for (int i = 0; i < touchPoints.count(); ++i) {
75+ const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
76+ if (touchPoint.state() == Qt::TouchPointPressed) {
77+ TouchInfo &touchInfo = m_touchInfoPool.getEmptySlot();
78+ touchInfo.init(touchPoint.id());
79+ } else if (touchPoint.state() == Qt::TouchPointReleased) {
80+ Pool<TouchInfo>::Iterator touchInfo = findTouchInfo(touchPoint.id());
81+
82+ touchInfo->physicallyEnded = true;
83+ }
84+ }
85+
86+ deliverTouchUpdatesToUndecidedCandidatesAndWatchers(event);
87+
88+ freeEndedTouchInfos();
89+}
90+
91+void TouchRegistry::deliverTouchUpdatesToUndecidedCandidatesAndWatchers(const QTouchEvent *event)
92+{
93+ // TODO: Look into how we could optimize this whole thing.
94+ // Although it's not really a problem as we should have at most two candidates
95+ // for each point and there should not be many active points at any given moment.
96+ // But having three nested for-loops does scare.
97+
98+ // TODO: Don't send it to the object that is already receiving the regular event
99+ // because QQuickWindow is sending it to him (i.e., he's the touch owner from Qt's point of view)
100+ // Problem is, we cannnot easily get this information.
101+
102+ const QList<QTouchEvent::TouchPoint> &updatedTouchPoints = event->touchPoints();
103+
104+ // Maps an item to the touches in this event he should be informed about.
105+ // E.g.: a QTouchEvent might have three touches but a given item might be interested in only
106+ // one of them. So he will get a UnownedTouchEvent from this QTouchEvent containing only that
107+ // touch point.
108+ QMap<QQuickItem*, QList<int>> touchIdsForItems;
109+
110+ // Build touchIdsForItems
111+ m_touchInfoPool.forEach([&](Pool<TouchInfo>::Iterator &touchInfo) {
112+ if (touchInfo->isOwned() && touchInfo->watchers.isEmpty())
113+ return true;
114+
115+ for (int j = 0; j < updatedTouchPoints.count(); ++j) {
116+ if (updatedTouchPoints[j].id() == touchInfo->id) {
117+ if (!touchInfo->isOwned()) {
118+ for (int i = 0; i < touchInfo->candidates.count(); ++i) {
119+ CandidateInfo &candidate = touchInfo->candidates[i];
120+ Q_ASSERT(!candidate.item.isNull());
121+ touchIdsForItems[candidate.item.data()].append(touchInfo->id);
122+ }
123+ }
124+
125+ const QList<QPointer<QQuickItem>> &watchers = touchInfo->watchers;
126+ for (int i = 0; i < watchers.count(); ++i) {
127+ if (!watchers[i].isNull()) {
128+ touchIdsForItems[watchers[i].data()].append(touchInfo->id);
129+ }
130+ }
131+
132+ return true;
133+ }
134+ }
135+
136+ return true;
137+ });
138+
139+ // TODO: Consider what happens if an item calls any of TouchRegistry's public methods
140+ // from the event handler callback.
141+ m_inDispatchLoop = true;
142+ auto it = touchIdsForItems.constBegin();
143+ while (it != touchIdsForItems.constEnd()) {
144+ QQuickItem *item = it.key();
145+ const QList<int> &touchIds = it.value();
146+ dispatchPointsToItem(event, touchIds, item);
147+ ++it;
148+ };
149+ m_inDispatchLoop = false;
150+}
151+
152+void TouchRegistry::freeEndedTouchInfos()
153+{
154+ m_touchInfoPool.forEach([&](Pool<TouchInfo>::Iterator &touchInfo) {
155+ if (touchInfo->ended()) {
156+ m_touchInfoPool.freeSlot(touchInfo);
157+ }
158+ return true;
159+ });
160+}
161+
162+/*
163+ Extracts the touches with the given touchIds from event and send them in a
164+ UnownedTouchEvent to the given item
165+ */
166+void TouchRegistry::dispatchPointsToItem(const QTouchEvent *event, const QList<int> &touchIds,
167+ QQuickItem *item)
168+{
169+ Qt::TouchPointStates touchPointStates = 0;
170+ QList<QTouchEvent::TouchPoint> touchPoints;
171+
172+ const QList<QTouchEvent::TouchPoint> &allTouchPoints = event->touchPoints();
173+
174+ QTransform windowToCandidateTransform = QQuickItemPrivate::get(item)->windowToItemTransform();
175+ QMatrix4x4 windowToCandidateMatrix(windowToCandidateTransform);
176+
177+ for (int i = 0; i < allTouchPoints.count(); ++i) {
178+ const QTouchEvent::TouchPoint &originalTouchPoint = allTouchPoints[i];
179+ if (touchIds.contains(originalTouchPoint.id())) {
180+ QTouchEvent::TouchPoint touchPoint = originalTouchPoint;
181+
182+ translateTouchPointFromScreenToWindowCoords(touchPoint);
183+
184+ // Set the point's local coordinates to that of the item
185+ touchPoint.setRect(windowToCandidateTransform.mapRect(touchPoint.sceneRect()));
186+ touchPoint.setStartPos(windowToCandidateTransform.map(touchPoint.startScenePos()));
187+ touchPoint.setLastPos(windowToCandidateTransform.map(touchPoint.lastScenePos()));
188+ touchPoint.setVelocity(windowToCandidateMatrix.mapVector(touchPoint.velocity()).toVector2D());
189+
190+ touchPoints.append(touchPoint);
191+ touchPointStates |= touchPoint.state();
192+ }
193+ }
194+
195+ QTouchEvent *eventForItem = new QTouchEvent(event->type(),
196+ event->device(),
197+ event->modifiers(),
198+ touchPointStates,
199+ touchPoints);
200+ eventForItem->setWindow(event->window());
201+ eventForItem->setTimestamp(event->timestamp());
202+ eventForItem->setTarget(event->target());
203+
204+ UnownedTouchEvent unownedTouchEvent(eventForItem);
205+
206+ #if TOUCHREGISTRY_DEBUG
207+ UG_DEBUG << "Sending unowned" << qPrintable(touchEventToString(eventForItem))
208+ << "to" << item;
209+ #endif
210+
211+ QCoreApplication::sendEvent(item, &unownedTouchEvent);
212+}
213+
214+void TouchRegistry::translateTouchPointFromScreenToWindowCoords(QTouchEvent::TouchPoint &touchPoint)
215+{
216+ touchPoint.setScreenRect(touchPoint.sceneRect());
217+ touchPoint.setStartScreenPos(touchPoint.startScenePos());
218+ touchPoint.setLastScreenPos(touchPoint.lastScenePos());
219+
220+ touchPoint.setSceneRect(touchPoint.rect());
221+ touchPoint.setStartScenePos(touchPoint.startPos());
222+ touchPoint.setLastScenePos(touchPoint.lastPos());
223+}
224+
225+bool TouchRegistry::eventFilter(QObject *watched, QEvent *event)
226+{
227+ Q_UNUSED(watched);
228+
229+ switch (event->type()) {
230+ case QEvent::TouchBegin:
231+ case QEvent::TouchUpdate:
232+ case QEvent::TouchEnd:
233+ case QEvent::TouchCancel:
234+ update(static_cast<QTouchEvent*>(event));
235+ break;
236+ default:
237+ // do nothing
238+ break;
239+ }
240+
241+ // Do not filter out the event. i.e., let it be handled further as
242+ // we're just monitoring events
243+ return false;
244+}
245+
246+void TouchRegistry::addCandidateOwnerForTouch(int id, QQuickItem *candidate)
247+{
248+ #if TOUCHREGISTRY_DEBUG
249+ UG_DEBUG << "addCandidateOwnerForTouch id" << id << "candidate" << candidate;
250+ #endif
251+
252+ Pool<TouchInfo>::Iterator touchInfo = findTouchInfo(id);
253+ if (!touchInfo) { qFatal("TouchRegistry: Failed to find TouchInfo"); }
254+
255+ if (touchInfo->isOwned()) {
256+ qWarning("TouchRegistry: trying to add candidate owner for a touch that's already owned");
257+ return;
258+ }
259+
260+ // TODO: Check if candidate already exists
261+
262+ CandidateInfo candidateInfo;
263+ candidateInfo.undecided = true;
264+ candidateInfo.item = candidate;
265+ candidateInfo.inactivityTimer = new CandidateInactivityTimer(id, candidate,
266+ *m_timerFactory,
267+ this);
268+ connect(candidateInfo.inactivityTimer, &CandidateInactivityTimer::candidateDefaulted,
269+ this, &TouchRegistry::rejectCandidateOwnerForTouch);
270+
271+ touchInfo->candidates.append(candidateInfo);
272+}
273+
274+void TouchRegistry::addTouchWatcher(int touchId, QQuickItem *watcher)
275+{
276+ #if TOUCHREGISTRY_DEBUG
277+ UG_DEBUG << "addTouchWatcher id" << touchId << "watcher" << watcher;
278+ #endif
279+
280+ Pool<TouchInfo>::Iterator touchInfo = findTouchInfo(touchId);
281+ if (!touchInfo) { qFatal("TouchRegistry: Failed to find TouchInfo"); }
282+
283+ // TODO: Check if watcher already exists
284+
285+ touchInfo->watchers.append(watcher);
286+}
287+
288+void TouchRegistry::removeCandidateOwnerForTouch(int id, QQuickItem *candidate)
289+{
290+ #if TOUCHREGISTRY_DEBUG
291+ UG_DEBUG << "removeCandidateOwnerForTouch id" << id << "candidate" << candidate;
292+ #endif
293+
294+ Pool<TouchInfo>::Iterator touchInfo = findTouchInfo(id);
295+ if (!touchInfo) { qFatal("TouchRegistry: Failed to find TouchInfo"); }
296+
297+ int indexRemoved = -1;
298+
299+ // TODO: check if the candidate is in fact the owner of the touch
300+
301+ for (int i = 0; i < touchInfo->candidates.count() && indexRemoved == -1; ++i) {
302+ CandidateInfo &candidateInfo = touchInfo->candidates[i];
303+ if (candidateInfo.item == candidate) {
304+ Q_ASSERT(i > 0 || candidateInfo.undecided);
305+ if (i == 0 && !candidateInfo.undecided) {
306+ qCritical("TouchRegistry: touch owner is being removed.");
307+ }
308+ delete candidateInfo.inactivityTimer;
309+ candidateInfo.inactivityTimer = nullptr;
310+ touchInfo->candidates.removeAt(i);
311+ indexRemoved = i;
312+ }
313+ }
314+
315+ if (indexRemoved == 0) {
316+ // the top candidate has been removed. if the new top candidate
317+ // wants the touch let him know he's now the owner.
318+ if (touchInfo->isOwned()) {
319+ touchInfo->notifyCandidatesOfOwnershipResolution();
320+ }
321+ }
322+
323+ if (!m_inDispatchLoop && touchInfo->ended()) {
324+ m_touchInfoPool.freeSlot(touchInfo);
325+ }
326+}
327+
328+void TouchRegistry::requestTouchOwnership(int id, QQuickItem *candidate)
329+{
330+ #if TOUCHREGISTRY_DEBUG
331+ UG_DEBUG << "requestTouchOwnership id " << id << "candidate" << candidate;
332+ #endif
333+
334+ Pool<TouchInfo>::Iterator touchInfo = findTouchInfo(id);
335+ if (!touchInfo) { qFatal("TouchRegistry: Failed to find TouchInfo"); }
336+
337+ Q_ASSERT(!touchInfo->isOwned());
338+
339+ int candidateIndex = -1;
340+ for (int i = 0; i < touchInfo->candidates.count(); ++i) {
341+ CandidateInfo &candidateInfo = touchInfo->candidates[i];
342+ if (candidateInfo.item == candidate) {
343+ candidateInfo.undecided = false;
344+ delete candidateInfo.inactivityTimer;
345+ candidateInfo.inactivityTimer = nullptr;
346+ candidateIndex = i;
347+ break;
348+ }
349+ }
350+
351+ // add it as a candidate if not present yet
352+ if (candidateIndex < 0) {
353+ CandidateInfo candidateInfo;
354+ candidateInfo.undecided = false;
355+ candidateInfo.item = candidate;
356+ candidateInfo.inactivityTimer = nullptr;
357+ touchInfo->candidates.append(candidateInfo);
358+ // it's the last one
359+ candidateIndex = touchInfo->candidates.count() - 1;
360+ }
361+
362+ // If it's the top candidate it means it's now the owner. Let
363+ // it know about it.
364+ if (candidateIndex == 0) {
365+ touchInfo->notifyCandidatesOfOwnershipResolution();
366+ }
367+}
368+
369+Pool<TouchRegistry::TouchInfo>::Iterator TouchRegistry::findTouchInfo(int id)
370+{
371+ Pool<TouchInfo>::Iterator touchInfo;
372+
373+ m_touchInfoPool.forEach([&](Pool<TouchInfo>::Iterator &someTouchInfo) -> bool {
374+ if (someTouchInfo->id == id) {
375+ touchInfo = someTouchInfo;
376+ return false;
377+ } else {
378+ return true;
379+ }
380+ });
381+
382+ return touchInfo;
383+}
384+
385+
386+void TouchRegistry::rejectCandidateOwnerForTouch(int id, QQuickItem *candidate)
387+{
388+ // NB: It's technically possible that candidate is a dangling pointer at this point.
389+ // Although that would most likely be due to a bug in our code.
390+ // In any case, only dereference it after it's confirmed that it indeed exists.
391+
392+ #if TOUCHREGISTRY_DEBUG
393+ UG_DEBUG << "rejectCandidateOwnerForTouch id" << id << "candidate" << (void*)candidate;
394+ #endif
395+
396+ Pool<TouchInfo>::Iterator touchInfo = findTouchInfo(id);
397+ if (!touchInfo) {
398+ #if TOUCHREGISTRY_DEBUG
399+ UG_DEBUG << "Failed to find TouchInfo for id" << id;
400+ #endif
401+ return;
402+ }
403+
404+ int rejectedCandidateIndex = -1;
405+
406+ // Check if the given candidate is valid and still undecided
407+ for (int i = 0; i < touchInfo->candidates.count() && rejectedCandidateIndex == -1; ++i) {
408+ CandidateInfo &candidateInfo = touchInfo->candidates[i];
409+ if (candidateInfo.item == candidate) {
410+ Q_ASSERT(i > 0 || candidateInfo.undecided);
411+ if (i == 0 && !candidateInfo.undecided) {
412+ qCritical() << "TouchRegistry: Can't reject item (" << (void*)candidate
413+ << ") as it already owns touch" << id;
414+ return;
415+ } else {
416+ // we found the guy and it's all fine.
417+ rejectedCandidateIndex = i;
418+ }
419+ }
420+ }
421+
422+ // If we reached this point it's because the given candidate exists and is indeed undecided.
423+
424+ Q_ASSERT(rejectedCandidateIndex >= 0 && rejectedCandidateIndex < touchInfo->candidates.size());
425+
426+ {
427+ TouchOwnershipEvent lostOwnershipEvent(id, false /*gained*/);
428+ QCoreApplication::sendEvent(candidate, &lostOwnershipEvent);
429+ }
430+
431+ touchInfo->candidates.removeAt(rejectedCandidateIndex);
432+
433+ if (rejectedCandidateIndex == 0) {
434+ // the top candidate has been removed. if the new top candidate
435+ // wants the touch let him know he's now the owner.
436+ if (touchInfo->isOwned()) {
437+ touchInfo->notifyCandidatesOfOwnershipResolution();
438+ }
439+ }
440+}
441+
442+////////////////////////////////////// TouchRegistry::TouchInfo ////////////////////////////////////
443+
444+TouchRegistry::TouchInfo::TouchInfo(int id)
445+{
446+ init(id);
447+}
448+
449+void TouchRegistry::TouchInfo::reset()
450+{
451+ id = -1;
452+
453+ for (int i = 0; i < candidates.count(); ++i) {
454+ CandidateInfo &candidate = candidates[i];
455+ delete candidate.inactivityTimer;
456+ candidate.inactivityTimer.clear(); // shoundn't be needed but anyway...
457+ }
458+}
459+
460+void TouchRegistry::TouchInfo::init(int id)
461+{
462+ this->id = id;
463+ physicallyEnded = false;
464+ candidates.clear();
465+ watchers.clear();
466+}
467+
468+bool TouchRegistry::TouchInfo::isOwned() const
469+{
470+ return !candidates.isEmpty() && !candidates.first().undecided;
471+}
472+
473+bool TouchRegistry::TouchInfo::ended() const
474+{
475+ Q_ASSERT(isValid());
476+ return physicallyEnded && (isOwned() || candidates.isEmpty());
477+}
478+
479+void TouchRegistry::TouchInfo::notifyCandidatesOfOwnershipResolution()
480+{
481+ Q_ASSERT(isOwned());
482+
483+ #if TOUCHREGISTRY_DEBUG
484+ UG_DEBUG << "sending TouchOwnershipEvent(id =" << id
485+ << " gained) to candidate" << candidates[0].item;
486+ #endif
487+
488+ TouchOwnershipEvent gainedOwnershipEvent(id, true /*gained*/);
489+ QCoreApplication::sendEvent(candidates[0].item, &gainedOwnershipEvent);
490+
491+
492+ TouchOwnershipEvent lostOwnershipEvent(id, false /*gained*/);
493+ for (int i = 1; i < candidates.count(); ++i) {
494+ #if TOUCHREGISTRY_DEBUG
495+ UG_DEBUG << "sending TouchWonershipEvent(id =" << id << " lost) to candidate"
496+ << candidates[i].item;
497+ #endif
498+ QCoreApplication::sendEvent(candidates[i].item, &lostOwnershipEvent);
499+ }
500+}
libs/UbuntuGestures/TouchRegistry.hnew file+182
@@ -0,0 +1,182 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef UNITY_TOUCHREGISTRY_H
18+#define UNITY_TOUCHREGISTRY_H
19+
20+#include <QQuickItem>
21+#include <QObject>
22+#include <QPointer>
23+#include <QTouchEvent>
24+#include <QVector>
25+
26+#include "UbuntuGesturesGlobal.h"
27+#include "CandidateInactivityTimer.h"
28+#include "Pool.h"
29+
30+namespace UbuntuGestures {
31+ class AbstractTimerFactory;
32+}
33+
34+/*
35+ Where the ownership of touches is registered.
36+
37+ Singleton used for adding a touch point ownership model analogous to the one
38+ described in the XInput 2.2 protocol[1] on top of the existing input dispatch logic in QQuickWindow.
39+
40+ It provides a much more flexible and powerful way of dealing with pointer ownership than the existing
41+ mechanisms in Qt. Namely QQuickItem::grabTouchPoints, QuickItem::keepTouchGrab,
42+ QQuickItem::setFiltersChildMouseEvents, QQuickItem::ungrabTouchPoints and QQuickItem::touchUngrabEvent.
43+
44+ Usage:
45+
46+ 1- An item receives a a new touch point. If he's not sure whether he wants it yet, he calls:
47+ TouchRegistry::instance()->addCandidateOwnerForTouch(touchId, this);
48+ touchEvent->ignore();
49+ Ignoring the event is crucial so that it can be seen by other interested parties, which will
50+ behave similarly.
51+
52+ 2- That item will then start receiving UnownedTouchEvents for that touch from step 1. Once he's
53+ made a decision he calls either:
54+ TouchRegistry::instance()->requestTouchOwnership(touchId, this);
55+ If he wants the touch point or:
56+ TouchRegistry::instance()->removeCandidateOwnerForTouch(touchId, this);
57+  if he does not want it.
58+
59+ Candidates are put in a priority queue. The first one to call addCandidateOwnerForTouch() will
60+ take precedence over the others for receiving ownership over the touch point (from now on called
61+ simply top-candidate).
62+
63+ If the top-candidate calls requestTouchOwnership() he will immediately receive a
64+ TouchOwnershipEvent(gained=true) for that touch point. He can then safely call
65+ QQuickItem::grabTouchPoints to actually get the owned touch points. The other candidates
66+ will receive TouchOwnershipEvent(gained=false) and will no longer receive UnownedTouchEvents
67+ for that touch point. They will have to undo whatever action they were performing with that
68+ touch point.
69+
70+ But if the top-candidate calls removeCandidateOwnerForTouch() instead, he's popped from the
71+ candidacy queue and ownership is given to the new top-most candidate if he has already
72+ made his decision, that is.
73+
74+ The TouchRegistry cannot enforce the results of this pointer ownership negotiation (i.e.,
75+ who gets to grab the touch points) as that would clash with QQuickWindow's input event
76+ dispatching logic. The candidates have to respect the decision and grab the touch points
77+ themselves.
78+
79+ If an item wants ownership over touches as soon as he receives the TouchBegin for them, his step 1
80+ would be instead:
81+ TouchRegistry::instance()->requestTouchOwnership(touchId, this);
82+ return true;
83+ He would then be notified once ownership has been granted to him, from which point onwards he could
84+ safely assume other TouchRegistry users wouldn't snatch this touch away from him.
85+
86+ Items oblivious to TouchRegistry will lose their touch points without warning, just like in plain Qt.
87+
88+ [1] - http://www.x.org/releases/X11R7.7/doc/inputproto/XI2proto.txt (see multitouch-ownership)
89+ */
90+class UBUNTUGESTURES_EXPORT TouchRegistry : public QObject
91+{
92+ Q_OBJECT
93+public:
94+ TouchRegistry(QObject *parent = nullptr);
95+ // Useful for tests, where you should feed a fake timer
96+ TouchRegistry(QObject *parent, UbuntuGestures::AbstractTimerFactory *timerFactory);
97+
98+ virtual ~TouchRegistry();
99+
100+ // Returns a pointer to the application's TouchRegistry instance.
101+ // If no instance has been allocated, null is returned.
102+ static TouchRegistry *instance() { return m_instance; }
103+
104+ void update(const QTouchEvent *event);
105+
106+ // Calls update() if the given event is a QTouchEvent
107+ bool eventFilter(QObject *watched, QEvent *event) override;
108+
109+ // An item that might later request ownership over the given touch point.
110+ // He will be kept informed about that touch point through UnownedTouchEvents
111+ // All candidates must eventually decide whether they want to own the touch point
112+ // or not. That decision is informed through requestTouchOwnership() or
113+ // removeCandidateOwnerForTouch()
114+ void addCandidateOwnerForTouch(int id, QQuickItem *candidate);
115+
116+ // The same as rejecting ownership of a touch
117+ void removeCandidateOwnerForTouch(int id, QQuickItem *candidate);
118+
119+ // The candidate object wants to be the owner of the touch with the given id.
120+ // If he's currently the oldest/top-most candidate, he will get an ownership
121+ // event immediately. If not, he will get ownership if (or once) he becomes the
122+ // top-most candidate.
123+ void requestTouchOwnership(int id, QQuickItem *candidate);
124+
125+ // An item that has no interest (effective or potential) in owning a touch point
126+ // but would nonetheless like to be kept up-to-date on its state.
127+ void addTouchWatcher(int touchId, QQuickItem *watcherItem);
128+
129+private Q_SLOTS:
130+ void rejectCandidateOwnerForTouch(int id, QQuickItem *candidate);
131+
132+private:
133+ class CandidateInfo {
134+ public:
135+ bool undecided;
136+ // TODO: Prune candidates that become null and resolve ownership accordingly.
137+ QPointer<QQuickItem> item;
138+ QPointer<UbuntuGestures::CandidateInactivityTimer> inactivityTimer;
139+ };
140+
141+ class TouchInfo {
142+ public:
143+ TouchInfo() : id(-1) {}
144+ TouchInfo(int id);
145+ bool isValid() const { return id >= 0; }
146+ void reset();
147+ void init(int id);
148+ int id;
149+ bool physicallyEnded;
150+ bool isOwned() const;
151+ bool ended() const;
152+ void notifyCandidatesOfOwnershipResolution();
153+
154+ // TODO optimize storage (s/QList/Pool)
155+ QList<CandidateInfo> candidates;
156+ QList<QPointer<QQuickItem>> watchers;
157+ };
158+
159+ Pool<TouchInfo>::Iterator findTouchInfo(int id);
160+
161+ void deliverTouchUpdatesToUndecidedCandidatesAndWatchers(const QTouchEvent *event);
162+
163+ static void translateTouchPointFromScreenToWindowCoords(QTouchEvent::TouchPoint &touchPoint);
164+
165+ static void dispatchPointsToItem(const QTouchEvent *event, const QList<int> &touchIds,
166+ QQuickItem *item);
167+ void freeEndedTouchInfos();
168+
169+ Pool<TouchInfo> m_touchInfoPool;
170+
171+ // the singleton instance
172+ static TouchRegistry *m_instance;
173+
174+ bool m_inDispatchLoop;
175+
176+ UbuntuGestures::AbstractTimerFactory *m_timerFactory;
177+
178+ friend class tst_TouchRegistry;
179+ friend class tst_DirectionalDragArea;
180+};
181+
182+#endif // UNITY_TOUCHREGISTRY_H
plugins/Ubuntu/Gestures/UbuntuGesturesGlobal.h → libs/UbuntuGestures/UbuntuGesturesGlobal.hrenamed+1−1
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright (C) 2013 Canonical, Ltd.
2+ * Copyright (C) 2014 Canonical, Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
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
5 * 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 @@
21#ifndef VELOCITY_CALCULATOR_H
21#ifndef VELOCITY_CALCULATOR_H
22#define VELOCITY_CALCULATOR_H
22#define VELOCITY_CALCULATOR_H
23
23
24−#include "UbuntuGesturesGlobal.h"
24+#include "UbuntuGesturesQmlGlobal.h"
25#include <stdint.h>
25#include <stdint.h>
26#include <QtCore/QObject>
26#include <QtCore/QObject>
27#include "TimeSource.h"
27#include "TimeSource.h"
@@ -49,7 +49,7 @@
49 }
49 }
50 }
50 }
51 */
51 */
52−class UBUNTUGESTURES_EXPORT AxisVelocityCalculator : public QObject
52+class UBUNTUGESTURESQML_EXPORT AxisVelocityCalculator : public QObject
53{
53{
54 Q_OBJECT
54 Q_OBJECT
55
55
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
2 plugin.cpp
5 plugin.cpp
3 AxisVelocityCalculator.cpp
6 AxisVelocityCalculator.cpp
4 Direction.cpp
7 Direction.cpp
5 DirectionalDragArea.cpp
8 DirectionalDragArea.cpp
6 PressedOutsideNotifier.cpp
9 PressedOutsideNotifier.cpp
7 TimeSource.cpp
10 TimeSource.cpp
11+ TouchGate.cpp
8)
12)
9
13
10−add_definitions(-DUBUNTUGESTURES_LIBRARY)
14+add_definitions(-DUBUNTUGESTURESQML_LIBRARY)
11
15
12−add_library(UbuntuGestureQml MODULE ${UbuntuGestureQml_SOURCES})
16+add_library(UbuntuGesturesQml MODULE ${UbuntuGesturesQml_SOURCES})
17+target_link_libraries(UbuntuGesturesQml UbuntuGestures)
13
18
14−qt5_use_modules(UbuntuGestureQml Core Quick)
19+qt5_use_modules(UbuntuGesturesQml Core Quick)
15
20
16# So that Foo.cpp can #include "Foo.moc"
21# So that Foo.cpp can #include "Foo.moc"
17include_directories(${CMAKE_CURRENT_BINARY_DIR})
22include_directories(${CMAKE_CURRENT_BINARY_DIR})
18
23
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+
19−add_unity8_plugin(Ubuntu.Gestures 0.1 Ubuntu/Gestures TARGETS UbuntuGestureQml)
38+add_unity8_plugin(Ubuntu.Gestures 0.1 Ubuntu/Gestures TARGETS UbuntuGesturesQml)
plugins/Ubuntu/Gestures/Direction.h+2−2
@@ -17,14 +17,14 @@
17#ifndef DIRECTION_H
17#ifndef DIRECTION_H
18#define DIRECTION_H
18#define DIRECTION_H
19
19
20−#include "UbuntuGesturesGlobal.h"
20+#include "UbuntuGesturesQmlGlobal.h"
21#include <QObject>
21#include <QObject>
22
22
23/*
23/*
24 A Direction enum wrapper so that we can do things like "direction: Direction.Righwards"
24 A Direction enum wrapper so that we can do things like "direction: Direction.Righwards"
25 from QML.
25 from QML.
26 */
26 */
27−class UBUNTUGESTURES_EXPORT Direction : public QObject {
27+class UBUNTUGESTURESQML_EXPORT Direction : public QObject {
28 Q_OBJECT
28 Q_OBJECT
29 Q_ENUMS(Type)
29 Q_ENUMS(Type)
30
30
plugins/Ubuntu/Gestures/DirectionalDragArea.cpp+317−191
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright (C) 2013 Canonical, Ltd.
2+ * Copyright (C) 2013-2014 Canonical, Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
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
5 * it under the terms of the GNU General Public License as published by
@@ -14,64 +14,32 @@
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 */
15 */
16
16
17+#define ACTIVETOUCHESINFO_DEBUG 0
18+#define DIRECTIONALDRAGAREA_DEBUG 0
19+
17#include "DirectionalDragArea.h"
20#include "DirectionalDragArea.h"
18
21
22+#include <QQuickWindow>
19#include <QtCore/qmath.h>
23#include <QtCore/qmath.h>
20−#include <QtCore/QTimer>
21#include <QDebug>
24#include <QDebug>
22
25
23−using namespace UbuntuGestures;
26+#pragma GCC diagnostic push
27+#pragma GCC diagnostic ignored "-pedantic"
28+#include <private/qquickwindow_p.h>
29+#pragma GCC diagnostic pop
24
30
25−#define DIRECTIONALDRAGAREA_DEBUG 0
31+// local
32+#include "TouchOwnershipEvent.h"
33+#include "TouchRegistry.h"
34+#include "UnownedTouchEvent.h"
35+
36+using namespace UbuntuGestures;
26
37
27#if DIRECTIONALDRAGAREA_DEBUG
38#if DIRECTIONALDRAGAREA_DEBUG
28#define DDA_DEBUG(msg) qDebug("[DDA] " msg)
39#define DDA_DEBUG(msg) qDebug("[DDA] " msg)
29−namespace {
40+#include "DebugHelpers.h"
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−}
74
41
42+namespace {
75const char *statusToString(DirectionalDragArea::Status status)
43const char *statusToString(DirectionalDragArea::Status status)
76{
44{
77 if (status == DirectionalDragArea::WaitingForTouch) {
45 if (status == DirectionalDragArea::WaitingForTouch) {
@@ -88,23 +56,6 @@ const char *statusToString(DirectionalDragArea::Status status)
88#define DDA_DEBUG(msg) do{}while(0)
56#define DDA_DEBUG(msg) do{}while(0)
89#endif // DIRECTIONALDRAGAREA_DEBUG
57#endif // DIRECTIONALDRAGAREA_DEBUG
90
58
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−};
108
59
109DirectionalDragArea::DirectionalDragArea(QQuickItem *parent)
60DirectionalDragArea::DirectionalDragArea(QQuickItem *parent)
110 : QQuickItem(parent)
61 : QQuickItem(parent)
@@ -126,12 +77,14 @@ DirectionalDragArea::DirectionalDragArea(QQuickItem *parent)
126 , m_timeSource(new RealTimeSource)
77 , m_timeSource(new RealTimeSource)
127 , m_activeTouches(m_timeSource)
78 , m_activeTouches(m_timeSource)
128{
79{
129− setRecognitionTimer(new RecognitionTimer(this));
80+ setRecognitionTimer(new Timer(this));
130 m_recognitionTimer->setInterval(60);
81 m_recognitionTimer->setInterval(60);
82+ m_recognitionTimer->setSingleShot(false);
131
83
132 m_velocityCalculator = new AxisVelocityCalculator(this);
84 m_velocityCalculator = new AxisVelocityCalculator(this);
133
85
134− connect(this, &QQuickItem::enabledChanged, this, &DirectionalDragArea::onEnabledChanged);
86+ connect(this, &QQuickItem::enabledChanged, this, &DirectionalDragArea::giveUpIfDisabledOrInvisible);
87+ connect(this, &QQuickItem::visibleChanged, this, &DirectionalDragArea::giveUpIfDisabledOrInvisible);
135}
88}
136
89
137Direction::Type DirectionalDragArea::direction() const
90Direction::Type DirectionalDragArea::direction() const
@@ -214,6 +167,7 @@ void DirectionalDragArea::setRecognitionTimer(UbuntuGestures::AbstractTimer *tim
214{
167{
215 int interval = 0;
168 int interval = 0;
216 bool timerWasRunning = false;
169 bool timerWasRunning = false;
170+ bool wasSingleShot = false;
217
171
218 // can be null when called from the constructor
172 // can be null when called from the constructor
219 if (m_recognitionTimer) {
173 if (m_recognitionTimer) {
@@ -226,6 +180,7 @@ void DirectionalDragArea::setRecognitionTimer(UbuntuGestures::AbstractTimer *tim
226
180
227 m_recognitionTimer = timer;
181 m_recognitionTimer = timer;
228 timer->setInterval(interval);
182 timer->setInterval(interval);
183+ timer->setSingleShot(wasSingleShot);
229 connect(timer, &UbuntuGestures::AbstractTimer::timeout,
184 connect(timer, &UbuntuGestures::AbstractTimer::timeout,
230 this, &DirectionalDragArea::checkSpeed);
185 this, &DirectionalDragArea::checkSpeed);
231 if (timerWasRunning) {
186 if (timerWasRunning) {
@@ -280,8 +235,144 @@ qreal DirectionalDragArea::touchSceneY() const
280 return m_previousScenePos.y();
235 return m_previousScenePos.y();
281}
236}
282
237
238+bool DirectionalDragArea::event(QEvent *event)
239+{
240+ if (event->type() == TouchOwnershipEvent::touchOwnershipEventType()) {
241+ touchOwnershipEvent(static_cast<TouchOwnershipEvent *>(event));
242+ return true;
243+ } else if (event->type() == UnownedTouchEvent::unownedTouchEventType()) {
244+ unownedTouchEvent(static_cast<UnownedTouchEvent *>(event));
245+ return true;
246+ } else {
247+ return QQuickItem::event(event);
248+ }
249+}
250+
251+void DirectionalDragArea::touchOwnershipEvent(TouchOwnershipEvent *event)
252+{
253+ if (event->gained()) {
254+ QVector<int> ids;
255+ ids.append(event->touchId());
256+ DDA_DEBUG("grabbing touch");
257+ grabTouchPoints(ids);
258+
259+ // Work around for Qt bug. If we grab a touch that is being used for mouse pointer
260+ // emulation it will cause the emulation logic to go nuts.
261+ // Thus we have to also grab the mouse in this case.
262+ // TODO: Report bug to Qt
263+ if (window()) {
264+ QQuickWindowPrivate *windowPrivate = QQuickWindowPrivate::get(window());
265+ if (windowPrivate->touchMouseId == event->touchId() && window()->mouseGrabberItem()) {
266+ DDA_DEBUG("removing mouse grabber");
267+ window()->mouseGrabberItem()->ungrabMouse();
268+ }
269+ }
270+ } else {
271+ // We still wanna know when it ends for keeping the composition time window up-to-date
272+ TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
273+
274+ setStatus(WaitingForTouch);
275+ }
276+}
277+
278+void DirectionalDragArea::unownedTouchEvent(UnownedTouchEvent *unownedTouchEvent)
279+{
280+ QTouchEvent *event = unownedTouchEvent->touchEvent();
281+
282+ Q_ASSERT(!event->touchPointStates().testFlag(Qt::TouchPointPressed));
283+
284+ #if DIRECTIONALDRAGAREA_DEBUG
285+ // TODO Consider using qCDebug() when available (Qt 5.2)
286+ qDebug() << "[DDA] Unowned" << m_timeSource->msecsSinceReference()
287+ << qPrintable(touchEventToString(event));
288+ #endif
289+
290+ switch (m_status) {
291+ case WaitingForTouch:
292+ // do nothing
293+ break;
294+ case Undecided:
295+ Q_ASSERT(isEnabled() && isVisible());
296+ unownedTouchEvent_undecided(unownedTouchEvent);
297+ break;
298+ default: // Recognized:
299+ // do nothing
300+ break;
301+ }
302+
303+ m_activeTouches.update(event);
304+}
305+
306+void DirectionalDragArea::unownedTouchEvent_undecided(UnownedTouchEvent *unownedTouchEvent)
307+{
308+ const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(unownedTouchEvent->touchEvent());
309+ if (!touchPoint) {
310+ qCritical() << "DirectionalDragArea[status=Undecided]: touch " << m_touchId
311+ << "missing from UnownedTouchEvent without first reaching state Qt::TouchPointReleased. "
312+ "Considering it as released.";
313+
314+ TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
315+ setStatus(WaitingForTouch);
316+ return;
317+ }
318+
319+ const QPointF &touchScenePos = touchPoint->scenePos();
320+
321+ if (touchPoint->state() == Qt::TouchPointReleased) {
322+ // touch has ended before recognition concluded
323+ DDA_DEBUG("Touch has ended before recognition concluded");
324+ TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
325+ emitSignalIfTapped();
326+ setStatus(WaitingForTouch);
327+ return;
328+ }
329+
330+ m_previousDampedScenePos.setX(m_dampedScenePos.x());
331+ m_previousDampedScenePos.setY(m_dampedScenePos.y());
332+ m_dampedScenePos.update(touchScenePos);
333+ updateVelocityCalculator(touchScenePos);
334+
335+ if (!pointInsideAllowedArea()) {
336+ DDA_DEBUG("Rejecting gesture because touch point is outside allowed area.");
337+ TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
338+ // We still wanna know when it ends for keeping the composition time window up-to-date
339+ TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
340+ setStatus(WaitingForTouch);
341+ return;
342+ }
343+
344+ if (!movingInRightDirection()) {
345+ DDA_DEBUG("Rejecting gesture because touch point is moving in the wrong direction.");
346+ TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
347+ // We still wanna know when it ends for keeping the composition time window up-to-date
348+ TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
349+ setStatus(WaitingForTouch);
350+ return;
351+ }
352+
353+ setPreviousPos(touchPoint->pos());
354+ setPreviousScenePos(touchScenePos);
355+
356+ if (isWithinTouchCompositionWindow()) {
357+ // There's still time for some new touch to appear and ruin our party as it would be combined
358+ // with our m_touchId one and therefore deny the possibility of a single-finger gesture.
359+ DDA_DEBUG("Sill within composition window. Let's wait more.");
360+ return;
361+ }
362+
363+ if (movedFarEnough(touchScenePos)) {
364+ TouchRegistry::instance()->requestTouchOwnership(m_touchId, this);
365+ setStatus(Recognized);
366+ } else {
367+ DDA_DEBUG("Didn't move far enough yet. Let's wait more.");
368+ }
369+}
370+
283void DirectionalDragArea::touchEvent(QTouchEvent *event)
371void DirectionalDragArea::touchEvent(QTouchEvent *event)
284{
372{
373+ // TODO: Consider when more than one touch starts in the same event (although it's not possible
374+ // with Mir's android-input). Have to track them all. Consider it a plus/bonus.
375+
285 #if DIRECTIONALDRAGAREA_DEBUG
376 #if DIRECTIONALDRAGAREA_DEBUG
286 // TODO Consider using qCDebug() when available (Qt 5.2)
377 // TODO Consider using qCDebug() when available (Qt 5.2)
287 qDebug() << "[DDA]" << m_timeSource->msecsSinceReference()
378 qDebug() << "[DDA]" << m_timeSource->msecsSinceReference()
@@ -310,29 +401,34 @@ void DirectionalDragArea::touchEvent(QTouchEvent *event)
310
401
311void DirectionalDragArea::touchEvent_absent(QTouchEvent *event)
402void DirectionalDragArea::touchEvent_absent(QTouchEvent *event)
312{
403{
404+ // TODO: accept/reject is for the whole event, not per touch id. See how that affects us.
405+
313 if (!event->touchPointStates().testFlag(Qt::TouchPointPressed)) {
406 if (!event->touchPointStates().testFlag(Qt::TouchPointPressed)) {
314 // Nothing to see here. No touch starting in this event.
407 // Nothing to see here. No touch starting in this event.
315 return;
408 return;
316 }
409 }
317
410
411+ // to be proven wrong, if that's the case
412+ bool allGood = true;
413+
318 if (isWithinTouchCompositionWindow()) {
414 if (isWithinTouchCompositionWindow()) {
319 // too close to the last touch start. So we consider them as starting roughly at the same time.
415 // too close to the last touch start. So we consider them as starting roughly at the same time.
320 // Can't be a single-touch gesture.
416 // Can't be a single-touch gesture.
321 #if DIRECTIONALDRAGAREA_DEBUG
417 #if DIRECTIONALDRAGAREA_DEBUG
322 qDebug("[DDA] A new touch point came in but we're still within time composition window. Ignoring it.");
418 qDebug("[DDA] A new touch point came in but we're still within time composition window. Ignoring it.");
323 #endif
419 #endif
324− return;
420+ allGood = false;
325 }
421 }
326
422
327 const QList<QTouchEvent::TouchPoint> &touchPoints = event->touchPoints();
423 const QList<QTouchEvent::TouchPoint> &touchPoints = event->touchPoints();
328
424
329 const QTouchEvent::TouchPoint *newTouchPoint = nullptr;
425 const QTouchEvent::TouchPoint *newTouchPoint = nullptr;
330− for (int i = 0; i < touchPoints.count(); ++i) {
426+ for (int i = 0; i < touchPoints.count() && allGood; ++i) {
331 const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
427 const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
332 if (touchPoint.state() == Qt::TouchPointPressed) {
428 if (touchPoint.state() == Qt::TouchPointPressed) {
333 if (newTouchPoint) {
429 if (newTouchPoint) {
334 // more than one touch starting in this QTouchEvent. Can't be a single-touch gesture
430 // more than one touch starting in this QTouchEvent. Can't be a single-touch gesture
335− return;
431+ allGood = false;
336 } else {
432 } else {
337 // that's our candidate
433 // that's our candidate
338 m_touchId = touchPoint.id();
434 m_touchId = touchPoint.id();
@@ -341,85 +437,61 @@ void DirectionalDragArea::touchEvent_absent(QTouchEvent *event)
341 }
437 }
342 }
438 }
343
439
440+ if (allGood) {
344− Q_ASSERT(newTouchPoint);
441+ Q_ASSERT(newTouchPoint);
345
442
346− // If we have made this far, we are good to go to the next status.
347−
348− m_startPos = newTouchPoint->pos();
443+ m_startPos = newTouchPoint->pos();
349− m_startScenePos = newTouchPoint->scenePos();
444+ m_startScenePos = newTouchPoint->scenePos();
350− m_touchId = newTouchPoint->id();
445+ m_touchId = newTouchPoint->id();
351− m_dampedScenePos.reset(m_startScenePos);
446+ m_dampedScenePos.reset(m_startScenePos);
352− m_velocityCalculator->setTrackedPosition(0.);
447+ m_velocityCalculator->setTrackedPosition(0.);
353− m_velocityCalculator->reset();
448+ m_velocityCalculator->reset();
354− m_numSamplesOnLastSpeedCheck = 0;
449+ m_numSamplesOnLastSpeedCheck = 0;
355− m_silenceTime = 0;
450+ m_silenceTime = 0;
356− setPreviousPos(m_startPos);
451+ setPreviousPos(m_startPos);
357− setPreviousScenePos(m_startScenePos);
452+ setPreviousScenePos(m_startScenePos);
358− updateSceneDirectionVector();
453+ updateSceneDirectionVector();
359−
454+
455+ if (recognitionIsDisabled()) {
456+ // Behave like a dumb TouchArea
457+ TouchRegistry::instance()->requestTouchOwnership(m_touchId, this);
458+ setStatus(Recognized);
459+ event->accept();
460+ } else {
461+ // just monitor the touch points for now.
462+ TouchRegistry::instance()->addCandidateOwnerForTouch(m_touchId, this);
463+
360− setStatus(Undecided);
464+ setStatus(Undecided);
465+ // Let the item below have it. We will monitor it and grab it later if a gesture
466+ // gets recognized.
467+ event->ignore();
468+ }
469+ } else {
470+ watchPressedTouchPoints(touchPoints);
471+ event->ignore();
472+ }
361}
473}
362
474
363void DirectionalDragArea::touchEvent_undecided(QTouchEvent *event)
475void DirectionalDragArea::touchEvent_undecided(QTouchEvent *event)
364{
476{
477+ Q_ASSERT(event->type() == QEvent::TouchBegin);
365− const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event);
478+ Q_ASSERT(fetchTargetTouchPoint(event) == nullptr);
366
479
367− if (!touchPoint) {
480+ // We're not interested in new touch points. We already have our candidate (m_touchId).
368− qCritical() << "DirectionalDragArea[status=Undecided]: touch " << m_touchId
481+ // But we do want to know when those new touches end for keeping the composition time
369− << "missing from QTouchEvent without first reaching state Qt::TouchPointReleased. "
482+ // window up-to-date
370− "Considering it as released.";
483+ event->ignore();
371− setStatus(WaitingForTouch);
484+ watchPressedTouchPoints(event->touchPoints());
372− return;
373− }
374−
375− const QPointF &touchScenePos = touchPoint->scenePos();
376−
377− if (touchPoint->state() == Qt::TouchPointReleased) {
378− // touch has ended before recognition concluded
379− DDA_DEBUG("Touch has ended before recognition concluded");
380− setStatus(WaitingForTouch);
381− Q_EMIT tapped();
382− return;
383− }
384
485
385 if (event->touchPointStates().testFlag(Qt::TouchPointPressed) && isWithinTouchCompositionWindow()) {
486 if (event->touchPointStates().testFlag(Qt::TouchPointPressed) && isWithinTouchCompositionWindow()) {
386 // multi-finger drags are not accepted
487 // multi-finger drags are not accepted
387 DDA_DEBUG("Multi-finger drags are not accepted");
488 DDA_DEBUG("Multi-finger drags are not accepted");
388− setStatus(WaitingForTouch);
389− return;
390− }
391
489
392− m_previousDampedScenePos.setX(m_dampedScenePos.x());
490+ TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
393− m_previousDampedScenePos.setY(m_dampedScenePos.y());
491+ // We still wanna know when it ends for keeping the composition time window up-to-date
394− m_dampedScenePos.update(touchScenePos);
492+ TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
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− }
402
493
403− if (!movingInRightDirection()) {
404− DDA_DEBUG("Rejecting gesture becauuse touch point is moving in the wrong direction.");
405 setStatus(WaitingForTouch);
494 setStatus(WaitingForTouch);
406− return;
407− }
408−
409− setPreviousPos(touchPoint->pos());
410− setPreviousScenePos(touchScenePos);
411−
412− if (isWithinTouchCompositionWindow()) {
413− // There's still time for some new touch to appear and ruin our party as it would be combined
414− // with our m_touchId one and therefore deny the possibility of a single-finger gesture.
415− DDA_DEBUG("Sill within composition window. Let's wait more.");
416− return;
417− }
418−
419− if (movedFarEnough(touchScenePos)) {
420− setStatus(Recognized);
421− } else {
422− DDA_DEBUG("Didn't move far enough yet. Let's wait more.");
423 }
495 }
424}
496}
425
497
@@ -437,11 +509,35 @@ void DirectionalDragArea::touchEvent_recognized(QTouchEvent *event)
437 setPreviousScenePos(touchPoint->scenePos());
509 setPreviousScenePos(touchPoint->scenePos());
438
510
439 if (touchPoint->state() == Qt::TouchPointReleased) {
511 if (touchPoint->state() == Qt::TouchPointReleased) {
512+ emitSignalIfTapped();
440 setStatus(WaitingForTouch);
513 setStatus(WaitingForTouch);
441 }
514 }
442 }
515 }
443}
516}
444
517
518+void DirectionalDragArea::watchPressedTouchPoints(const QList<QTouchEvent::TouchPoint> &touchPoints)
519+{
520+ for (int i = 0; i < touchPoints.count(); ++i) {
521+ const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
522+ if (touchPoint.state() == Qt::TouchPointPressed) {
523+ TouchRegistry::instance()->addTouchWatcher(touchPoint.id(), this);
524+ }
525+ }
526+}
527+
528+bool DirectionalDragArea::recognitionIsDisabled() const
529+{
530+ return distanceThreshold() <= 0 && compositionTime() <= 0;
531+}
532+
533+void DirectionalDragArea::emitSignalIfTapped()
534+{
535+ qint64 touchDuration = m_timeSource->msecsSinceReference() - m_activeTouches.touchStartTime(m_touchId);
536+ if (touchDuration <= maxTapDuration()) {
537+ Q_EMIT tapped();
538+ }
539+}
540+
445const QTouchEvent::TouchPoint *DirectionalDragArea::fetchTargetTouchPoint(QTouchEvent *event)
541const QTouchEvent::TouchPoint *DirectionalDragArea::fetchTargetTouchPoint(QTouchEvent *event)
446{
542{
447 const QList<QTouchEvent::TouchPoint> &touchPoints = event->touchPoints();
543 const QList<QTouchEvent::TouchPoint> &touchPoints = event->touchPoints();
@@ -509,12 +605,16 @@ bool DirectionalDragArea::movedFarEnough(const QPointF &point) const
509
605
510void DirectionalDragArea::checkSpeed()
606void DirectionalDragArea::checkSpeed()
511{
607{
608+ Q_ASSERT(m_status == Undecided);
609+
512 if (m_velocityCalculator->numSamples() >= AxisVelocityCalculator::MIN_SAMPLES_NEEDED) {
610 if (m_velocityCalculator->numSamples() >= AxisVelocityCalculator::MIN_SAMPLES_NEEDED) {
513 qreal speed = qFabs(m_velocityCalculator->calculate());
611 qreal speed = qFabs(m_velocityCalculator->calculate());
514 qreal minSpeedMsecs = m_minSpeed / 1000.0;
612 qreal minSpeedMsecs = m_minSpeed / 1000.0;
515
613
516 if (speed < minSpeedMsecs) {
614 if (speed < minSpeedMsecs) {
517 DDA_DEBUG("Rejecting gesture because it's below minimum speed.");
615 DDA_DEBUG("Rejecting gesture because it's below minimum speed.");
616+ TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
617+ TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
518 setStatus(WaitingForTouch);
618 setStatus(WaitingForTouch);
519 }
619 }
520 }
620 }
@@ -523,7 +623,9 @@ void DirectionalDragArea::checkSpeed()
523 m_silenceTime += m_recognitionTimer->interval();
623 m_silenceTime += m_recognitionTimer->interval();
524
624
525 if (m_silenceTime > m_maxSilenceTime) {
625 if (m_silenceTime > m_maxSilenceTime) {
526− DDA_DEBUG("Rejecting gesture because it's silence time has been exceeded.");
626+ DDA_DEBUG("Rejecting gesture because its silence time has been exceeded.");
627+ TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
628+ TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
527 setStatus(WaitingForTouch);
629 setStatus(WaitingForTouch);
528 }
630 }
529 } else {
631 } else {
@@ -532,10 +634,19 @@ void DirectionalDragArea::checkSpeed()
532 m_numSamplesOnLastSpeedCheck = m_velocityCalculator->numSamples();
634 m_numSamplesOnLastSpeedCheck = m_velocityCalculator->numSamples();
533}
635}
534
636
535−void DirectionalDragArea::onEnabledChanged()
637+void DirectionalDragArea::giveUpIfDisabledOrInvisible()
536{
638{
639+ if (!isEnabled() || !isVisible()) {
640+ if (m_status == Undecided) {
641+ TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
642+ // We still wanna know when it ends for keeping the composition time window up-to-date
643+ TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
644+ }
645+
537− if (!isEnabled() && m_status != WaitingForTouch) {
646+ if (m_status != WaitingForTouch) {
647+ DDA_DEBUG("Resetting status because got disabled or made invisible");
538− setStatus(WaitingForTouch);
648+ setStatus(WaitingForTouch);
649+ }
539 }
650 }
540}
651}
541
652
@@ -631,7 +742,9 @@ void DirectionalDragArea::updateVelocityCalculator(const QPointF &scenePos)
631
742
632bool DirectionalDragArea::isWithinTouchCompositionWindow()
743bool DirectionalDragArea::isWithinTouchCompositionWindow()
633{
744{
745+ return
746+ compositionTime() > 0 &&
634− return !m_activeTouches.isEmpty() &&
747+ !m_activeTouches.isEmpty() &&
635 m_timeSource->msecsSinceReference() <=
748 m_timeSource->msecsSinceReference() <=
636 m_activeTouches.mostRecentStartTime() + (qint64)compositionTime();
749 m_activeTouches.mostRecentStartTime() + (qint64)compositionTime();
637}
750}
@@ -639,18 +752,17 @@ bool DirectionalDragArea::isWithinTouchCompositionWindow()
639//************************** ActiveTouchesInfo **************************
752//************************** ActiveTouchesInfo **************************
640
753
641DirectionalDragArea::ActiveTouchesInfo::ActiveTouchesInfo(const SharedTimeSource &timeSource)
754DirectionalDragArea::ActiveTouchesInfo::ActiveTouchesInfo(const SharedTimeSource &timeSource)
642− : m_timeSource(timeSource), m_lastUsedIndex(-1)
755+ : m_timeSource(timeSource)
643{
756{
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);
648}
757}
649
758
650void DirectionalDragArea::ActiveTouchesInfo::update(QTouchEvent *event)
759void DirectionalDragArea::ActiveTouchesInfo::update(QTouchEvent *event)
651{
760{
652 if (!(event->touchPointStates() & (Qt::TouchPointPressed | Qt::TouchPointReleased))) {
761 if (!(event->touchPointStates() & (Qt::TouchPointPressed | Qt::TouchPointReleased))) {
653 // nothing to update
762 // nothing to update
763+ #if ACTIVETOUCHESINFO_DEBUG
764+ qDebug("[DDA::ActiveTouchesInfo] Nothing to Update");
765+ #endif
654 return;
766 return;
655 }
767 }
656
768
@@ -658,74 +770,88 @@ void DirectionalDragArea::ActiveTouchesInfo::update(QTouchEvent *event)
658 for (int i = 0; i < touchPoints.count(); ++i) {
770 for (int i = 0; i < touchPoints.count(); ++i) {
659 const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
771 const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
660 if (touchPoint.state() == Qt::TouchPointPressed) {
772 if (touchPoint.state() == Qt::TouchPointPressed) {
661− addTouchPoint(touchPoint);
773+ addTouchPoint(touchPoint.id());
662 } else if (touchPoint.state() == Qt::TouchPointReleased) {
774 } else if (touchPoint.state() == Qt::TouchPointReleased) {
663− removeTouchPoint(touchPoint);
775+ removeTouchPoint(touchPoint.id());
664 }
776 }
665 }
777 }
666}
778}
667
779
780+#if ACTIVETOUCHESINFO_DEBUG
668−void DirectionalDragArea::ActiveTouchesInfo::addTouchPoint(const QTouchEvent::TouchPoint &touchPoint)
781+QString DirectionalDragArea::ActiveTouchesInfo::toString()
669{
782{
670− ActiveTouchInfo &activeTouchInfo = getEmptySlot();
783+ QString string = "(";
671− activeTouchInfo.id = touchPoint.id();
784+
672− activeTouchInfo.startTime = m_timeSource->msecsSinceReference();
785+ {
786+ QTextStream stream(&string);
787+ m_touchInfoPool.forEach([&](Pool<ActiveTouchInfo>::Iterator &touchInfo) {
788+ stream << "(id=" << touchInfo->id << ",startTime=" << touchInfo->startTime << ")";
789+ return true;
790+ });
791+ }
792+
793+ string.append(")");
794+
795+ return string;
673}
796}
797+#endif // ACTIVETOUCHESINFO_DEBUG
674
798
675−void DirectionalDragArea::ActiveTouchesInfo::removeTouchPoint(const QTouchEvent::TouchPoint &touchPoint)
799+void DirectionalDragArea::ActiveTouchesInfo::addTouchPoint(int touchId)
676{
800{
677− for (int i = 0; i <= m_lastUsedIndex; ++i) {
801+ ActiveTouchInfo &activeTouchInfo = m_touchInfoPool.getEmptySlot();
678− if (touchPoint.id() == m_vector.at(i).id) {
802+ activeTouchInfo.id = touchId;
679− freeSlot(i);
803+ activeTouchInfo.startTime = m_timeSource->msecsSinceReference();
680− return;
804+
681− }
805+ #if ACTIVETOUCHESINFO_DEBUG
682− }
806+ qDebug() << "[DDA::ActiveTouchesInfo]" << qPrintable(toString());
683− Q_ASSERT(false); // shouldn't reach this point
807+ #endif
684}
808}
685
809
686−DirectionalDragArea::ActiveTouchInfo &DirectionalDragArea::ActiveTouchesInfo::getEmptySlot()
810+qint64 DirectionalDragArea::ActiveTouchesInfo::touchStartTime(int touchId)
687{
811{
688− Q_ASSERT(m_lastUsedIndex < m_vector.size());
812+ qint64 result = -1;
689
813
690− // Look for an in-between vacancy first
814+ m_touchInfoPool.forEach([&](Pool<ActiveTouchInfo>::Iterator &touchInfo) {
691− for (int i = 0; i < m_lastUsedIndex; ++i) {
815+ if (touchId == touchInfo->id) {
692− ActiveTouchInfo &activeTouchInfo = m_vector[i];
816+ result = touchInfo->startTime;
693− if (!activeTouchInfo.isValid()) {
817+ return false;
694− return activeTouchInfo;
818+ } else {
819+ return true;
695 }
820 }
696− }
821+ });
697−
698− ++m_lastUsedIndex;
699− if (m_lastUsedIndex >= m_vector.size()) {
700− m_vector.resize(m_lastUsedIndex + 1);
701− }
702
822
703− return m_vector[m_lastUsedIndex];
823+ Q_ASSERT(result != -1);
824+ return result;
704}
825}
705
826
706−void DirectionalDragArea::ActiveTouchesInfo::freeSlot(int index)
827+void DirectionalDragArea::ActiveTouchesInfo::removeTouchPoint(int touchId)
707{
828{
708− m_vector[index].reset();
829+ m_touchInfoPool.forEach([&](Pool<ActiveTouchInfo>::Iterator &touchInfo) {
709− if (index == m_lastUsedIndex) {
830+ if (touchId == touchInfo->id) {
710− do {
831+ m_touchInfoPool.freeSlot(touchInfo);
711− --m_lastUsedIndex;
832+ return false;
712− } while (m_lastUsedIndex >= 0 && !m_vector.at(m_lastUsedIndex).isValid());
833+ } else {
713− }
834+ return true;
835+ }
836+ });
837+
838+ #if ACTIVETOUCHESINFO_DEBUG
839+ qDebug() << "[DDA::ActiveTouchesInfo]" << qPrintable(toString());
840+ #endif
714}
841}
715
842
716qint64 DirectionalDragArea::ActiveTouchesInfo::mostRecentStartTime()
843qint64 DirectionalDragArea::ActiveTouchesInfo::mostRecentStartTime()
717{
844{
718− Q_ASSERT(m_lastUsedIndex >= 0);
845+ Q_ASSERT(!m_touchInfoPool.isEmpty());
846+
847+ qint64 highestStartTime = -1;
719
848
720− qint64 highestStartTime = m_vector.at(0).startTime;
721− int i = 1;
722− do {
723− const ActiveTouchInfo &activeTouchInfo = m_vector.at(i);
849+ m_touchInfoPool.forEach([&](Pool<ActiveTouchInfo>::Iterator &activeTouchInfo) {
724− if (activeTouchInfo.isValid() && activeTouchInfo.startTime > highestStartTime) {
850+ if (activeTouchInfo->startTime > highestStartTime) {
725− highestStartTime = activeTouchInfo.startTime;
851+ highestStartTime = activeTouchInfo->startTime;
726 }
852 }
727− ++i;
853+ return true;
728− } while (i < m_lastUsedIndex);
854+ });
729
855
730 return highestStartTime;
856 return highestStartTime;
731}
857}
plugins/Ubuntu/Gestures/DirectionalDragArea.h+37−29
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright (C) 2013 Canonical, Ltd.
2+ * Copyright (C) 2013,2014 Canonical, Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
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
5 * it under the terms of the GNU General Public License as published by
@@ -19,27 +19,16 @@
19
19
20#include <QtQuick/QQuickItem>
20#include <QtQuick/QQuickItem>
21#include "AxisVelocityCalculator.h"
21#include "AxisVelocityCalculator.h"
22−#include "UbuntuGesturesGlobal.h"
22+#include "UbuntuGesturesQmlGlobal.h"
23#include "Damper.h"
23#include "Damper.h"
24#include "Direction.h"
24#include "Direction.h"
25
25
26−namespace UbuntuGestures {
26+// lib UbuntuGestures
27−/* Defines an interface for a Timer. */
27+#include <Pool.h>
28−class UBUNTUGESTURES_EXPORT AbstractTimer : public QObject {
28+#include <Timer.h>
29− Q_OBJECT
29+
30−public:
30+class TouchOwnershipEvent;
31− AbstractTimer(QObject *parent) : QObject(parent), m_isRunning(false) {}
31+class UnownedTouchEvent;
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−}
43
32
44/*
33/*
45 An area that detects axis-aligned single-finger drag gestures
34 An area that detects axis-aligned single-finger drag gestures
@@ -50,7 +39,7 @@ private:
50
39
51 See doc/DirectionalDragArea.svg
40 See doc/DirectionalDragArea.svg
52 */
41 */
53−class UBUNTUGESTURES_EXPORT DirectionalDragArea : public QQuickItem {
42+class UBUNTUGESTURESQML_EXPORT DirectionalDragArea : public QQuickItem {
54 Q_OBJECT
43 Q_OBJECT
55
44
56 // The direction in which the gesture should move in order to be recognized.
45 // The direction in which the gesture should move in order to be recognized.
@@ -196,6 +185,12 @@ public:
196 // Useful for testing, where a fake time source can be supplied
185 // Useful for testing, where a fake time source can be supplied
197 void setTimeSource(const UbuntuGestures::SharedTimeSource &timeSource);
186 void setTimeSource(const UbuntuGestures::SharedTimeSource &timeSource);
198
187
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+
199Q_SIGNALS:
194Q_SIGNALS:
200 void directionChanged(Direction::Type direction);
195 void directionChanged(Direction::Type direction);
201 void statusChanged(Status value);
196 void statusChanged(Status value);
@@ -212,6 +207,10 @@ Q_SIGNALS:
212 void touchYChanged(qreal value);
207 void touchYChanged(qreal value);
213 void touchSceneXChanged(qreal value);
208 void touchSceneXChanged(qreal value);
214 void touchSceneYChanged(qreal value);
209 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()
215 void tapped();
214 void tapped();
216
215
217protected:
216protected:
@@ -219,7 +218,7 @@ protected:
219
218
220private Q_SLOTS:
219private Q_SLOTS:
221 void checkSpeed();
220 void checkSpeed();
222− void onEnabledChanged();
221+ void giveUpIfDisabledOrInvisible();
223
222
224private:
223private:
225 void touchEvent_absent(QTouchEvent *event);
224 void touchEvent_absent(QTouchEvent *event);
@@ -238,6 +237,12 @@ private:
238 // returns the scalar projection between the given vector (in scene coordinates)
237 // returns the scalar projection between the given vector (in scene coordinates)
239 // and m_sceneDirectionVector
238 // and m_sceneDirectionVector
240 qreal projectOntoDirectionVector(const QPointF &sceneVector) const;
239 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();
241
246
242 Status m_status;
247 Status m_status;
243
248
@@ -283,18 +288,21 @@ private:
283 public:
288 public:
284 ActiveTouchesInfo(const UbuntuGestures::SharedTimeSource &timeSource);
289 ActiveTouchesInfo(const UbuntuGestures::SharedTimeSource &timeSource);
285 void update(QTouchEvent *event);
290 void update(QTouchEvent *event);
286− ActiveTouchInfo &touchInfo(int id);
291+ qint64 touchStartTime(int id);
292+ bool isEmpty() const { return m_touchInfoPool.isEmpty(); }
287 qint64 mostRecentStartTime();
293 qint64 mostRecentStartTime();
288 UbuntuGestures::SharedTimeSource m_timeSource;
294 UbuntuGestures::SharedTimeSource m_timeSource;
289− bool isEmpty() const { return m_lastUsedIndex == -1; }
290 private:
295 private:
291− void addTouchPoint(const QTouchEvent::TouchPoint &touchPoint);
296+ void addTouchPoint(int touchId);
292− ActiveTouchInfo &getEmptySlot();
293− void freeSlot(int index);
294− void removeTouchPoint(const QTouchEvent::TouchPoint &touchPoint);
297+ void removeTouchPoint(int touchId);
298+ #if ACTIVETOUCHESINFO_DEBUG
299+ QString toString();
300+ #endif
301+
295− QVector<struct ActiveTouchInfo> m_vector;
302+ Pool<ActiveTouchInfo> m_touchInfoPool;
296− int m_lastUsedIndex;
297 } m_activeTouches;
303 } m_activeTouches;
304+
305+ friend class tst_DirectionalDragArea;
298};
306};
299
307
300#endif // DIRECTIONAL_DRAG_AREA_H
308#endif // DIRECTIONAL_DRAG_AREA_H
plugins/Ubuntu/Gestures/TimeSource.h+2−2
@@ -21,14 +21,14 @@
21#ifndef UBUNTUGESTURES_TIMESOURCE_H
21#ifndef UBUNTUGESTURES_TIMESOURCE_H
22#define UBUNTUGESTURES_TIMESOURCE_H
22#define UBUNTUGESTURES_TIMESOURCE_H
23
23
24−#include "UbuntuGesturesGlobal.h"
24+#include "UbuntuGesturesQmlGlobal.h"
25#include <QSharedPointer>
25#include <QSharedPointer>
26
26
27namespace UbuntuGestures {
27namespace UbuntuGestures {
28/*
28/*
29 Interface for a time source.
29 Interface for a time source.
30 */
30 */
31−class UBUNTUGESTURES_EXPORT TimeSource {
31+class UBUNTUGESTURESQML_EXPORT TimeSource {
32public:
32public:
33 virtual ~TimeSource() {}
33 virtual ~TimeSource() {}
34 /* Returns the current time in milliseconds since some reference time in the past. */
34 /* Returns the current time in milliseconds since some reference time in the past. */
plugins/Ubuntu/Gestures/TouchGate.cppnew file+347
@@ -0,0 +1,347 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "TouchGate.h"
18+
19+#include <QCoreApplication>
20+#include <QDebug>
21+
22+#include <TouchOwnershipEvent.h>
23+#include <TouchRegistry.h>
24+
25+#pragma GCC diagnostic push
26+#pragma GCC diagnostic ignored "-pedantic"
27+#include <private/qquickitem_p.h>
28+#pragma GCC diagnostic pop
29+
30+#if TOUCHGATE_DEBUG
31+#include <DebugHelpers.h>
32+#endif
33+
34+
35+bool TouchGate::event(QEvent *e)
36+{
37+ if (e->type() == TouchOwnershipEvent::touchOwnershipEventType()) {
38+ touchOwnershipEvent(static_cast<TouchOwnershipEvent *>(e));
39+ return true;
40+ } else {
41+ return QQuickItem::event(e);
42+ }
43+}
44+
45+void TouchGate::touchEvent(QTouchEvent *event)
46+{
47+ #if TOUCHGATE_DEBUG
48+ qDebug() << "[TouchGate] got touch event" << qPrintable(touchEventToString(event));
49+ #endif
50+ event->accept();
51+
52+ const QList<QTouchEvent::TouchPoint> &touchPoints = event->touchPoints();
53+ bool goodToGo = true;
54+ for (int i = 0; i < touchPoints.count(); ++i) {
55+ const QTouchEvent::TouchPoint &touchPoint = touchPoints[i];
56+
57+ if (touchPoint.state() == Qt::TouchPointPressed) {
58+ Q_ASSERT(!m_touchInfoMap.contains(touchPoint.id()));
59+ m_touchInfoMap[touchPoint.id()].ownership = OwnershipRequested;
60+ m_touchInfoMap[touchPoint.id()].ended = false;
61+ TouchRegistry::instance()->requestTouchOwnership(touchPoint.id(), this);
62+ }
63+
64+ goodToGo &= m_touchInfoMap.contains(touchPoint.id())
65+ && m_touchInfoMap[touchPoint.id()].ownership == OwnershipGranted;
66+
67+ if (touchPoint.state() == Qt::TouchPointReleased && m_touchInfoMap.contains(touchPoint.id())) {
68+ m_touchInfoMap[touchPoint.id()].ended = true;
69+ }
70+
71+ }
72+
73+ if (goodToGo) {
74+ if (m_storedEvents.isEmpty()) {
75+ // let it pass through
76+ dispatchTouchEventToTarget(event);
77+ } else {
78+ // Retain the event to ensure TouchGate dispatches them in order.
79+ // Otherwise the current event would come before the stored ones, which are older.
80+ #if TOUCHGATE_DEBUG
81+ qDebug("[TouchGate] Storing event because thouches %s are still pending ownership.",
82+ qPrintable(oldestPendingTouchIdsString()));
83+ #endif
84+ storeTouchEvent(event);
85+ }
86+ } else {
87+ // Retain events that have unowned touches
88+ storeTouchEvent(event);
89+ }
90+}
91+
92+void TouchGate::touchOwnershipEvent(TouchOwnershipEvent *event)
93+{
94+ // TODO: Optimization: batch those actions as TouchOwnershipEvents
95+ // might come one right after the other.
96+
97+ Q_ASSERT(m_touchInfoMap.contains(event->touchId()));
98+
99+ TouchInfo &touchInfo = m_touchInfoMap[event->touchId()];
100+
101+ if (event->gained()) {
102+ #if TOUCHGATE_DEBUG
103+ qDebug() << "[TouchGate] Got ownership of touch " << event->touchId();
104+ #endif
105+ touchInfo.ownership = OwnershipGranted;
106+ } else {
107+ #if TOUCHGATE_DEBUG
108+ qDebug() << "[TouchGate] Lost ownership of touch " << event->touchId();
109+ #endif
110+ m_touchInfoMap.remove(event->touchId());
111+ removeTouchFromStoredEvents(event->touchId());
112+ }
113+
114+ dispatchFullyOwnedEvents();
115+}
116+
117+bool TouchGate::isTouchPointOwned(int touchId) const
118+{
119+ return m_touchInfoMap[touchId].ownership == OwnershipGranted;
120+}
121+
122+void TouchGate::storeTouchEvent(const QTouchEvent *event)
123+{
124+ #if TOUCHGATE_DEBUG
125+ qDebug() << "[TouchGate] Storing" << qPrintable(touchEventToString(event));
126+ #endif
127+
128+ TouchEvent clonedEvent(event);
129+ m_storedEvents.append(std::move(clonedEvent));
130+}
131+
132+void TouchGate::removeTouchFromStoredEvents(int touchId)
133+{
134+ int i = 0;
135+ while (i < m_storedEvents.count()) {
136+ TouchEvent &event = m_storedEvents[i];
137+ bool removed = event.removeTouch(touchId);
138+
139+ if (removed && event.touchPoints.isEmpty()) {
140+ m_storedEvents.removeAt(i);
141+ } else {
142+ ++i;
143+ }
144+ }
145+}
146+
147+void TouchGate::dispatchFullyOwnedEvents()
148+{
149+ while (!m_storedEvents.isEmpty() && eventIsFullyOwned(m_storedEvents.first())) {
150+ TouchEvent event = m_storedEvents.takeFirst();
151+ dispatchTouchEventToTarget(event);
152+ }
153+}
154+
155+#if TOUCHGATE_DEBUG
156+QString TouchGate::oldestPendingTouchIdsString()
157+{
158+ Q_ASSERT(!m_storedEvents.isEmpty());
159+
160+ QString str;
161+
162+ const auto &touchPoints = m_storedEvents.first().touchPoints;
163+ for (int i = 0; i < touchPoints.count(); ++i) {
164+ if (!isTouchPointOwned(touchPoints[i].id())) {
165+ if (!str.isEmpty()) {
166+ str.append(", ");
167+ }
168+ str.append(QString::number(touchPoints[i].id()));
169+ }
170+ }
171+
172+ return str;
173+}
174+#endif
175+
176+bool TouchGate::eventIsFullyOwned(const TouchGate::TouchEvent &event) const
177+{
178+ for (int i = 0; i < event.touchPoints.count(); ++i) {
179+ if (!isTouchPointOwned(event.touchPoints[i].id())) {
180+ return false;
181+ }
182+ }
183+
184+ return true;
185+}
186+
187+void TouchGate::setTargetItem(QQuickItem *item)
188+{
189+ // TODO: changing the target item while dispatch of touch events is taking place will
190+ // create a mess
191+
192+ if (item == m_targetItem.data())
193+ return;
194+
195+ m_targetItem = item;
196+ Q_EMIT targetItemChanged(item);
197+}
198+
199+void TouchGate::dispatchTouchEventToTarget(const TouchEvent &event)
200+{
201+ dispatchTouchEventToTarget(event.eventType,
202+ event.device,
203+ event.modifiers,
204+ event.touchPoints,
205+ event.target,
206+ event.window,
207+ event.timestamp);
208+}
209+
210+void TouchGate::dispatchTouchEventToTarget(QTouchEvent* event)
211+{
212+ dispatchTouchEventToTarget(event->type(),
213+ event->device(),
214+ event->modifiers(),
215+ event->touchPoints(),
216+ event->target(),
217+ event->window(),
218+ event->timestamp());
219+}
220+
221+void TouchGate::dispatchTouchEventToTarget(QEvent::Type eventType,
222+ QTouchDevice *device,
223+ Qt::KeyboardModifiers modifiers,
224+ const QList<QTouchEvent::TouchPoint> &touchPoints,
225+ QObject *target,
226+ QWindow *window,
227+ ulong timestamp)
228+{
229+ removeTouchInfoForEndedTouches(touchPoints);
230+
231+ if (m_targetItem.isNull()) {
232+ qWarning("[TouchGate] Cannot dispatch touch event because target item is null");
233+ return;
234+ }
235+
236+ QQuickItem *targetItem = m_targetItem.data();
237+
238+ if (!targetItem->isEnabled() || !targetItem->isVisible()) {
239+ #if TOUCHGATE_DEBUG
240+ qDebug() << "[TouchGate] Cannot dispatch touch event to" << targetItem
241+ << "because it's disabled or invisible.";
242+ #endif
243+ return;
244+ }
245+
246+ // Map touch points to targetItem coordinates
247+ QList<QTouchEvent::TouchPoint> targetTouchPoints = touchPoints;
248+ transformTouchPoints(targetTouchPoints, QQuickItemPrivate::get(targetItem)->windowToItemTransform());
249+ QTouchEvent *eventForTargetItem = createQTouchEvent(eventType, device, modifiers, targetTouchPoints,
250+ target, window, timestamp);
251+
252+ #if TOUCHGATE_DEBUG
253+ qDebug() << "[TouchGate] dispatching" << qPrintable(touchEventToString(eventForTargetItem))
254+ << "to" << targetItem;
255+ #endif
256+
257+ QCoreApplication::sendEvent(targetItem, eventForTargetItem);
258+
259+ delete eventForTargetItem;
260+}
261+
262+// NB: From QQuickWindow
263+void TouchGate::transformTouchPoints(QList<QTouchEvent::TouchPoint> &touchPoints, const QTransform &transform)
264+{
265+ QMatrix4x4 transformMatrix(transform);
266+ for (int i=0; i<touchPoints.count(); i++) {
267+ QTouchEvent::TouchPoint &touchPoint = touchPoints[i];
268+ touchPoint.setRect(transform.mapRect(touchPoint.sceneRect()));
269+ touchPoint.setStartPos(transform.map(touchPoint.startScenePos()));
270+ touchPoint.setLastPos(transform.map(touchPoint.lastScenePos()));
271+ touchPoint.setVelocity(transformMatrix.mapVector(touchPoint.velocity()).toVector2D());
272+ }
273+}
274+
275+QTouchEvent *TouchGate::createQTouchEvent(QEvent::Type eventType,
276+ QTouchDevice *device,
277+ Qt::KeyboardModifiers modifiers,
278+ const QList<QTouchEvent::TouchPoint> &touchPoints,
279+ QObject *target,
280+ QWindow *window,
281+ ulong timestamp)
282+{
283+ Qt::TouchPointStates eventStates = 0;
284+ for (int i = 0; i < touchPoints.count(); i++)
285+ eventStates |= touchPoints[i].state();
286+ // if all points have the same state, set the event type accordingly
287+ switch (eventStates) {
288+ case Qt::TouchPointPressed:
289+ eventType = QEvent::TouchBegin;
290+ break;
291+ case Qt::TouchPointReleased:
292+ eventType = QEvent::TouchEnd;
293+ break;
294+ default:
295+ eventType = QEvent::TouchUpdate;
296+ break;
297+ }
298+
299+ QTouchEvent *touchEvent = new QTouchEvent(eventType);
300+ touchEvent->setWindow(window);
301+ touchEvent->setTarget(target);
302+ touchEvent->setDevice(device);
303+ touchEvent->setModifiers(modifiers);
304+ touchEvent->setTouchPoints(touchPoints);
305+ touchEvent->setTouchPointStates(eventStates);
306+ touchEvent->setTimestamp(timestamp);
307+ touchEvent->accept();
308+ return touchEvent;
309+}
310+
311+void TouchGate::removeTouchInfoForEndedTouches(const QList<QTouchEvent::TouchPoint> &touchPoints)
312+{
313+ for (int i = 0; i < touchPoints.size(); ++i) {\
314+ const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
315+
316+ if (touchPoint.state() == Qt::TouchPointReleased) {
317+ Q_ASSERT(m_touchInfoMap.contains(touchPoint.id()));
318+ Q_ASSERT(m_touchInfoMap[touchPoint.id()].ended);
319+ Q_ASSERT(m_touchInfoMap[touchPoint.id()].ownership == OwnershipGranted);
320+ m_touchInfoMap.remove(touchPoint.id());
321+ }
322+ }
323+}
324+
325+TouchGate::TouchEvent::TouchEvent(const QTouchEvent *event)
326+ : eventType(event->type())
327+ , device(event->device())
328+ , modifiers(event->modifiers())
329+ , touchPoints(event->touchPoints())
330+ , target(event->target())
331+ , window(event->window())
332+ , timestamp(event->timestamp())
333+{
334+}
335+
336+bool TouchGate::TouchEvent::removeTouch(int touchId)
337+{
338+ bool removed = false;
339+ for (int i = 0; i < touchPoints.count() && !removed; ++i) {
340+ if (touchPoints[i].id() == touchId) {
341+ touchPoints.removeAt(i);
342+ removed = true;
343+ }
344+ }
345+
346+ return removed;
347+}
plugins/Ubuntu/Gestures/TouchGate.hnew file+126
@@ -0,0 +1,126 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef UBUNTU_TOUCH_GATE_H
18+#define UBUNTU_TOUCH_GATE_H
19+
20+#include "UbuntuGesturesQmlGlobal.h"
21+
22+#include <QQuickItem>
23+#include <QList>
24+#include <QPointer>
25+#include <QMap>
26+
27+#define TOUCHGATE_DEBUG 0
28+
29+class TouchOwnershipEvent;
30+
31+/*
32+ Blocks the passage of events until ownership over the related touch points is granted.
33+
34+ Blocked touch events won't be discarded. Instead they will be buffered until ownership
35+ is granted. If ownership is given to another item, the event buffer is cleared.
36+
37+ A TouchGate is useful as a mediator for items that do not understand, or gracefully handle,
38+ touch canceling. By having a TouchGate in front of them you guarantee that only owned touches (i.e.,
39+ touches that won't be canceled later) reaches them.
40+ */
41+class UBUNTUGESTURESQML_EXPORT TouchGate : public QQuickItem {
42+ Q_OBJECT
43+
44+ // Item that's going to receive the touch events that make it through the gate.
45+ Q_PROPERTY(QQuickItem* targetItem READ targetItem WRITE setTargetItem NOTIFY targetItemChanged)
46+
47+public:
48+ bool event(QEvent *e) override;
49+
50+ QQuickItem *targetItem() { return m_targetItem; }
51+ void setTargetItem(QQuickItem *item);
52+
53+Q_SIGNALS:
54+ void targetItemChanged(QQuickItem *item);
55+
56+protected:
57+ void touchEvent(QTouchEvent *event) override;
58+private:
59+ class TouchEvent {
60+ public:
61+ TouchEvent(const QTouchEvent *event);
62+
63+ bool removeTouch(int touchId);
64+
65+ QEvent::Type eventType;
66+ QTouchDevice *device;
67+ Qt::KeyboardModifiers modifiers;
68+ QList<QTouchEvent::TouchPoint> touchPoints;
69+ QObject *target;
70+ QWindow *window;
71+ ulong timestamp;
72+ };
73+
74+ void touchOwnershipEvent(TouchOwnershipEvent *event);
75+ bool isTouchPointOwned(int touchId) const;
76+ void storeTouchEvent(const QTouchEvent *event);
77+ void removeTouchFromStoredEvents(int touchId);
78+ void dispatchFullyOwnedEvents();
79+ bool eventIsFullyOwned(const TouchEvent &event) const;
80+
81+ void dispatchTouchEventToTarget(const TouchEvent &event);
82+ void dispatchTouchEventToTarget(QTouchEvent* event);
83+ void dispatchTouchEventToTarget(
84+ QEvent::Type eventType,
85+ QTouchDevice *device,
86+ Qt::KeyboardModifiers modifiers,
87+ const QList<QTouchEvent::TouchPoint> &touchPoints,
88+ QObject *target,
89+ QWindow *window,
90+ ulong timestamp);
91+
92+ void transformTouchPoints(QList<QTouchEvent::TouchPoint> &touchPoints, const QTransform &transform);
93+ static QTouchEvent *createQTouchEvent(QEvent::Type eventType,
94+ QTouchDevice *device,
95+ Qt::KeyboardModifiers modifiers,
96+ const QList<QTouchEvent::TouchPoint> &touchPoints,
97+ QObject *target,
98+ QWindow *window,
99+ ulong timestamp);
100+ void removeTouchInfoForEndedTouches(const QList<QTouchEvent::TouchPoint> &touchPoints);
101+
102+ #if TOUCHGATE_DEBUG
103+ QString oldestPendingTouchIdsString();
104+ #endif
105+
106+ QList<TouchEvent> m_storedEvents;
107+
108+ enum {
109+ OwnershipUndefined,
110+ OwnershipRequested,
111+ OwnershipGranted,
112+ };
113+ class TouchInfo {
114+ public:
115+ TouchInfo() {ownership = OwnershipUndefined; ended = false;}
116+ int ownership;
117+ bool ended;
118+ };
119+ QMap<int, TouchInfo> m_touchInfoMap;
120+
121+ QPointer<QQuickItem> m_targetItem;
122+
123+ friend class tst_TouchGate;
124+};
125+
126+#endif // UBUNTU_TOUCH_GATE_H
plugins/Ubuntu/Gestures/UbuntuGesturesQmlGlobal.hnew file+23
@@ -0,0 +1,23 @@
1+/*
2+ * Copyright (C) 2013 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include <QtCore/QtGlobal>
18+
19+#if defined(UBUNTUGESTURESQML_LIBRARY)
20+# define UBUNTUGESTURESQML_EXPORT Q_DECL_EXPORT
21+#else
22+# define UBUNTUGESTURESQML_EXPORT Q_DECL_IMPORT
23+#endif
plugins/Ubuntu/Gestures/plugin.cpp+3−1
@@ -19,6 +19,7 @@
19#include "Direction.h"
19#include "Direction.h"
20#include "DirectionalDragArea.h"
20#include "DirectionalDragArea.h"
21#include "PressedOutsideNotifier.h"
21#include "PressedOutsideNotifier.h"
22+#include "TouchGate.h"
22
23
23#include <qqml.h>
24#include <qqml.h>
24
25
@@ -28,10 +29,11 @@ static QObject* directionSingleton(QQmlEngine* engine, QJSEngine* scriptEngine)
28 return new Direction;
29 return new Direction;
29}
30}
30
31
31−void UbuntuGestureQmlPlugin::registerTypes(const char *uri)
32+void UbuntuGesturesQmlPlugin::registerTypes(const char *uri)
32{
33{
33 qmlRegisterSingletonType<Direction>(uri, 0, 1, "Direction", directionSingleton);
34 qmlRegisterSingletonType<Direction>(uri, 0, 1, "Direction", directionSingleton);
34 qmlRegisterType<DirectionalDragArea>(uri, 0, 1, "DirectionalDragArea");
35 qmlRegisterType<DirectionalDragArea>(uri, 0, 1, "DirectionalDragArea");
35 qmlRegisterType<AxisVelocityCalculator>(uri, 0, 1, "AxisVelocityCalculator");
36 qmlRegisterType<AxisVelocityCalculator>(uri, 0, 1, "AxisVelocityCalculator");
36 qmlRegisterType<PressedOutsideNotifier>(uri, 0, 1, "PressedOutsideNotifier");
37 qmlRegisterType<PressedOutsideNotifier>(uri, 0, 1, "PressedOutsideNotifier");
38+ qmlRegisterType<TouchGate>(uri, 0, 1, "TouchGate");
37}
39}
plugins/Ubuntu/Gestures/plugin.h+1−1
@@ -19,7 +19,7 @@
19
19
20#include <QtQml/QQmlExtensionPlugin>
20#include <QtQml/QQmlExtensionPlugin>
21
21
22−class UbuntuGestureQmlPlugin : public QQmlExtensionPlugin
22+class UbuntuGesturesQmlPlugin : public QQmlExtensionPlugin
23{
23{
24 Q_OBJECT
24 Q_OBJECT
25 Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface")
25 Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface")
plugins/Ubuntu/Gestures/qmldir+1−1
@@ -1,3 +1,3 @@
1module Ubuntu.Gestures
1module Ubuntu.Gestures
2−plugin UbuntuGestureQml
2+plugin UbuntuGesturesQml
3typeinfo Gestures.qmltypes
3typeinfo Gestures.qmltypes
qml/Components/DragHandle.qml+2−2
@@ -47,12 +47,12 @@ EdgeDragArea {
47 id: dragArea
47 id: dragArea
48 objectName: "dragHandle"
48 objectName: "dragHandle"
49
49
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
51 // it conflicts with the hinting idea.
51 // it conflicts with the hinting idea.
52− // The only part we keep in this situation is that it must be a single-finger gesture.
53 distanceThreshold: hintDisplacement > 0 ? 0 : defaultDistanceThreshold
52 distanceThreshold: hintDisplacement > 0 ? 0 : defaultDistanceThreshold
54 maxSilenceTime: hintDisplacement > 0 ? 60*60*1000 : defaultMaxSilenceTime
53 maxSilenceTime: hintDisplacement > 0 ? 60*60*1000 : defaultMaxSilenceTime
55 maxDeviation: hintDisplacement > 0 ? 999999 : defaultMaxDeviation
54 maxDeviation: hintDisplacement > 0 ? 999999 : defaultMaxDeviation
55+ compositionTime: hintDisplacement > 0 ? 0 : defaultCompositionTime
56
56
57 property bool stretch: false
57 property bool stretch: false
58
58
qml/Components/EdgeDragArea.qml+2
@@ -34,6 +34,7 @@ DirectionalDragArea {
34 distanceThreshold: defaultDistanceThreshold
34 distanceThreshold: defaultDistanceThreshold
35 minSpeed: defaultMinSpeed
35 minSpeed: defaultMinSpeed
36 maxSilenceTime: defaultMaxSilenceTime
36 maxSilenceTime: defaultMaxSilenceTime
37+ compositionTime: defaultCompositionTime
37
38
38 readonly property real defaultMaxDeviation: units.gu(3)
39 readonly property real defaultMaxDeviation: units.gu(3)
39 readonly property real defaultWideningAngle: 50
40 readonly property real defaultWideningAngle: 50
@@ -41,4 +42,5 @@ DirectionalDragArea {
41 // some people were getting false negatives with it enabled.
42 // some people were getting false negatives with it enabled.
42 readonly property real defaultMinSpeed: units.gu(0)
43 readonly property real defaultMinSpeed: units.gu(0)
43 readonly property int defaultMaxSilenceTime: 200
44 readonly property int defaultMaxSilenceTime: 200
45+ readonly property int defaultCompositionTime: 60
44}
46}
qml/Components/InputMethod.qml+13
@@ -17,6 +17,7 @@
17import QtQuick 2.0
17import QtQuick 2.0
18import Unity.Application 0.1
18import Unity.Application 0.1
19import Ubuntu.Components 0.1
19import Ubuntu.Components 0.1
20+import Ubuntu.Gestures 0.1
20
21
21Item {
22Item {
22 id: root
23 id: root
@@ -25,6 +26,18 @@ Item {
25
26
26 property int transitionDuration: UbuntuAnimation.FastDuration
27 property int transitionDuration: UbuntuAnimation.FastDuration
27
28
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+
28 state: {
41 state: {
29 if (surface && surface.state != MirSurfaceItem.Minimized) {
42 if (surface && surface.state != MirSurfaceItem.Minimized) {
30 return "shown";
43 return "shown";
qml/Dash/Dash.qml+29−7
@@ -207,6 +207,12 @@ Showable {
207 }
207 }
208 }
208 }
209
209
210+ // This is to avoid the situation where a bottom-edge swipe would bring up the dash overview
211+ // (as expected) but would also cause the dash content flickable to move a bit, because
212+ // that flickable was getting the touch events while overviewDragHandle was still undecided
213+ // about whether that touch was indeed performing a directional drag gesture.
214+ forceNonInteractive: overviewDragHandle.status != DirectionalDragArea.WaitingForTouch
215+
210 enabled: overviewController.progress == 0
216 enabled: overviewController.progress == 0
211 opacity: enabled ? 1 : 0
217 opacity: enabled ? 1 : 0
212 }
218 }
@@ -363,16 +369,32 @@ Showable {
363 height: units.gu(2)
369 height: units.gu(2)
364
370
365 onSceneDistanceChanged: {
371 onSceneDistanceChanged: {
372+ if (status == DirectionalDragArea.Recognized && initialSceneDistance != -1) {
366− if (overviewController.enableAnimation) {
373+ if (overviewController.enableAnimation) {
367− dashContentCache.scheduleUpdate();
374+ dashContentCache.scheduleUpdate();
375+ }
376+ overviewController.enableAnimation = false;
377+ var deltaDistance = sceneDistance - initialSceneDistance;
378+ overviewController.progress = Math.max(0, Math.min(1, deltaDistance / fullMovement));
368 }
379 }
369− overviewController.enableAnimation = false;
370− overviewController.progress = Math.max(0, Math.min(1, sceneDistance / fullMovement));
371 }
380 }
372
381
373− onDraggingChanged: {
382+ property int previousStatus: -1
383+ property int currentStatus: DirectionalDragArea.WaitingForTouch
384+ property real initialSceneDistance: -1
385+
386+ onStatusChanged: {
387+ previousStatus = currentStatus;
388+ currentStatus = status;
389+
390+ if (status == DirectionalDragArea.Recognized) {
391+ initialSceneDistance = sceneDistance;
392+ } else if (status == DirectionalDragArea.WaitingForTouch &&
393+ previousStatus == DirectionalDragArea.Recognized) {
374− overviewController.enableAnimation = true;
394+ overviewController.enableAnimation = true;
375− overviewController.progress = (overviewController.progress > 0.7) ? 1 : 0;
395+ overviewController.progress = (overviewController.progress > 0.7) ? 1 : 0;
396+ initialSceneDistance = -1;
397+ }
376 }
398 }
377 }
399 }
378
400
qml/Dash/DashContent.qml+4−1
@@ -23,6 +23,7 @@ import "../Components"
23Item {
23Item {
24 id: dashContent
24 id: dashContent
25
25
26+ property bool forceNonInteractive: false
26 property alias scopes: dashContentList.model
27 property alias scopes: dashContentList.model
27 readonly property alias currentIndex: dashContentList.currentIndex
28 readonly property alias currentIndex: dashContentList.currentIndex
28 readonly property string currentScopeId: dashContentList.currentItem ? dashContentList.currentItem.scopeId : ""
29 readonly property string currentScopeId: dashContentList.currentItem ? dashContentList.currentItem.scopeId : ""
@@ -101,7 +102,8 @@ Item {
101 id: dashContentList
102 id: dashContentList
102 objectName: "dashContentList"
103 objectName: "dashContentList"
103
104
105+ interactive: !dashContent.forceNonInteractive && dashContent.scopes.loaded && currentItem
104− interactive: dashContent.scopes.loaded && currentItem && !currentItem.moving && !currentItem.navigationShown && !currentItem.subPageShown
106+ && !currentItem.moving && !currentItem.navigationShown && !currentItem.subPageShown
105 anchors.fill: parent
107 anchors.fill: parent
106 orientation: ListView.Horizontal
108 orientation: ListView.Horizontal
107 boundsBehavior: Flickable.DragAndOvershootBounds
109 boundsBehavior: Flickable.DragAndOvershootBounds
@@ -180,6 +182,7 @@ Item {
180 item.paginationCount = Qt.binding(function() { return dashContentList.count } )
182 item.paginationCount = Qt.binding(function() { return dashContentList.count } )
181 item.paginationIndex = Qt.binding(function() { return dashContentList.currentIndex } )
183 item.paginationIndex = Qt.binding(function() { return dashContentList.currentIndex } )
182 item.holdingList = dashContentList;
184 item.holdingList = dashContentList;
185+ item.forceNonInteractive = Qt.binding(function() { return dashContent.forceNonInteractive } )
183 }
186 }
184 Connections {
187 Connections {
185 target: isCurrent ? scope : null
188 target: isCurrent ? scope : null
qml/Dash/GenericScopeView.qml+2
@@ -26,6 +26,7 @@ FocusScope {
26 id: scopeView
26 id: scopeView
27
27
28 readonly property bool navigationShown: pageHeaderLoader.item ? pageHeaderLoader.item.bottomItem[0].openList : false
28 readonly property bool navigationShown: pageHeaderLoader.item ? pageHeaderLoader.item.bottomItem[0].openList : false
29+ property bool forceNonInteractive: false
29 property var scope: null
30 property var scope: null
30 property SortFilterProxyModel categories: categoryFilter
31 property SortFilterProxyModel categories: categoryFilter
31 property bool isCurrent: false
32 property bool isCurrent: false
@@ -149,6 +150,7 @@ FocusScope {
149 ScopeListView {
150 ScopeListView {
150 id: categoryView
151 id: categoryView
151 objectName: "categoryListView"
152 objectName: "categoryListView"
153+ interactive: !forceNonInteractive
152
154
153 x: subPageLoader.open ? -width : 0
155 x: subPageLoader.open ? -width : 0
154 visible: x != -width
156 visible: x != -width
qml/Launcher/Launcher.qml+3−1
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright (C) 2013 Canonical, Ltd.
2+ * Copyright (C) 2013-2014 Canonical, Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
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
5 * it under the terms of the GNU General Public License as published by
@@ -76,6 +76,7 @@ Item {
76
76
77 Timer {
77 Timer {
78 id: dismissTimer
78 id: dismissTimer
79+ objectName: "dismissTimer"
79 interval: 5000
80 interval: 5000
80 onTriggered: {
81 onTriggered: {
81 if (!panel.preventHiding) {
82 if (!panel.preventHiding) {
@@ -218,6 +219,7 @@ Item {
218
219
219 EdgeDragArea {
220 EdgeDragArea {
220 id: dragArea
221 id: dragArea
222+ objectName: "launcherDragArea"
221
223
222 direction: Direction.Rightwards
224 direction: Direction.Rightwards
223
225
qml/Launcher/LauncherPanel.qml+1
@@ -45,6 +45,7 @@ Rectangle {
45 }
45 }
46
46
47 Rectangle {
47 Rectangle {
48+ objectName: "buttonShowDashHome"
48 width: parent.width
49 width: parent.width
49 height: units.gu(7)
50 height: units.gu(7)
50 color: UbuntuColors.orange
51 color: UbuntuColors.orange
qml/Stages/ApplicationWindow.qml−18
@@ -61,14 +61,6 @@ Item {
61 // Remove this when possible
61 // Remove this when possible
62 property bool surfaceInitialized: false
62 property bool surfaceInitialized: false
63
63
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− }
72 }
64 }
73
65
74 Timer {
66 Timer {
@@ -77,15 +69,6 @@ Item {
77 onTriggered: { if (sessionContainer.surface) {d.surfaceInitialized = true;} }
69 onTriggered: { if (sessionContainer.surface) {d.surfaceInitialized = true;} }
78 }
70 }
79
71
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−
89 Image {
72 Image {
90 id: screenshotImage
73 id: screenshotImage
91 objectName: "screenshotImage"
74 objectName: "screenshotImage"
@@ -132,7 +115,6 @@ Item {
132 onSurfaceChanged: {
115 onSurfaceChanged: {
133 if (sessionContainer.surface) {
116 if (sessionContainer.surface) {
134 surfaceInitTimer.start();
117 surfaceInitTimer.start();
135− d.forceSurfaceActiveFocusIfReady();
136 } else {
118 } else {
137 d.surfaceInitialized = false;
119 d.surfaceInitialized = false;
138 }
120 }
qml/Stages/PhoneStage.qml+14−5
@@ -131,7 +131,7 @@ Rectangle {
131
131
132 // This indicates when the spreadView is active. That means, all the animations
132 // This indicates when the spreadView is active. That means, all the animations
133 // are activated and tiles need to line up for the spread.
133 // are activated and tiles need to line up for the spread.
134− readonly property bool active: shiftedContentX > 0 || spreadDragArea.dragging
134+ readonly property bool active: shiftedContentX > 0 || spreadDragArea.status === DirectionalDragArea.Recognized
135
135
136 // The flickable needs to fill the screen in order to get touch events all over.
136 // The flickable needs to fill the screen in order to get touch events all over.
137 // However, we don't want to the user to be able to scroll back all the way. For
137 // However, we don't want to the user to be able to scroll back all the way. For
@@ -418,6 +418,7 @@ Rectangle {
418
418
419 EdgeDragArea {
419 EdgeDragArea {
420 id: spreadDragArea
420 id: spreadDragArea
421+ objectName: "spreadDragArea"
421 direction: Direction.Leftwards
422 direction: Direction.Leftwards
422 enabled: spreadView.phase != 2 && root.spreadEnabled
423 enabled: spreadView.phase != 2 && root.spreadEnabled
423
424
@@ -437,9 +438,10 @@ Rectangle {
437 spreadView.phase = 0;
438 spreadView.phase = 0;
438 spreadView.contentX = -spreadView.shift;
439 spreadView.contentX = -spreadView.shift;
439 }
440 }
440− if (dragging && attachedToView) {
441+ if (dragging && status == DirectionalDragArea.Recognized && attachedToView) {
441 // Gesture recognized. Let's move the spreadView with the finger
442 // Gesture recognized. Let's move the spreadView with the finger
443+ var finalX = Math.min(touchX + width, width);
442− spreadView.contentX = -touchX + spreadDragArea.width - spreadView.shift;
444+ spreadView.contentX = -finalX + spreadDragArea.width - spreadView.shift;
443 }
445 }
444 if (attachedToView && spreadView.shiftedContentX >= spreadView.width * spreadView.positionMarker3) {
446 if (attachedToView && spreadView.shiftedContentX >= spreadView.width * spreadView.positionMarker3) {
445 // We passed positionMarker3. Detach from spreadView and snap it.
447 // We passed positionMarker3. Detach from spreadView and snap it.
@@ -449,7 +451,13 @@ Rectangle {
449 gesturePoints.push(touchX);
451 gesturePoints.push(touchX);
450 }
452 }
451
453
454+ property int previousStatus: -1
455+ property int currentStatus: DirectionalDragArea.WaitingForTouch
456+
452 onStatusChanged: {
457 onStatusChanged: {
458+ previousStatus = currentStatus;
459+ currentStatus = status;
460+
453 if (status == DirectionalDragArea.Recognized) {
461 if (status == DirectionalDragArea.Recognized) {
454 attachedToView = true;
462 attachedToView = true;
455 }
463 }
@@ -457,7 +465,7 @@ Rectangle {
457
465
458 onDraggingChanged: {
466 onDraggingChanged: {
459 if (dragging) {
467 if (dragging) {
460− // Gesture recognized. Start recording this gesture
468+ // A potential edge-drag gesture has started. Start recording it
461 gesturePoints = [];
469 gesturePoints = [];
462 return;
470 return;
463 }
471 }
@@ -474,7 +482,8 @@ Rectangle {
474 }
482 }
475 gesturePoints = [];
483 gesturePoints = [];
476
484
485+ if (previousStatus == DirectionalDragArea.Recognized &&
477− if (oneWayFlick && spreadView.shiftedContentX > units.gu(2) &&
486+ oneWayFlick && spreadView.shiftedContentX > units.gu(2) &&
478 spreadView.shiftedContentX < spreadView.positionMarker1 * spreadView.width) {
487 spreadView.shiftedContentX < spreadView.positionMarker1 * spreadView.width) {
479 // If it was a short one-way movement, do the Alt+Tab switch
488 // If it was a short one-way movement, do the Alt+Tab switch
480 // no matter if we didn't cross positionMarker1 yet.
489 // no matter if we didn't cross positionMarker1 yet.
qml/Stages/SessionContainer.qml+1−13
@@ -23,7 +23,7 @@ Item {
23 property QtObject session
23 property QtObject session
24 readonly property var childSessions: session ? session.childSessions : null
24 readonly property var childSessions: session ? session.childSessions : null
25 readonly property alias surface: _surfaceContainer.surface
25 readonly property alias surface: _surfaceContainer.surface
26− property bool interactive: true
26+ property alias interactive: _surfaceContainer.interactive
27 property int orientation
27 property int orientation
28
28
29 readonly property alias surfaceContainer: _surfaceContainer
29 readonly property alias surfaceContainer: _surfaceContainer
@@ -34,18 +34,6 @@ Item {
34 orientation: root.orientation
34 orientation: root.orientation
35 }
35 }
36
36
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− }
49
37
50 Repeater {
38 Repeater {
51 model: root.childSessions
39 model: root.childSessions
qml/Stages/SurfaceContainer.qml+37−6
@@ -16,6 +16,7 @@
16
16
17import QtQuick 2.0
17import QtQuick 2.0
18import Ubuntu.Components 1.1
18import Ubuntu.Components 1.1
19+import Ubuntu.Gestures 0.1 // For TouchGate
19
20
20Item {
21Item {
21 id: root
22 id: root
@@ -23,21 +24,51 @@ Item {
23 property Item surface: null
24 property Item surface: null
24 property bool hadSurface: false
25 property bool hadSurface: false
25 property int orientation
26 property int orientation
27+ property bool interactive
26
28
27 onSurfaceChanged: {
29 onSurfaceChanged: {
28 if (surface) {
30 if (surface) {
29 surface.parent = root;
31 surface.parent = root;
32+ d.forceSurfaceActiveFocusIfReady();
30 } else {
33 } else {
31 hadSurface = true;
34 hadSurface = true;
32 }
35 }
33 }
36 }
34− Binding {
37+ Binding { target: surface; property: "anchors.fill"; value: root }
35− target: surface
38+ Binding { target: surface; property: "orientation"; value: root.orientation }
36− property: "anchors.fill"; value: root
39+ Binding { target: surface; property: "z"; value: 1 }
40+ Binding { target: surface; property: "enabled"; value: root.interactive; when: surface }
41+ Binding { target: surface; property: "focus"; value: root.interactive; when: surface }
42+
43+ TouchGate {
44+ targetItem: surface
45+ anchors.fill: root
46+ enabled: root.surface ? root.surface.enabled : false
47+ z: 2
48+ }
49+
50+ Connections {
51+ target: root.surface
52+ // FIXME: I would rather not need to do this, but currently it doesn't get
53+ // active focus without it and I don't know why.
54+ // Possibly because if an item get focus=true before it has a parent, once
55+ // it gets a parent QQuickWindow won't check its focus and update its activeFocus
56+ // accordingly. Unlike when you focus=true after the item already has a parent.
57+ onFocusChanged: d.forceSurfaceActiveFocusIfReady();
58+ onParentChanged: d.forceSurfaceActiveFocusIfReady();
59+ onEnabledChanged: d.forceSurfaceActiveFocusIfReady();
37 }
60 }
38− Binding {
61+
62+ QtObject {
63+ id: d
64+ function forceSurfaceActiveFocusIfReady() {
65+ if (root.surface !== null &&
66+ root.surface.focus &&
39− target: surface
67+ root.surface.parent === root &&
40− property: "orientation"; value: root.orientation
68+ root.surface.enabled) {
69+ root.surface.forceActiveFocus();
70+ }
71+ }
41 }
72 }
42
73
43 states: [
74 states: [
src/CMakeLists.txt+6
@@ -1,5 +1,6 @@
1include_directories(
1include_directories(
2 ${Qt5Gui_PRIVATE_INCLUDE_DIRS}
2 ${Qt5Gui_PRIVATE_INCLUDE_DIRS}
3+ ${CMAKE_SOURCE_DIR}/libs/UbuntuGestures
3)
4)
4
5
5file(GLOB_RECURSE QML_FILES
6file(GLOB_RECURSE QML_FILES
@@ -29,6 +30,11 @@ if (NOT "${XCB_INCLUDE_DIRS}" STREQUAL "")
29 target_link_libraries(${SHELL_APP} ${XCB_LDFLAGS})
30 target_link_libraries(${SHELL_APP} ${XCB_LDFLAGS})
30endif()
31endif()
31
32
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+
32# install binaries
38# install binaries
33install(TARGETS ${SHELL_APP}
39install(TARGETS ${SHELL_APP}
34 RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
40 RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
src/Dash/CMakeLists.txt+5
@@ -9,6 +9,11 @@ add_executable(unity8-dash ${DASH_SRCS})
9
9
10qt5_use_modules(unity8-dash Gui Qml Quick Test)
10qt5_use_modules(unity8-dash Gui Qml Quick Test)
11
11
12+# For it to find libUbuntuGestures.so, needed by Ubuntu.Gestures QML module.
13+set_target_properties(unity8-dash PROPERTIES INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/${SHELL_PRIVATE_LIBDIR}")
14+
15+target_link_libraries(unity8-dash UbuntuGestures)
16+
12set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fvisibility=hidden")
17set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fvisibility=hidden")
13
18
14# install binaries
19# install binaries
src/Dash/main.cpp+6
@@ -31,6 +31,9 @@
31#include "../ApplicationArguments.h"
31#include "../ApplicationArguments.h"
32#include "../CachingNetworkManagerFactory.h"
32#include "../CachingNetworkManagerFactory.h"
33
33
34+// Ubuntu Gestures
35+#include <TouchRegistry.h>
36+
34int main(int argc, const char *argv[])
37int main(int argc, const char *argv[])
35{
38{
36 QGuiApplication *application = new QGuiApplication(argc, (char**)argv);
39 QGuiApplication *application = new QGuiApplication(argc, (char**)argv);
@@ -88,6 +91,9 @@ int main(int argc, const char *argv[])
88 view->setTitle("Scopes");
91 view->setTitle("Scopes");
89 view->rootContext()->setContextProperty("applicationArguments", &qmlArgs);
92 view->rootContext()->setContextProperty("applicationArguments", &qmlArgs);
90
93
94+ TouchRegistry touchRegistry;
95+ view->installEventFilter(&touchRegistry);
96+
91 // You will need this if you want to interact with touch-only components using a mouse
97 // You will need this if you want to interact with touch-only components using a mouse
92 // Needed only when manually testing on a desktop.
98 // Needed only when manually testing on a desktop.
93 MouseTouchAdaptor *mouseTouchAdaptor = 0;
99 MouseTouchAdaptor *mouseTouchAdaptor = 0;
src/main.cpp+5
@@ -34,6 +34,9 @@
34#include "ApplicationArguments.h"
34#include "ApplicationArguments.h"
35#include "CachingNetworkManagerFactory.h"
35#include "CachingNetworkManagerFactory.h"
36
36
37+// Ubuntu Gestures
38+#include <TouchRegistry.h>
39+
37int main(int argc, const char *argv[])
40int main(int argc, const char *argv[])
38{
41{
39 bool isMirServer = false;
42 bool isMirServer = false;
@@ -120,6 +123,8 @@ Load the testability driver");
120 if (parser.isSet(framelessOption)) {
123 if (parser.isSet(framelessOption)) {
121 view->setFlags(Qt::FramelessWindowHint);
124 view->setFlags(Qt::FramelessWindowHint);
122 }
125 }
126+ TouchRegistry touchRegistry;
127+ view->installEventFilter(&touchRegistry);
123
128
124 // You will need this if you want to interact with touch-only components using a mouse
129 // You will need this if you want to interact with touch-only components using a mouse
125 // Needed only when manually testing on a desktop.
130 // Needed only when manually testing on a desktop.
tests/CMakeLists.txt+1
@@ -7,6 +7,7 @@ add_custom_target(qmluitests)
7add_custom_target(qmltests)
7add_custom_target(qmltests)
8add_dependencies(qmltests qmlunittests qmluitests)
8add_dependencies(qmltests qmlunittests qmluitests)
9
9
10+add_subdirectory(libs)
10add_subdirectory(qmltests)
11add_subdirectory(qmltests)
11add_subdirectory(mocks)
12add_subdirectory(mocks)
12add_subdirectory(whitespace)
13add_subdirectory(whitespace)
tests/libs/CMakeLists.txtnew file+1
@@ -0,0 +1 @@
1+add_subdirectory(UbuntuGestures)
tests/libs/UbuntuGestures/CMakeLists.txtnew file+20
@@ -0,0 +1,20 @@
1+include_directories(
2+ ${CMAKE_SOURCE_DIR}/libs/UbuntuGestures
3+ ${CMAKE_CURRENT_BINARY_DIR}
4+ )
5+
6+macro(add_gesture_test CLASSNAME)
7+ set(testCommand
8+ ${CLASSNAME}TestExec -o ${CMAKE_BINARY_DIR}/test${CLASSNAME}.xml,xunitxml -o -,txt)
9+
10+ add_test(NAME test${CLASSNAME} COMMAND ${testCommand})
11+
12+ set_tests_properties(test${CLASSNAME} PROPERTIES
13+ ENVIRONMENT LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/libs/UbuntuGestures)
14+
15+ add_executable(${CLASSNAME}TestExec tst_${CLASSNAME}.cpp)
16+ qt5_use_modules(${CLASSNAME}TestExec Test Core Gui Quick)
17+ target_link_libraries(${CLASSNAME}TestExec UbuntuGestures)
18+endmacro(add_gesture_test)
19+
20+add_gesture_test(TouchRegistry)
tests/libs/UbuntuGestures/tst_TouchRegistry.cppnew file+803
@@ -0,0 +1,803 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include <QtTest>
18+#include <QSet>
19+#include <QTouchEvent>
20+
21+#include <Timer.h>
22+#include <TouchOwnershipEvent.h>
23+#include <TouchRegistry.h>
24+#include <UnownedTouchEvent.h>
25+
26+using namespace UbuntuGestures;
27+
28+class TouchMemento {
29+public:
30+ TouchMemento(const QTouchEvent *touchEvent);
31+ Qt::TouchPointStates touchPointStates;
32+ QList<QTouchEvent::TouchPoint> touchPoints;
33+
34+ bool containsTouchWithId(int touchId) const;
35+};
36+
37+class DummyCandidate : public QQuickItem
38+{
39+ Q_OBJECT
40+public:
41+ bool event(QEvent *e) override;
42+ QSet<int> ownedTouches;
43+ QSet<int> lostTouches;
44+ QList<TouchMemento> unownedTouchEvents;
45+};
46+
47+class tst_TouchRegistry : public QObject
48+{
49+ Q_OBJECT
50+public:
51+ tst_TouchRegistry() : QObject(0) { }
52+private Q_SLOTS:
53+ void initTestCase() {} // will be called before the first test function is executed
54+ void cleanupTestCase() {} // will be called after the last test function was executed.
55+
56+ void init() {} // called right before each and every test function is executed
57+ void cleanup() {} // called right after each and every test function is executed
58+
59+ void requestWithNoCandidates();
60+ void lateCandidateRequestGetsNothing();
61+ void lateCandidateGestOwnershipOnceEarlyCandidateQuits();
62+ void dispatchesTouchEventsToCandidates();
63+ void dispatchesTouchEventsToWatchers();
64+ void keepDispatchingToWatchersAfterLastCandidateGivesUp();
65+ void candidatesAndWatchers_1();
66+ void candidatesAndWatchers_2();
67+ void rejectingTouchfterItsEnd();
68+ void removeOldUndecidedCandidates();
69+};
70+
71+void tst_TouchRegistry::requestWithNoCandidates()
72+{
73+ TouchRegistry touchRegistry;
74+ DummyCandidate candidate;
75+
76+ {
77+ QList<QTouchEvent::TouchPoint> touchPoints;
78+ touchPoints.append(QTouchEvent::TouchPoint(0));
79+ touchPoints[0].setState(Qt::TouchPointPressed);
80+ QTouchEvent touchEvent(QEvent::TouchBegin,
81+ 0 /* device */,
82+ Qt::NoModifier,
83+ Qt::TouchPointPressed,
84+ touchPoints);
85+ touchRegistry.update(&touchEvent);
86+ }
87+
88+ touchRegistry.requestTouchOwnership(0, &candidate);
89+
90+ QVERIFY(candidate.ownedTouches.contains(0));
91+}
92+
93+void tst_TouchRegistry::lateCandidateRequestGetsNothing()
94+{
95+ TouchRegistry touchRegistry;
96+ DummyCandidate earlyCandidate;
97+ earlyCandidate.setObjectName("early");
98+ DummyCandidate lateCandidate;
99+ lateCandidate.setObjectName("late");
100+
101+ {
102+ QList<QTouchEvent::TouchPoint> touchPoints;
103+ touchPoints.append(QTouchEvent::TouchPoint(0));
104+ touchPoints[0].setState(Qt::TouchPointPressed);
105+ QTouchEvent touchEvent(QEvent::TouchBegin,
106+ 0 /* device */,
107+ Qt::NoModifier,
108+ Qt::TouchPointPressed,
109+ touchPoints);
110+ touchRegistry.update(&touchEvent);
111+ }
112+
113+ touchRegistry.addCandidateOwnerForTouch(0, &earlyCandidate);
114+ touchRegistry.addCandidateOwnerForTouch(0, &lateCandidate);
115+
116+ QVERIFY(earlyCandidate.ownedTouches.isEmpty());
117+ QVERIFY(lateCandidate.ownedTouches.isEmpty());
118+
119+ touchRegistry.requestTouchOwnership(0, &lateCandidate);
120+
121+ QVERIFY(earlyCandidate.ownedTouches.isEmpty());
122+ QVERIFY(lateCandidate.ownedTouches.isEmpty());
123+
124+ touchRegistry.requestTouchOwnership(0, &earlyCandidate);
125+
126+ QVERIFY(earlyCandidate.ownedTouches.contains(0));
127+ QVERIFY(lateCandidate.ownedTouches.isEmpty());
128+}
129+
130+void tst_TouchRegistry::lateCandidateGestOwnershipOnceEarlyCandidateQuits()
131+{
132+ TouchRegistry touchRegistry;
133+ DummyCandidate earlyCandidate;
134+ DummyCandidate lateCandidate;
135+
136+ {
137+ QList<QTouchEvent::TouchPoint> touchPoints;
138+ touchPoints.append(QTouchEvent::TouchPoint(0));
139+ touchPoints[0].setState(Qt::TouchPointPressed);
140+ QTouchEvent touchEvent(QEvent::TouchBegin,
141+ 0 /* device */,
142+ Qt::NoModifier,
143+ Qt::TouchPointPressed,
144+ touchPoints);
145+ touchRegistry.update(&touchEvent);
146+ }
147+
148+ touchRegistry.addCandidateOwnerForTouch(0, &earlyCandidate);
149+ touchRegistry.addCandidateOwnerForTouch(0, &lateCandidate);
150+
151+ QVERIFY(earlyCandidate.ownedTouches.isEmpty());
152+ QVERIFY(lateCandidate.ownedTouches.isEmpty());
153+
154+ touchRegistry.requestTouchOwnership(0, &lateCandidate);
155+
156+ QVERIFY(earlyCandidate.ownedTouches.isEmpty());
157+ QVERIFY(lateCandidate.ownedTouches.isEmpty());
158+
159+ touchRegistry.removeCandidateOwnerForTouch(0, &earlyCandidate);
160+
161+ QVERIFY(earlyCandidate.ownedTouches.isEmpty());
162+ QVERIFY(lateCandidate.ownedTouches.contains(0));
163+}
164+
165+void tst_TouchRegistry::dispatchesTouchEventsToCandidates()
166+{
167+ TouchRegistry touchRegistry;
168+ QQuickItem rootItem;
169+
170+ DummyCandidate candidate0;
171+ candidate0.setObjectName("0");
172+ candidate0.setParentItem(&rootItem);
173+ candidate0.setX(1);
174+ candidate0.setY(2);
175+
176+ DummyCandidate candidate1;
177+ candidate1.setObjectName("1");
178+ candidate0.setParentItem(&rootItem);
179+ candidate1.setX(3);
180+ candidate1.setY(4);
181+
182+ {
183+ QList<QTouchEvent::TouchPoint> touchPoints;
184+ touchPoints.append(QTouchEvent::TouchPoint(0));
185+ touchPoints[0].setState(Qt::TouchPointPressed);
186+ touchPoints[0].setRect(QRect(10, 10, 0, 0));
187+ QTouchEvent touchEvent(QEvent::TouchBegin,
188+ 0 /* device */,
189+ Qt::NoModifier,
190+ Qt::TouchPointPressed,
191+ touchPoints);
192+ touchRegistry.update(&touchEvent);
193+ }
194+
195+ touchRegistry.addCandidateOwnerForTouch(0, &candidate0);
196+
197+ {
198+ QList<QTouchEvent::TouchPoint> touchPoints;
199+ touchPoints.append(QTouchEvent::TouchPoint(0));
200+ touchPoints[0].setState(Qt::TouchPointMoved);
201+ touchPoints[0].setRect(QRect(11, 11, 0, 0));
202+ touchPoints.append(QTouchEvent::TouchPoint(1));
203+ touchPoints[1].setState(Qt::TouchPointPressed);
204+ touchPoints[1].setRect(QRect(20, 20, 0, 0));
205+ QTouchEvent touchEvent(QEvent::TouchUpdate,
206+ 0 /* device */,
207+ Qt::NoModifier,
208+ Qt::TouchPointPressed | Qt::TouchPointMoved,
209+ touchPoints);
210+ touchRegistry.update(&touchEvent);
211+ }
212+
213+ // candidate0 should have received an update on the touch he's interested on.
214+ QCOMPARE(candidate0.unownedTouchEvents.count(), 1);
215+ // make sure only touch 0 is there (i.e. no mention of touch 1)
216+ QCOMPARE(candidate0.unownedTouchEvents[0].touchPoints.count(), 1);
217+ QCOMPARE(candidate0.unownedTouchEvents[0].touchPoints[0].id(), 0);
218+ // Check that the points local coordinates have been mapped to candidate0's coordinate system.
219+ QCOMPARE(candidate0.unownedTouchEvents[0].touchPoints[0].rect().x(), 11 - candidate0.x());
220+ QCOMPARE(candidate0.unownedTouchEvents[0].touchPoints[0].rect().y(), 11 - candidate0.y());
221+
222+ touchRegistry.addCandidateOwnerForTouch(1, &candidate1);
223+
224+ {
225+ QList<QTouchEvent::TouchPoint> touchPoints;
226+ touchPoints.append(QTouchEvent::TouchPoint(0));
227+ touchPoints[0].setState(Qt::TouchPointMoved);
228+ touchPoints[0].setRect(QRect(12, 12, 0, 0));
229+ touchPoints.append(QTouchEvent::TouchPoint(1));
230+ touchPoints[1].setState(Qt::TouchPointMoved);
231+ touchPoints[1].setRect(QRect(21, 21, 0, 0));
232+ QTouchEvent touchEvent(QEvent::TouchUpdate,
233+ 0 /* device */,
234+ Qt::NoModifier,
235+ Qt::TouchPointPressed | Qt::TouchPointMoved,
236+ touchPoints);
237+ touchRegistry.update(&touchEvent);
238+ }
239+
240+ // candidate0 gets updates only for touch 0 and
241+ // candidate1 gets updates only for touch 1
242+
243+ QCOMPARE(candidate0.unownedTouchEvents.count(), 2);
244+ QCOMPARE(candidate0.unownedTouchEvents[1].touchPoints.count(), 1);
245+ QCOMPARE(candidate0.unownedTouchEvents[1].touchPoints[0].id(), 0);
246+
247+ QCOMPARE(candidate1.unownedTouchEvents.count(), 1);
248+ QCOMPARE(candidate1.unownedTouchEvents[0].touchPoints.count(), 1);
249+ QCOMPARE(candidate1.unownedTouchEvents[0].touchPoints[0].id(), 1);
250+}
251+
252+void tst_TouchRegistry::dispatchesTouchEventsToWatchers()
253+{
254+ TouchRegistry touchRegistry;
255+ QQuickItem rootItem;
256+
257+ DummyCandidate watcher;
258+ watcher.setObjectName("watcher");
259+ watcher.setParentItem(&rootItem);
260+ watcher.setX(1);
261+ watcher.setY(2);
262+
263+ {
264+ QList<QTouchEvent::TouchPoint> touchPoints;
265+ touchPoints.append(QTouchEvent::TouchPoint(0));
266+ touchPoints[0].setState(Qt::TouchPointPressed);
267+ touchPoints[0].setRect(QRect(10, 10, 0, 0));
268+ QTouchEvent touchEvent(QEvent::TouchBegin,
269+ 0 /* device */,
270+ Qt::NoModifier,
271+ Qt::TouchPointPressed,
272+ touchPoints);
273+ touchRegistry.update(&touchEvent);
274+ }
275+
276+ touchRegistry.addTouchWatcher(0, &watcher);
277+
278+ {
279+ QList<QTouchEvent::TouchPoint> touchPoints;
280+ touchPoints.append(QTouchEvent::TouchPoint(0));
281+ touchPoints[0].setState(Qt::TouchPointMoved);
282+ touchPoints[0].setRect(QRect(11, 11, 0, 0));
283+ touchPoints.append(QTouchEvent::TouchPoint(1));
284+ touchPoints[1].setState(Qt::TouchPointPressed);
285+ touchPoints[1].setRect(QRect(20, 20, 0, 0));
286+ QTouchEvent touchEvent(QEvent::TouchUpdate,
287+ 0 /* device */,
288+ Qt::NoModifier,
289+ Qt::TouchPointPressed | Qt::TouchPointMoved,
290+ touchPoints);
291+ touchRegistry.update(&touchEvent);
292+ }
293+
294+ // watcher should have received an update on the touch he's interested on.
295+ QCOMPARE(watcher.unownedTouchEvents.count(), 1);
296+ // make sure only touch 0 is there (i.e. no mention of touch 1)
297+ QCOMPARE(watcher.unownedTouchEvents[0].touchPoints.count(), 1);
298+ QCOMPARE(watcher.unownedTouchEvents[0].touchPoints[0].id(), 0);
299+ // Check that the points local coordinates have been mapped to watcher's coordinate system.
300+ QCOMPARE(watcher.unownedTouchEvents[0].touchPoints[0].rect().x(), 11 - watcher.x());
301+ QCOMPARE(watcher.unownedTouchEvents[0].touchPoints[0].rect().y(), 11 - watcher.y());
302+
303+ {
304+ QList<QTouchEvent::TouchPoint> touchPoints;
305+ touchPoints.append(QTouchEvent::TouchPoint(0));
306+ touchPoints[0].setState(Qt::TouchPointMoved);
307+ touchPoints[0].setRect(QRect(12, 12, 0, 0));
308+ touchPoints.append(QTouchEvent::TouchPoint(1));
309+ touchPoints[1].setState(Qt::TouchPointMoved);
310+ touchPoints[1].setRect(QRect(21, 21, 0, 0));
311+ QTouchEvent touchEvent(QEvent::TouchUpdate,
312+ 0 /* device */,
313+ Qt::NoModifier,
314+ Qt::TouchPointPressed | Qt::TouchPointMoved,
315+ touchPoints);
316+ touchRegistry.update(&touchEvent);
317+ }
318+
319+ // watcher gets updates only for touch 0
320+
321+ QCOMPARE(watcher.unownedTouchEvents.count(), 2);
322+ QCOMPARE(watcher.unownedTouchEvents[1].touchPoints.count(), 1);
323+ QCOMPARE(watcher.unownedTouchEvents[1].touchPoints[0].id(), 0);
324+}
325+
326+void tst_TouchRegistry::keepDispatchingToWatchersAfterLastCandidateGivesUp()
327+{
328+ TouchRegistry touchRegistry;
329+ DummyCandidate item;
330+
331+ {
332+ QList<QTouchEvent::TouchPoint> touchPoints;
333+ touchPoints.append(QTouchEvent::TouchPoint(0));
334+ touchPoints[0].setState(Qt::TouchPointPressed);
335+ QTouchEvent touchEvent(QEvent::TouchBegin,
336+ 0 /* device */,
337+ Qt::NoModifier,
338+ Qt::TouchPointPressed,
339+ touchPoints);
340+ touchRegistry.update(&touchEvent);
341+ }
342+
343+ touchRegistry.addCandidateOwnerForTouch(0, &item);
344+
345+ {
346+ QList<QTouchEvent::TouchPoint> touchPoints;
347+ touchPoints.append(QTouchEvent::TouchPoint(0));
348+ touchPoints[0].setState(Qt::TouchPointMoved);
349+ touchPoints.append(QTouchEvent::TouchPoint(1));
350+ touchPoints[1].setState(Qt::TouchPointPressed);
351+ QTouchEvent touchEvent(QEvent::TouchUpdate,
352+ 0 /* device */,
353+ Qt::NoModifier,
354+ Qt::TouchPointPressed | Qt::TouchPointMoved,
355+ touchPoints);
356+ touchRegistry.update(&touchEvent);
357+ }
358+
359+ QCOMPARE(item.unownedTouchEvents.count(), 1);
360+ QCOMPARE(item.unownedTouchEvents[0].touchPoints.count(), 1);
361+ QCOMPARE(item.unownedTouchEvents[0].touchPoints[0].id(), 0);
362+ item.unownedTouchEvents.clear();
363+
364+ touchRegistry.addTouchWatcher(1, &item);
365+
366+ {
367+ QList<QTouchEvent::TouchPoint> touchPoints;
368+ touchPoints.append(QTouchEvent::TouchPoint(0));
369+ touchPoints[0].setState(Qt::TouchPointMoved);
370+ touchPoints.append(QTouchEvent::TouchPoint(1));
371+ touchPoints[1].setState(Qt::TouchPointMoved);
372+ QTouchEvent touchEvent(QEvent::TouchUpdate,
373+ 0 /* device */,
374+ Qt::NoModifier,
375+ Qt::TouchPointMoved,
376+ touchPoints);
377+ touchRegistry.update(&touchEvent);
378+ }
379+
380+ QCOMPARE(item.unownedTouchEvents.count(), 1);
381+ QCOMPARE(item.unownedTouchEvents[0].touchPoints.count(), 2);
382+ QVERIFY(item.unownedTouchEvents[0].containsTouchWithId(0));
383+ QVERIFY(item.unownedTouchEvents[0].containsTouchWithId(1));
384+ item.unownedTouchEvents.clear();
385+
386+ {
387+ QList<QTouchEvent::TouchPoint> touchPoints;
388+ touchPoints.append(QTouchEvent::TouchPoint(0));
389+ touchPoints[0].setState(Qt::TouchPointReleased);
390+ touchPoints.append(QTouchEvent::TouchPoint(1));
391+ touchPoints[1].setState(Qt::TouchPointMoved);
392+ QTouchEvent touchEvent(QEvent::TouchUpdate,
393+ 0 /* device */,
394+ Qt::NoModifier,
395+ Qt::TouchPointReleased | Qt::TouchPointMoved,
396+ touchPoints);
397+ touchRegistry.update(&touchEvent);
398+ }
399+
400+ QCOMPARE(item.unownedTouchEvents.count(), 1);
401+ QCOMPARE(item.unownedTouchEvents[0].touchPoints.count(), 2);
402+ QVERIFY(item.unownedTouchEvents[0].containsTouchWithId(0));
403+ QVERIFY(item.unownedTouchEvents[0].containsTouchWithId(1));
404+ item.unownedTouchEvents.clear();
405+
406+ touchRegistry.removeCandidateOwnerForTouch(0, &item);
407+
408+ {
409+ QList<QTouchEvent::TouchPoint> touchPoints;
410+ touchPoints.append(QTouchEvent::TouchPoint(1));
411+ touchPoints[0].setState(Qt::TouchPointReleased);
412+ QTouchEvent touchEvent(QEvent::TouchEnd,
413+ 0 /* device */,
414+ Qt::NoModifier,
415+ Qt::TouchPointReleased,
416+ touchPoints);
417+ touchRegistry.update(&touchEvent);
418+ }
419+
420+ QCOMPARE(item.unownedTouchEvents.count(), 1);
421+ QCOMPARE(item.unownedTouchEvents[0].touchPoints.count(), 1);
422+ QVERIFY(item.unownedTouchEvents[0].containsTouchWithId(1));
423+ item.unownedTouchEvents.clear();
424+
425+ QVERIFY(touchRegistry.m_touchInfoPool.isEmpty());
426+}
427+
428+/*
429+ Regression test that reproduces a problematic scenario that came up during manual testing.
430+ It reproduces the interaction between TouchRegistry and a DirectionalDragArea
431+ */
432+void tst_TouchRegistry::candidatesAndWatchers_1()
433+{
434+ TouchRegistry touchRegistry;
435+ DummyCandidate item;
436+
437+ {
438+ QList<QTouchEvent::TouchPoint> touchPoints;
439+ touchPoints.append(QTouchEvent::TouchPoint(0));
440+ touchPoints[0].setState(Qt::TouchPointPressed);
441+ QTouchEvent touchEvent(QEvent::TouchBegin,
442+ 0 /* device */,
443+ Qt::NoModifier,
444+ Qt::TouchPointPressed,
445+ touchPoints);
446+ touchRegistry.update(&touchEvent);
447+ }
448+
449+ touchRegistry.addCandidateOwnerForTouch(0, &item);
450+
451+ {
452+ QList<QTouchEvent::TouchPoint> touchPoints;
453+ touchPoints.append(QTouchEvent::TouchPoint(0));
454+ touchPoints[0].setState(Qt::TouchPointMoved);
455+ touchPoints.append(QTouchEvent::TouchPoint(1));
456+ touchPoints[1].setState(Qt::TouchPointPressed);
457+ QTouchEvent touchEvent(QEvent::TouchUpdate,
458+ 0 /* device */,
459+ Qt::NoModifier,
460+ Qt::TouchPointPressed | Qt::TouchPointMoved,
461+ touchPoints);
462+ touchRegistry.update(&touchEvent);
463+ }
464+
465+ touchRegistry.addTouchWatcher(1, &item);
466+
467+ touchRegistry.removeCandidateOwnerForTouch(0, &item);
468+
469+ touchRegistry.addTouchWatcher(0, &item);
470+
471+ {
472+ QList<QTouchEvent::TouchPoint> touchPoints;
473+ touchPoints.append(QTouchEvent::TouchPoint(0));
474+ touchPoints[0].setState(Qt::TouchPointReleased);
475+ touchPoints.append(QTouchEvent::TouchPoint(1));
476+ touchPoints[1].setState(Qt::TouchPointMoved);
477+ QTouchEvent touchEvent(QEvent::TouchUpdate,
478+ 0 /* device */,
479+ Qt::NoModifier,
480+ Qt::TouchPointReleased | Qt::TouchPointMoved,
481+ touchPoints);
482+ touchRegistry.update(&touchEvent);
483+ }
484+
485+ {
486+ QList<QTouchEvent::TouchPoint> touchPoints;
487+ touchPoints.append(QTouchEvent::TouchPoint(1));
488+ touchPoints[0].setState(Qt::TouchPointReleased);
489+ QTouchEvent touchEvent(QEvent::TouchEnd,
490+ 0 /* device */,
491+ Qt::NoModifier,
492+ Qt::TouchPointReleased,
493+ touchPoints);
494+ touchRegistry.update(&touchEvent);
495+ }
496+
497+ QVERIFY(touchRegistry.m_touchInfoPool.isEmpty());
498+
499+ item.unownedTouchEvents.clear();
500+
501+ {
502+ QList<QTouchEvent::TouchPoint> touchPoints;
503+ touchPoints.append(QTouchEvent::TouchPoint(0));
504+ touchPoints[0].setState(Qt::TouchPointPressed);
505+ QTouchEvent touchEvent(QEvent::TouchBegin,
506+ 0 /* device */,
507+ Qt::NoModifier,
508+ Qt::TouchPointPressed,
509+ touchPoints);
510+ touchRegistry.update(&touchEvent);
511+ }
512+
513+ // Haven't made any subscription for that new touch 0.
514+ QCOMPARE(item.unownedTouchEvents.count(), 0);
515+}
516+
517+/*
518+ Regression test that reproduces a problematic scenario that came up during manual testing.
519+ It reproduces the interaction between TouchRegistry, DirectionalDragArea and a TouchGate.
520+ */
521+void tst_TouchRegistry::candidatesAndWatchers_2()
522+{
523+ TouchRegistry touchRegistry;
524+
525+ DummyCandidate directionalDragArea;
526+ directionalDragArea.setObjectName("DirectionalDragArea");
527+
528+ DummyCandidate touchGate;
529+ touchGate.setObjectName("TouchGate");
530+
531+ // [DDA] 1298 TouchBegin (id:0, state:pressed, scenePos:(147,1023))
532+ {
533+ QList<QTouchEvent::TouchPoint> touchPoints;
534+ touchPoints.append(QTouchEvent::TouchPoint(0));
535+ touchPoints[0].setState(Qt::TouchPointPressed);
536+ QTouchEvent touchEvent(QEvent::TouchBegin,
537+ 0 /* device */,
538+ Qt::NoModifier,
539+ Qt::TouchPointPressed,
540+ touchPoints);
541+ touchRegistry.update(&touchEvent);
542+ }
543+
544+ // [TouchRegistry] addCandidateOwnerForTouch id 0 candidate DirectionalDragArea
545+ touchRegistry.addCandidateOwnerForTouch(0, &directionalDragArea);
546+
547+ // [TouchRegistry] requestTouchOwnership id 0 candidate TouchGate
548+ touchRegistry.requestTouchOwnership(0, &touchGate);
549+
550+ //[TouchRegistry] got TouchUpdate (id:0, state:moved, scenePos:(147,1023)) (id:1, state:pressed, scenePos:(327,792))
551+ {
552+ QList<QTouchEvent::TouchPoint> touchPoints;
553+ touchPoints.append(QTouchEvent::TouchPoint(0));
554+ touchPoints[0].setState(Qt::TouchPointMoved);
555+ touchPoints.append(QTouchEvent::TouchPoint(1));
556+ touchPoints[1].setState(Qt::TouchPointPressed);
557+ QTouchEvent touchEvent(QEvent::TouchUpdate,
558+ 0 /* device */,
559+ Qt::NoModifier,
560+ Qt::TouchPointPressed | Qt::TouchPointMoved,
561+ touchPoints);
562+ touchRegistry.update(&touchEvent);
563+ }
564+
565+ // [TouchRegistry] addTouchWatcher id 1 watcher DirectionalDragArea
566+ touchRegistry.addTouchWatcher(1, &directionalDragArea);
567+
568+ // [TouchRegistry] removeCandidateOwnerForTouch id 0 candidate DirectionalDragArea
569+ touchRegistry.removeCandidateOwnerForTouch(0, &directionalDragArea);
570+
571+ //[TouchRegistry] sending TouchWonershipEvent(id = 0 gained) to candidate TouchGate
572+ QCOMPARE(touchGate.ownedTouches.count(), 1);
573+ QVERIFY(touchGate.ownedTouches.contains(0));
574+
575+ // [TouchRegistry] addTouchWatcher id 0 watcher DirectionalDragArea
576+ touchRegistry.addTouchWatcher(0, &directionalDragArea);
577+
578+ // [TouchRegistry] requestTouchOwnership id 1 candidate TouchGate
579+ touchRegistry.requestTouchOwnership(1, &touchGate);
580+
581+ //[TouchRegistry] sending TouchWonershipEvent(id = 1 gained) to candidate TouchGate
582+ QCOMPARE(touchGate.ownedTouches.count(), 2);
583+ QVERIFY(touchGate.ownedTouches.contains(1));
584+
585+ directionalDragArea.unownedTouchEvents.clear();
586+ touchGate.unownedTouchEvents.clear();
587+
588+ //[TouchRegistry] got TouchUpdate (id:0, state:moved, scenePos:(148,1025)) (id:1, state:moved, scenePos:(329,795))
589+ {
590+ QList<QTouchEvent::TouchPoint> touchPoints;
591+ touchPoints.append(QTouchEvent::TouchPoint(0));
592+ touchPoints[0].setState(Qt::TouchPointMoved);
593+ touchPoints.append(QTouchEvent::TouchPoint(1));
594+ touchPoints[1].setState(Qt::TouchPointMoved);
595+ QTouchEvent touchEvent(QEvent::TouchUpdate,
596+ 0 /* device */,
597+ Qt::NoModifier,
598+ Qt::TouchPointMoved,
599+ touchPoints);
600+ touchRegistry.update(&touchEvent);
601+ }
602+
603+ //vvvvv DDA Watchers are being ignored from now on vvvvvvv
604+
605+ QCOMPARE(directionalDragArea.unownedTouchEvents.count(), 1);
606+ QCOMPARE(directionalDragArea.unownedTouchEvents.first().touchPoints.count(), 2);
607+ QVERIFY(directionalDragArea.unownedTouchEvents.first().containsTouchWithId(0));
608+ QVERIFY(directionalDragArea.unownedTouchEvents.first().containsTouchWithId(1));
609+
610+ QVERIFY(touchGate.unownedTouchEvents.isEmpty());
611+
612+ directionalDragArea.unownedTouchEvents.clear();
613+
614+ //[TouchRegistry] got TouchUpdate (id:0, state:moved, scenePos:(151,1029)) (id:1, state:released, scenePos:(329,795))
615+ {
616+ QList<QTouchEvent::TouchPoint> touchPoints;
617+ touchPoints.append(QTouchEvent::TouchPoint(0));
618+ touchPoints[0].setState(Qt::TouchPointMoved);
619+ touchPoints.append(QTouchEvent::TouchPoint(1));
620+ touchPoints[1].setState(Qt::TouchPointReleased);
621+ QTouchEvent touchEvent(QEvent::TouchUpdate,
622+ 0 /* device */,
623+ Qt::NoModifier,
624+ Qt::TouchPointMoved | Qt::TouchPointReleased,
625+ touchPoints);
626+ touchRegistry.update(&touchEvent);
627+ }
628+
629+ QCOMPARE(directionalDragArea.unownedTouchEvents.count(), 1);
630+ QCOMPARE(directionalDragArea.unownedTouchEvents.first().touchPoints.count(), 2);
631+ QVERIFY(directionalDragArea.unownedTouchEvents.first().containsTouchWithId(0));
632+ QVERIFY(directionalDragArea.unownedTouchEvents.first().containsTouchWithId(1));
633+
634+ QVERIFY(touchGate.unownedTouchEvents.isEmpty());
635+
636+ directionalDragArea.unownedTouchEvents.clear();
637+
638+ //[TouchRegistry] got TouchEnd (id:0, state:released, scenePos:(157,1034))
639+ {
640+ QList<QTouchEvent::TouchPoint> touchPoints;
641+ touchPoints.append(QTouchEvent::TouchPoint(0));
642+ touchPoints[0].setState(Qt::TouchPointReleased);
643+ QTouchEvent touchEvent(QEvent::TouchEnd,
644+ 0 /* device */,
645+ Qt::NoModifier,
646+ Qt::TouchPointReleased,
647+ touchPoints);
648+ touchRegistry.update(&touchEvent);
649+ }
650+
651+ QCOMPARE(directionalDragArea.unownedTouchEvents.count(), 1);
652+ QCOMPARE(directionalDragArea.unownedTouchEvents.first().touchPoints.count(), 1);
653+ QVERIFY(directionalDragArea.unownedTouchEvents.first().containsTouchWithId(0));
654+
655+ QVERIFY(touchGate.unownedTouchEvents.isEmpty());
656+}
657+
658+void tst_TouchRegistry::rejectingTouchfterItsEnd()
659+{
660+ TouchRegistry touchRegistry;
661+ DummyCandidate earlyCandidate;
662+ DummyCandidate lateCandidate;
663+
664+ {
665+ QList<QTouchEvent::TouchPoint> touchPoints;
666+ touchPoints.append(QTouchEvent::TouchPoint(0));
667+ touchPoints[0].setState(Qt::TouchPointPressed);
668+ QTouchEvent touchEvent(QEvent::TouchBegin,
669+ 0 /* device */,
670+ Qt::NoModifier,
671+ Qt::TouchPointPressed,
672+ touchPoints);
673+ touchRegistry.update(&touchEvent);
674+ }
675+
676+ touchRegistry.addCandidateOwnerForTouch(0, &earlyCandidate);
677+ touchRegistry.addCandidateOwnerForTouch(0, &lateCandidate);
678+
679+ QVERIFY(earlyCandidate.ownedTouches.isEmpty());
680+ QVERIFY(lateCandidate.ownedTouches.isEmpty());
681+
682+ touchRegistry.requestTouchOwnership(0, &lateCandidate);
683+
684+ QVERIFY(earlyCandidate.ownedTouches.isEmpty());
685+ QVERIFY(lateCandidate.ownedTouches.isEmpty());
686+
687+ {
688+ QList<QTouchEvent::TouchPoint> touchPoints;
689+ touchPoints.append(QTouchEvent::TouchPoint(0));
690+ touchPoints[0].setState(Qt::TouchPointReleased);
691+ QTouchEvent touchEvent(QEvent::TouchEnd,
692+ 0 /* device */,
693+ Qt::NoModifier,
694+ Qt::TouchPointPressed,
695+ touchPoints);
696+ touchRegistry.update(&touchEvent);
697+ }
698+
699+ QVERIFY(earlyCandidate.ownedTouches.isEmpty());
700+ QVERIFY(lateCandidate.ownedTouches.isEmpty());
701+
702+ touchRegistry.removeCandidateOwnerForTouch(0, &earlyCandidate);
703+
704+ QCOMPARE(lateCandidate.ownedTouches.count(), 1);
705+ QCOMPARE(lateCandidate.ownedTouches.contains(0), 1);
706+
707+ // Check that there's no trace left of touch 0 as we no longer need to keep tabs on it.
708+ QVERIFY(!touchRegistry.findTouchInfo(0));
709+}
710+
711+void tst_TouchRegistry::removeOldUndecidedCandidates()
712+{
713+ FakeTimerFactory *fakeTimerFactory = new FakeTimerFactory;
714+ TouchRegistry *touchRegistry = new TouchRegistry(nullptr, fakeTimerFactory);
715+
716+ DummyCandidate undecidedCandidate;
717+ undecidedCandidate.setObjectName("undecided");
718+
719+ DummyCandidate candidateThatWantsTouch;
720+ candidateThatWantsTouch.setObjectName("wantsTouch");
721+
722+ {
723+ QList<QTouchEvent::TouchPoint> touchPoints;
724+ touchPoints.append(QTouchEvent::TouchPoint(0));
725+ touchPoints[0].setState(Qt::TouchPointPressed);
726+ QTouchEvent touchEvent(QEvent::TouchBegin,
727+ 0 /* device */,
728+ Qt::NoModifier,
729+ Qt::TouchPointPressed,
730+ touchPoints);
731+ touchRegistry->update(&touchEvent);
732+ }
733+
734+ touchRegistry->addCandidateOwnerForTouch(0, &undecidedCandidate);
735+
736+ touchRegistry->requestTouchOwnership(0, &candidateThatWantsTouch);
737+
738+ QVERIFY(undecidedCandidate.ownedTouches.isEmpty());
739+ QVERIFY(undecidedCandidate.lostTouches.isEmpty());
740+ QVERIFY(candidateThatWantsTouch.ownedTouches.isEmpty());
741+ QVERIFY(candidateThatWantsTouch.lostTouches.isEmpty());
742+
743+ // Simulate that enough time has passed to cause the CandidateInactivityTimer to timeout,
744+ // making TouchRegistry consider that undecidedCantidate defaulted.
745+ fakeTimerFactory->makeRunningTimersTimeout();
746+
747+ QVERIFY(undecidedCandidate.ownedTouches.isEmpty());
748+ QVERIFY(undecidedCandidate.lostTouches.contains(0));
749+ QVERIFY(candidateThatWantsTouch.ownedTouches.contains(0));
750+ QVERIFY(candidateThatWantsTouch.lostTouches.isEmpty());
751+
752+ delete touchRegistry;
753+}
754+
755+////////////// TouchMemento //////////
756+
757+TouchMemento::TouchMemento(const QTouchEvent *touchEvent)
758+ : touchPointStates(touchEvent->touchPointStates()), touchPoints(touchEvent->touchPoints())
759+{
760+}
761+
762+bool TouchMemento::containsTouchWithId(int touchId) const
763+{
764+ for (int i = 0; i < touchPoints.count(); ++i) {
765+ if (touchPoints.at(i).id() == touchId) {
766+ return true;
767+ }
768+ }
769+ return false;
770+}
771+
772+////////////// DummyCandidate //////////
773+
774+bool DummyCandidate::event(QEvent *e)
775+{
776+ if (e->type() == TouchOwnershipEvent::touchOwnershipEventType()) {
777+ TouchOwnershipEvent *touchOwnershipEvent = static_cast<TouchOwnershipEvent *>(e);
778+
779+ // NB: Cannot use QVERIFY here because the macro doesn't return a boolean and is
780+ // meant for use only directly in the body of a test function
781+ if (ownedTouches.contains(touchOwnershipEvent->touchId()))
782+ qFatal("Sent ownership event for a touch that is already owned.");
783+ if (lostTouches.contains(touchOwnershipEvent->touchId()))
784+ qFatal("Sent ownership event for a touch that has already been lost.");
785+
786+ if (touchOwnershipEvent->gained()) {
787+ ownedTouches.insert(touchOwnershipEvent->touchId());
788+ } else {
789+ lostTouches.insert(touchOwnershipEvent->touchId());
790+ }
791+ return true;
792+ } else if (e->type() == UnownedTouchEvent::unownedTouchEventType()) {
793+ UnownedTouchEvent *unownedTouchEvent = static_cast<UnownedTouchEvent *>(e);
794+ unownedTouchEvents.append(TouchMemento(unownedTouchEvent->touchEvent()));
795+ return true;
796+ } else {
797+ return QObject::event(e);
798+ }
799+}
800+
801+QTEST_GUILESS_MAIN(tst_TouchRegistry)
802+
803+#include "tst_TouchRegistry.moc"
tests/mocks/Unity/Application/CMakeLists.txt+1
@@ -13,6 +13,7 @@ set(FakeUnityApplicationQml_SOURCES
13 SessionManager.cpp
13 SessionManager.cpp
14 SurfaceManager.cpp
14 SurfaceManager.cpp
15 SessionModel.h
15 SessionModel.h
16+ UbuntuKeyboardInfo.cpp
16 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationInfoInterface.h
17 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationInfoInterface.h
17 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationManagerInterface.h
18 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationManagerInterface.h
18)
19)
tests/mocks/Unity/Application/MirSurfaceItem.cpp+18−22
@@ -40,6 +40,8 @@ MirSurfaceItem::MirSurfaceItem(const QString& name,
40 , m_state(state)
40 , m_state(state)
41 , m_live(true)
41 , m_live(true)
42 , m_orientation(Qt::PortraitOrientation)
42 , m_orientation(Qt::PortraitOrientation)
43+ , m_touchPressCount(0)
44+ , m_touchReleaseCount(0)
43 , m_qmlItem(nullptr)
45 , m_qmlItem(nullptr)
44 , m_screenshotUrl(screenshot)
46 , m_screenshotUrl(screenshot)
45{
47{
@@ -144,24 +146,12 @@ void MirSurfaceItem::setLive(bool live)
144 }
146 }
145}
147}
146
148
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−
157void MirSurfaceItem::onFocusChanged()
149void MirSurfaceItem::onFocusChanged()
158{
150{
159− QQmlProperty prop(m_qmlItem, "wantInputMethod");
160− bool wantInputMethod = prop.read().toBool();
161−
162− if (!hasFocus() && wantInputMethod) {
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.
163− Q_EMIT inputMethodDismissed();
154+ //Q_EMIT inputMethodDismissed();
164− prop.write(QVariant::fromValue(false));
165 }
155 }
166}
156}
167
157
@@ -194,12 +184,18 @@ void MirSurfaceItem::createQmlContentItem()
194 QQmlProperty screenshotSource(m_qmlItem, "screenshotSource");
184 QQmlProperty screenshotSource(m_qmlItem, "screenshotSource");
195 screenshotSource.write(QVariant::fromValue(m_screenshotUrl));
185 screenshotSource.write(QVariant::fromValue(m_screenshotUrl));
196 }
186 }
187+}
197
188
198− {
189+void MirSurfaceItem::touchEvent(QTouchEvent * event)
199− QQmlProperty prop(m_qmlItem, "wantInputMethod");
190+{
200− if (prop.type() == QQmlProperty::Property) {
191+ if (event->touchPointStates() & Qt::TouchPointPressed) {
201− bool ok = prop.connectNotifySignal(this, SLOT(onQmlWantInputMethodChanged()));
192+ ++m_touchPressCount;
202− if (!ok) qCritical("MirSurfaceItem: failed to connect to wantInputMethod notify signal");
193+ Q_EMIT touchPressCountChanged(m_touchPressCount);
203− }
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);
204 }
200 }
205}
201}
tests/mocks/Unity/Application/MirSurfaceItem.h+18−1
@@ -36,6 +36,11 @@ class MirSurfaceItem : public QQuickItem
36 Q_PROPERTY(bool live READ live NOTIFY liveChanged)
36 Q_PROPERTY(bool live READ live NOTIFY liveChanged)
37 Q_PROPERTY(Qt::ScreenOrientation orientation READ orientation WRITE setOrientation NOTIFY orientationChanged DESIGNABLE false)
37 Q_PROPERTY(Qt::ScreenOrientation orientation READ orientation WRITE setOrientation NOTIFY orientationChanged DESIGNABLE false)
38
38
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+
39public:
44public:
40 enum Type {
45 enum Type {
41 Normal,
46 Normal,
@@ -73,6 +78,12 @@ public:
73 void setScreenshot(const QUrl& screenshot);
78 void setScreenshot(const QUrl& screenshot);
74 void setLive(bool live);
79 void setLive(bool live);
75
80
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+
76 Q_INVOKABLE void setState(State newState);
87 Q_INVOKABLE void setState(State newState);
77 Q_INVOKABLE void release();
88 Q_INVOKABLE void release();
78
89
@@ -81,6 +92,8 @@ Q_SIGNALS:
81 void stateChanged(State);
92 void stateChanged(State);
82 void liveChanged(bool live);
93 void liveChanged(bool live);
83 void orientationChanged();
94 void orientationChanged();
95+ void touchPressCountChanged(int count);
96+ void touchReleaseCountChanged(int count);
84
97
85 void inputMethodRequested();
98 void inputMethodRequested();
86 void inputMethodDismissed();
99 void inputMethodDismissed();
@@ -88,10 +101,12 @@ Q_SIGNALS:
88 // internal mock use
101 // internal mock use
89 void deregister();
102 void deregister();
90
103
104+protected:
105+ void touchEvent(QTouchEvent * event) override;
106+
91private Q_SLOTS:
107private Q_SLOTS:
92 void onFocusChanged();
108 void onFocusChanged();
93 void onComponentStatusChanged(QQmlComponent::Status status);
109 void onComponentStatusChanged(QQmlComponent::Status status);
94− void onQmlWantInputMethodChanged();
95
110
96private:
111private:
97 explicit MirSurfaceItem(const QString& name,
112 explicit MirSurfaceItem(const QString& name,
@@ -110,6 +125,8 @@ private:
110 State m_state;
125 State m_state;
111 bool m_live;
126 bool m_live;
112 Qt::ScreenOrientation m_orientation;
127 Qt::ScreenOrientation m_orientation;
128+ int m_touchPressCount;
129+ int m_touchReleaseCount;
113
130
114 QQmlComponent *m_qmlContentComponent;
131 QQmlComponent *m_qmlContentComponent;
115 QQuickItem *m_qmlItem;
132 QQuickItem *m_qmlItem;
tests/mocks/Unity/Application/MirSurfaceItem.qml−12
@@ -17,7 +17,6 @@
17import QtQuick 2.0
17import QtQuick 2.0
18
18
19Rectangle {
19Rectangle {
20− objectName: "fakeSurfaceQML"
21 id: root
20 id: root
22 color: "pink"
21 color: "pink"
23
22
@@ -38,11 +37,6 @@ Rectangle {
38 property alias screenshotSource: screenshotImage.source
37 property alias screenshotSource: screenshotImage.source
39 property int orientation: Qt.PortraitOrientation
38 property int orientation: Qt.PortraitOrientation
40
39
41− property bool wantInputMethod: false
42−
43− property int touchPressCount: 0
44− property int touchReleaseCount: 0
45−
46 Image {
40 Image {
47 id: screenshotImage
41 id: screenshotImage
48 anchors.fill: parent
42 anchors.fill: parent
@@ -58,10 +52,4 @@ Rectangle {
58 minimumPixelSize: 10; font.pixelSize: 200
52 minimumPixelSize: 10; font.pixelSize: 200
59 verticalAlignment: Text.AlignVCenter
53 verticalAlignment: Text.AlignVCenter
60 }
54 }
61−
62− MultiPointTouchArea {
63− anchors.fill: parent
64− onPressed: { root.wantInputMethod = true; root.touchPressCount++; }
65− onReleased: { root.touchReleaseCount++; }
66− }
67}
55}
tests/mocks/Unity/Application/UbuntuKeyboardInfo.cppnew file+28
@@ -0,0 +1,28 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software: you can redistribute it and/or modify it under
5+ * the terms of the GNU Lesser General Public License version 3, as published by
6+ * the Free Software Foundation.
7+ *
8+ * This program is distributed in the hope that it will be useful, but WITHOUT
9+ * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
10+ * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11+ * Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "UbuntuKeyboardInfo.h"
18+
19+UbuntuKeyboardInfo *UbuntuKeyboardInfo::m_instance = nullptr;
20+
21+UbuntuKeyboardInfo::UbuntuKeyboardInfo(QObject *parent)
22+ : QObject(parent),
23+ m_x(0),
24+ m_y(0),
25+ m_width(200),
26+ m_height(200)
27+{
28+}
tests/mocks/Unity/Application/UbuntuKeyboardInfo.hnew file+60
@@ -0,0 +1,60 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software: you can redistribute it and/or modify it under
5+ * the terms of the GNU Lesser General Public License version 3, as published by
6+ * the Free Software Foundation.
7+ *
8+ * This program is distributed in the hope that it will be useful, but WITHOUT
9+ * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
10+ * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11+ * Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef UBUNTU_KEYBOARD_INFO_H
18+#define UBUNTU_KEYBOARD_INFO_H
19+
20+#include <QLocalSocket>
21+#include <QTimer>
22+
23+class UbuntuKeyboardInfo : public QObject {
24+ Q_OBJECT
25+ Q_PROPERTY(qreal x READ x NOTIFY xChanged)
26+ Q_PROPERTY(qreal y READ y NOTIFY yChanged)
27+ Q_PROPERTY(qreal width READ width NOTIFY widthChanged)
28+ Q_PROPERTY(qreal height READ height NOTIFY heightChanged)
29+public:
30+ UbuntuKeyboardInfo(QObject *parent = 0);
31+ virtual ~UbuntuKeyboardInfo() {}
32+ qreal x() const { return m_x; }
33+ qreal y() const { return m_y; }
34+ qreal width() const { return m_width; }
35+ qreal height() const { return m_height; }
36+
37+ static UbuntuKeyboardInfo *singleton() {
38+ if (!m_instance) {
39+ m_instance = new UbuntuKeyboardInfo;
40+ }
41+ return m_instance;
42+ }
43+
44+Q_SIGNALS:
45+ void xChanged(qreal x);
46+ void yChanged(qreal y);
47+ void widthChanged(qreal width);
48+ void heightChanged(qreal height);
49+
50+private:
51+ QLocalSocket m_socket;
52+ qint32 m_x;
53+ qint32 m_y;
54+ qint32 m_width;
55+ qint32 m_height;
56+
57+ static UbuntuKeyboardInfo *m_instance;
58+};
59+
60+#endif // UBUNTU_KEYBOARD_INFO_H
tests/mocks/Unity/Application/plugin.cpp+7
@@ -24,6 +24,7 @@
24#include "SurfaceManager.h"
24#include "SurfaceManager.h"
25#include "SessionManager.h"
25#include "SessionManager.h"
26#include "ApplicationTestInterface.h"
26#include "ApplicationTestInterface.h"
27+#include "UbuntuKeyboardInfo.h"
27
28
28#include <qqml.h>
29#include <qqml.h>
29#include <QQmlEngine>
30#include <QQmlEngine>
@@ -46,6 +47,11 @@ static QObject* sessionManagerSingleton(QQmlEngine* engine, QJSEngine* scriptEng
46 return SessionManager::singleton();
47 return SessionManager::singleton();
47}
48}
48
49
50+static QObject* ubuntuKeyboardInfoSingleton(QQmlEngine*, QJSEngine*) {
51+ return UbuntuKeyboardInfo::singleton();
52+}
53+
54+
49ApplicationTestInterface* s_appTestInterface = nullptr;
55ApplicationTestInterface* s_appTestInterface = nullptr;
50
56
51static QObject* applicationTestInterface(QQmlEngine* engine, QJSEngine* scriptEngine) {
57static QObject* applicationTestInterface(QQmlEngine* engine, QJSEngine* scriptEngine) {
@@ -76,6 +82,7 @@ void FakeUnityApplicationQmlPlugin::registerTypes(const char *uri)
76 qmlRegisterSingletonType<SurfaceManager>(uri, 0, 1, "SurfaceManager", surfaceManagerSingleton);
82 qmlRegisterSingletonType<SurfaceManager>(uri, 0, 1, "SurfaceManager", surfaceManagerSingleton);
77 qmlRegisterSingletonType<SessionManager>(uri, 0, 1, "SessionManager", sessionManagerSingleton);
83 qmlRegisterSingletonType<SessionManager>(uri, 0, 1, "SessionManager", sessionManagerSingleton);
78 qmlRegisterSingletonType<ApplicationTestInterface>(uri, 0, 1, "ApplicationTest", applicationTestInterface);
84 qmlRegisterSingletonType<ApplicationTestInterface>(uri, 0, 1, "ApplicationTest", applicationTestInterface);
85+ qmlRegisterSingletonType<UbuntuKeyboardInfo>(uri, 0, 1, "UbuntuKeyboardInfo", ubuntuKeyboardInfoSingleton);
79
86
80 qRegisterMetaType<MirSurfaceItem::Type>("MirSurfaceItem::Type");
87 qRegisterMetaType<MirSurfaceItem::Type>("MirSurfaceItem::Type");
81 qRegisterMetaType<MirSurfaceItem::State>("MirSurfaceItem::State");
88 qRegisterMetaType<MirSurfaceItem::State>("MirSurfaceItem::State");
tests/plugins/Ubuntu/Gestures/CMakeLists.txt+9−2
@@ -1,7 +1,13 @@
1include(QmlTest)
1include(QmlTest)
2
2
3+# DANGER! DANGER! Using Qt's private API!
4+include_directories(
5+ ${Qt5Quick_PRIVATE_INCLUDE_DIRS}
6+)
7+
3include_directories(
8include_directories(
4 ${CMAKE_SOURCE_DIR}/plugins/Ubuntu/Gestures
9 ${CMAKE_SOURCE_DIR}/plugins/Ubuntu/Gestures
10+ ${CMAKE_SOURCE_DIR}/libs/UbuntuGestures
5 ${CMAKE_CURRENT_BINARY_DIR}
11 ${CMAKE_CURRENT_BINARY_DIR}
6 ${Qt5Gui_PRIVATE_INCLUDE_DIRS}
12 ${Qt5Gui_PRIVATE_INCLUDE_DIRS}
7 )
13 )
@@ -17,7 +23,7 @@ add_definitions(-DUBUNTU_GESTURES_PLUGIN_DIR="${CMAKE_BINARY_DIR}/plugins")
17macro(add_gesture_ui_test CLASSNAME)
23macro(add_gesture_ui_test CLASSNAME)
18 add_executable(${CLASSNAME}TestExec tst_${CLASSNAME}.cpp GestureTest.cpp)
24 add_executable(${CLASSNAME}TestExec tst_${CLASSNAME}.cpp GestureTest.cpp)
19 qt5_use_modules(${CLASSNAME}TestExec Test Core Qml Gui Quick)
25 qt5_use_modules(${CLASSNAME}TestExec Test Core Qml Gui Quick)
20− target_link_libraries(${CLASSNAME}TestExec UbuntuGestureQml)
26+ target_link_libraries(${CLASSNAME}TestExec UbuntuGesturesQml UbuntuGestures)
21
27
22 add_binary_qml_test(${CLASSNAME} ${CMAKE_BINARY_DIR}/plugins/Ubuntu/Gestures UbuntuGesturesTestQmlFiles "")
28 add_binary_qml_test(${CLASSNAME} ${CMAKE_BINARY_DIR}/plugins/Ubuntu/Gestures UbuntuGesturesTestQmlFiles "")
23 add_manual_qml_test(. ${CLASSNAME} IMPORT_PATHS ${CMAKE_BINARY_DIR}/plugins)
29 add_manual_qml_test(. ${CLASSNAME} IMPORT_PATHS ${CMAKE_BINARY_DIR}/plugins)
@@ -34,10 +40,11 @@ macro(add_gesture_test CLASSNAME)
34
40
35 add_executable(${CLASSNAME}TestExec tst_${CLASSNAME}.cpp)
41 add_executable(${CLASSNAME}TestExec tst_${CLASSNAME}.cpp)
36 qt5_use_modules(${CLASSNAME}TestExec Test Core)
42 qt5_use_modules(${CLASSNAME}TestExec Test Core)
37− target_link_libraries(${CLASSNAME}TestExec UbuntuGestureQml)
43+ target_link_libraries(${CLASSNAME}TestExec UbuntuGesturesQml)
38endmacro(add_gesture_test)
44endmacro(add_gesture_test)
39
45
40add_gesture_ui_test(DirectionalDragArea)
46add_gesture_ui_test(DirectionalDragArea)
41add_gesture_ui_test(PressedOutsideNotifier)
47add_gesture_ui_test(PressedOutsideNotifier)
48+add_gesture_ui_test(TouchGate)
42add_gesture_test(Damper)
49add_gesture_test(Damper)
43add_gesture_test(AxisVelocityCalculator)
50add_gesture_test(AxisVelocityCalculator)
tests/plugins/Ubuntu/Gestures/RightwardsLauncher.qml+3
@@ -46,6 +46,9 @@ Item {
46 id: dragArea
46 id: dragArea
47 objectName: "hpDragArea"
47 objectName: "hpDragArea"
48
48
49+ // give some room for items to be dynamically stacked right behind him
50+ z: 10.0
51+
49 width: units.gu(5)
52 width: units.gu(5)
50
53
51 direction: Direction.Rightwards
54 direction: Direction.Rightwards
tests/plugins/Ubuntu/Gestures/touchGateExample.qmlnew file+27
@@ -0,0 +1,27 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.0
18+import Ubuntu.Gestures 0.1
19+
20+Rectangle {
21+ width: 720; height: 720
22+
23+ TouchGate {
24+ anchors.fill: parent
25+ objectName: "touchGate"
26+ }
27+}
tests/plugins/Ubuntu/Gestures/tst_DirectionalDragArea.cpp+491−14
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright (C) 2013 Canonical, Ltd.
2+ * Copyright (C) 2013-2014 Canonical, Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
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
5 * it under the terms of the GNU General Public License as published by
@@ -19,37 +19,63 @@
19#include <QtQuick/QQuickView>
19#include <QtQuick/QQuickView>
20#include <QtQml/QQmlEngine>
20#include <QtQml/QQmlEngine>
21#include <QPointer>
21#include <QPointer>
22+#include <private/qquickmousearea_p.h>
23+
24+// C++ std lib
25+#include <functional>
22
26
23#include <DirectionalDragArea.h>
27#include <DirectionalDragArea.h>
28+#include <TouchRegistry.h>
24
29
25#include "GestureTest.h"
30#include "GestureTest.h"
26
31
27using namespace UbuntuGestures;
32using namespace UbuntuGestures;
28
33
29−class FakeTimer : public AbstractTimer
34+class TouchMemento {
35+public:
36+ TouchMemento(const QTouchEvent *touchEvent);
37+ Qt::TouchPointStates touchPointStates;
38+ QList<QTouchEvent::TouchPoint> touchPoints;
39+
40+ bool containsTouchWithId(int touchId) const;
41+};
42+
43+class DummyItem : public QQuickItem
30{
44{
31 Q_OBJECT
45 Q_OBJECT
32public:
46public:
47+ DummyItem(QQuickItem *parent = 0);
48+
49+ QList<TouchMemento> touchEvents;
50+ std::function<void(QTouchEvent*)> touchEventHandler;
51+protected:
52+ void touchEvent(QTouchEvent *event) override;
53+private:
54+ static void defaultTouchEventHandler(QTouchEvent *event);
55+};
56+
57+class ComplexFakeTimer : public FakeTimer
58+{
59+ Q_OBJECT
60+public:
33− FakeTimer(const SharedTimeSource &timeSource, QObject *parent = 0)
61+ ComplexFakeTimer(const SharedTimeSource &timeSource, QObject *parent = 0)
34− : UbuntuGestures::AbstractTimer(parent),
62+ : FakeTimer(parent),
35 m_timeSource(timeSource)
63 m_timeSource(timeSource)
36 {}
64 {}
37
65
38− int interval() const override { return m_interval; }
39− void setInterval(int msecs) override { m_interval = msecs; }
40 void start() override {
66 void start() override {
41 AbstractTimer::start();
67 AbstractTimer::start();
42− m_nextTimeoutTime = m_timeSource->msecsSinceReference() + (qint64)m_interval;
68+ m_nextTimeoutTime = m_timeSource->msecsSinceReference() + (qint64)interval();
43 }
69 }
44
70
45 void emitTimeout() {
71 void emitTimeout() {
46− m_nextTimeoutTime += m_interval;
72+ m_nextTimeoutTime += interval();
47 Q_EMIT timeout();
73 Q_EMIT timeout();
48 }
74 }
49
75
50 qint64 nextTimeoutTime() const { return m_nextTimeoutTime; }
76 qint64 nextTimeoutTime() const { return m_nextTimeoutTime; }
77+
51private:
78private:
52− int m_interval;
53 SharedTimeSource m_timeSource;
79 SharedTimeSource m_timeSource;
54 qint64 m_nextTimeoutTime;
80 qint64 m_nextTimeoutTime;
55};
81};
@@ -62,6 +88,30 @@ public:
62 qint64 m_msecsSinceReference;
88 qint64 m_msecsSinceReference;
63};
89};
64
90
91+/*
92+ QQuickMouseArea::canceled() signal is not registered in the meta object system.
93+ So using a QSignalSpy to track it won't work. Thus the only way to connect to it
94+ is using its method address directly.
95+ */
96+class MouseAreaSpy : public QObject
97+{
98+ Q_OBJECT
99+public:
100+ MouseAreaSpy(QQuickMouseArea *mouseArea)
101+ : canceledCount(0)
102+ {
103+ connect(mouseArea, &QQuickMouseArea::canceled,
104+ this, &MouseAreaSpy::onMouseAreaCanceled);
105+ }
106+
107+ int canceledCount;
108+
109+private Q_SLOTS:
110+ void onMouseAreaCanceled() {
111+ ++canceledCount;
112+ }
113+};
114+
65class tst_DirectionalDragArea: public GestureTest
115class tst_DirectionalDragArea: public GestureTest
66{
116{
67 Q_OBJECT
117 Q_OBJECT
@@ -87,10 +137,20 @@ private Q_SLOTS:
87 void sceneDistance();
137 void sceneDistance();
88 void sceneDistance_data();
138 void sceneDistance_data();
89 void disabledWhileDragging();
139 void disabledWhileDragging();
140+ void oneFingerDownFollowedByLateSecondFingerDown();
141+ void givesUpWhenLosesTouch();
142+ void threeFingerDrag();
143+ void immediateRecognitionWhenConstraintsDisabled();
144+ void withdrawTouchOwnershipCandidacyIfDisabledDuringRecognition();
145+ void withdrawTouchOwnershipCandidacyIfDisabledDuringRecognition_data();
146+ void tappedSignal();
147+ void tappedSignal_data();
148+ void gettingTouchOwnershipMakesMouseAreaBehindGetCanceled();
90
149
91private:
150private:
92− void passTime(qint64 timeSpan);
151+ void passTime(qint64 timeSpanMs);
152+ TouchRegistry *touchRegistry;
93− FakeTimer *fakeTimer;
153+ ComplexFakeTimer *fakeTimer;
94 QSharedPointer<FakeTimeSource> fakeTimeSource;
154 QSharedPointer<FakeTimeSource> fakeTimeSource;
95};
155};
96
156
@@ -111,12 +171,19 @@ void tst_DirectionalDragArea::init()
111 QTRY_COMPARE(m_view->width(), (int)m_view->rootObject()->width());
171 QTRY_COMPARE(m_view->width(), (int)m_view->rootObject()->width());
112 QTRY_COMPARE(m_view->height(), (int)m_view->rootObject()->height());
172 QTRY_COMPARE(m_view->height(), (int)m_view->rootObject()->height());
113
173
174+ touchRegistry = new TouchRegistry;
175+ m_view->installEventFilter(touchRegistry);
176+
114 fakeTimeSource.reset(new FakeTimeSource);
177 fakeTimeSource.reset(new FakeTimeSource);
115− fakeTimer = new FakeTimer(fakeTimeSource);
178+ fakeTimer = new ComplexFakeTimer(fakeTimeSource);
116}
179}
117
180
118void tst_DirectionalDragArea::cleanup()
181void tst_DirectionalDragArea::cleanup()
119{
182{
183+ m_view->removeEventFilter(touchRegistry);
184+ delete touchRegistry;
185+ touchRegistry = nullptr;
186+
120 delete fakeTimer;
187 delete fakeTimer;
121 fakeTimer = 0;
188 fakeTimer = 0;
122
189
@@ -125,9 +192,9 @@ void tst_DirectionalDragArea::cleanup()
125 GestureTest::cleanup();
192 GestureTest::cleanup();
126}
193}
127
194
128−void tst_DirectionalDragArea::passTime(qint64 timeSpan)
195+void tst_DirectionalDragArea::passTime(qint64 timeSpanMs)
129{
196{
130− qint64 finalTime = fakeTimeSource->m_msecsSinceReference + timeSpan;
197+ qint64 finalTime = fakeTimeSource->m_msecsSinceReference + timeSpanMs;
131
198
132 if (fakeTimer->isRunning() && finalTime >= fakeTimer->nextTimeoutTime()) {
199 if (fakeTimer->isRunning() && finalTime >= fakeTimer->nextTimeoutTime()) {
133 fakeTimeSource->m_msecsSinceReference = fakeTimer->nextTimeoutTime();
200 fakeTimeSource->m_msecsSinceReference = fakeTimer->nextTimeoutTime();
@@ -849,6 +916,416 @@ void tst_DirectionalDragArea::disabledWhileDragging()
849 QTest::touchEvent(m_view, m_device).release(0, touchPoint.toPoint());
916 QTest::touchEvent(m_view, m_device).release(0, touchPoint.toPoint());
850}
917}
851
918
919+void tst_DirectionalDragArea::oneFingerDownFollowedByLateSecondFingerDown()
920+{
921+ DirectionalDragArea *edgeDragArea =
922+ m_view->rootObject()->findChild<DirectionalDragArea*>("hpDragArea");
923+ Q_ASSERT(edgeDragArea != 0);
924+ edgeDragArea->setRecognitionTimer(fakeTimer);
925+ edgeDragArea->setTimeSource(fakeTimeSource);
926+
927+ // Disable some constraints we're not interested in
928+ edgeDragArea->setMaxSilenceTime(60 * 1000);
929+ edgeDragArea->setMinSpeed(0);
930+
931+ // And ensure others have the values we want
932+ edgeDragArea->setCompositionTime(60);
933+
934+ // Put an item right behind edgeDragArea to receive the touches ignored by it
935+ DummyItem *dummyItem = new DummyItem;
936+ dummyItem->setParentItem(edgeDragArea->parentItem());
937+ dummyItem->setX(edgeDragArea->x());
938+ dummyItem->setY(edgeDragArea->y());
939+ dummyItem->setZ(edgeDragArea->z() - 1.0);
940+ dummyItem->setWidth(edgeDragArea->width());
941+ dummyItem->setHeight(edgeDragArea->height());
942+
943+ // Make touches evenly spaced along the edgeDragArea
944+ QPoint touch0Pos(edgeDragArea->width()/2.0f, m_view->height()*0.33f);
945+ QPoint touch1Pos(edgeDragArea->width()/2.0f, m_view->height()*0.66f);
946+
947+ QTest::touchEvent(m_view, m_device).press(0, touch0Pos);
948+
949+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Undecided);
950+
951+ // We are now going to be way beyond compositionTime
952+ passTime(edgeDragArea->compositionTime()*3);
953+
954+ QTest::touchEvent(m_view, m_device)
955+ .move(0, touch0Pos)
956+ .press(1, touch1Pos);
957+
958+ // A new touch has come, but as it can't be composed with touch 0, it should be
959+ // ignored/rejected by the DirectionalDragArea
960+ // Therefore the last event received by dummyItem must have both touch points (0 and 1)
961+ {
962+ TouchMemento &touchMemento = dummyItem->touchEvents.last();
963+ QCOMPARE(touchMemento.touchPoints.count(), 2);
964+ QVERIFY(touchMemento.containsTouchWithId(0));
965+ QVERIFY(touchMemento.containsTouchWithId(1));
966+ }
967+
968+ passTime(30);
969+
970+ QTest::touchEvent(m_view, m_device)
971+ .move(0, touch0Pos)
972+ .move(1, touch1Pos);
973+
974+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Undecided);
975+
976+ passTime(5);
977+
978+ QTest::touchEvent(m_view, m_device)
979+ .release(0, touch0Pos)
980+ .move(1, touch1Pos);
981+
982+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch);
983+
984+ passTime(5);
985+
986+ QTest::touchEvent(m_view, m_device)
987+ .release(1, touch1Pos);
988+
989+ // Shouldn't be keepping info about touches that no longer exist or interest us
990+ QVERIFY(edgeDragArea->m_activeTouches.isEmpty());
991+
992+ delete dummyItem;
993+}
994+
995+void tst_DirectionalDragArea::givesUpWhenLosesTouch()
996+{
997+ DirectionalDragArea *edgeDragArea =
998+ m_view->rootObject()->findChild<DirectionalDragArea*>("hpDragArea");
999+ Q_ASSERT(edgeDragArea != 0);
1000+ edgeDragArea->setRecognitionTimer(fakeTimer);
1001+ edgeDragArea->setTimeSource(fakeTimeSource);
1002+
1003+ // Disable some constraints we're not interested in
1004+ edgeDragArea->setMaxSilenceTime(60 * 1000);
1005+ edgeDragArea->setMinSpeed(0);
1006+
1007+ // Put an item right in front of edgeDragArea
1008+ DummyItem *dummyItem = new DummyItem(edgeDragArea->parentItem());
1009+ dummyItem->setX(edgeDragArea->x());
1010+ dummyItem->setY(edgeDragArea->y());
1011+ dummyItem->setZ(edgeDragArea->z() + 1.0);
1012+ dummyItem->setWidth(edgeDragArea->width());
1013+ dummyItem->setHeight(edgeDragArea->height());
1014+
1015+ QPoint touchPos(edgeDragArea->width()/2.0f, m_view->height()/2.0f);
1016+
1017+ dummyItem->touchEventHandler = [&](QTouchEvent *event) {
1018+ touchRegistry->addCandidateOwnerForTouch(0, dummyItem);
1019+ event->ignore();
1020+ };
1021+
1022+ QTest::touchEvent(m_view, m_device).press(0, touchPos);
1023+
1024+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Undecided);
1025+
1026+ touchRegistry->requestTouchOwnership(0, dummyItem);
1027+
1028+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch);
1029+
1030+ dummyItem->grabTouchPoints({0});
1031+ dummyItem->touchEventHandler = [&](QTouchEvent *event) { event->accept(); };
1032+
1033+ passTime(5);
1034+ QTest::touchEvent(m_view, m_device).release(0, touchPos);
1035+
1036+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch);
1037+
1038+ QVERIFY(edgeDragArea->m_activeTouches.isEmpty());
1039+}
1040+
1041+void tst_DirectionalDragArea::threeFingerDrag()
1042+{
1043+ DirectionalDragArea *edgeDragArea =
1044+ m_view->rootObject()->findChild<DirectionalDragArea*>("hpDragArea");
1045+ Q_ASSERT(edgeDragArea != 0);
1046+ edgeDragArea->setRecognitionTimer(fakeTimer);
1047+ edgeDragArea->setTimeSource(fakeTimeSource);
1048+
1049+ // Disable some constraints we're not interested in
1050+ edgeDragArea->setMaxSilenceTime(60 * 1000);
1051+ edgeDragArea->setMinSpeed(0);
1052+
1053+ // And ensure others have the values we want
1054+ edgeDragArea->setCompositionTime(60);
1055+
1056+ // Make touches evenly spaced along the edgeDragArea
1057+ QPoint touch0Pos(edgeDragArea->width()/2.0f, m_view->height()*0.25f);
1058+ QPoint touch1Pos(edgeDragArea->width()/2.0f, m_view->height()*0.50f);
1059+ QPoint touch2Pos(edgeDragArea->width()/2.0f, m_view->height()*0.75f);
1060+
1061+ QTest::touchEvent(m_view, m_device)
1062+ .press(0, touch0Pos);
1063+
1064+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Undecided);
1065+
1066+ passTime(5);
1067+ QTest::touchEvent(m_view, m_device)
1068+ .move(0, touch0Pos)
1069+ .press(1, touch1Pos);
1070+
1071+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch);
1072+
1073+ passTime(5);
1074+ QTest::touchEvent(m_view, m_device)
1075+ .move(0, touch0Pos)
1076+ .move(1, touch1Pos)
1077+ .press(2, touch2Pos);
1078+
1079+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch);
1080+
1081+ passTime(10);
1082+ QTest::touchEvent(m_view, m_device)
1083+ .move(0, touch0Pos)
1084+ .move(1, touch1Pos)
1085+ .move(2, touch2Pos);
1086+
1087+ passTime(200);
1088+ QTest::touchEvent(m_view, m_device)
1089+ .move(0, touch0Pos)
1090+ .release(1, touch1Pos)
1091+ .move(2, touch2Pos);
1092+
1093+ passTime(10);
1094+ QTest::touchEvent(m_view, m_device)
1095+ .move(0, touch0Pos)
1096+ .release(2, touch2Pos);
1097+
1098+ passTime(5);
1099+ QTest::touchEvent(m_view, m_device)
1100+ .release(0, touch0Pos);
1101+
1102+ // Shouldn't be keepping info about touches that no longer exist or interest us
1103+ QVERIFY(edgeDragArea->m_activeTouches.isEmpty());
1104+}
1105+
1106+/*
1107+ If all the relevant gesture recognition constraints/parameters have been disabled,
1108+ it means that the gesture recognition itself has been disabled and DirectionalDragArea
1109+ will therefore work like a simple touch area, merely reporting touch movement.
1110+ */
1111+void tst_DirectionalDragArea::immediateRecognitionWhenConstraintsDisabled()
1112+{
1113+ DirectionalDragArea *edgeDragArea =
1114+ m_view->rootObject()->findChild<DirectionalDragArea*>("hpDragArea");
1115+ Q_ASSERT(edgeDragArea != 0);
1116+ edgeDragArea->setRecognitionTimer(fakeTimer);
1117+ edgeDragArea->setTimeSource(fakeTimeSource);
1118+
1119+ // Disable the minimum amount of constraints to ensure immediate recognition
1120+ edgeDragArea->setDistanceThreshold(0);
1121+ edgeDragArea->setCompositionTime(0);
1122+
1123+ // Put an item right behind edgeDragArea to receive the touches ignored by it
1124+ DummyItem *dummyItem = new DummyItem;
1125+ dummyItem->setParentItem(edgeDragArea->parentItem());
1126+ dummyItem->setX(edgeDragArea->x());
1127+ dummyItem->setY(edgeDragArea->y());
1128+ dummyItem->setZ(edgeDragArea->z() - 1.0);
1129+ dummyItem->setWidth(edgeDragArea->width());
1130+ dummyItem->setHeight(edgeDragArea->height());
1131+
1132+ QPoint touch0Pos(edgeDragArea->width()/2.0f, m_view->height()/2.0f);
1133+
1134+ QTest::touchEvent(m_view, m_device).press(0, touch0Pos);
1135+
1136+ // check for immediate recognition
1137+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Recognized);
1138+
1139+ // and therefore it should have immediately grabbed the touch point,
1140+ // not letting it leak to items behind him.
1141+ QCOMPARE(dummyItem->touchEvents.count(), 0);
1142+}
1143+
1144+void tst_DirectionalDragArea::withdrawTouchOwnershipCandidacyIfDisabledDuringRecognition()
1145+{
1146+ DirectionalDragArea *edgeDragArea =
1147+ m_view->rootObject()->findChild<DirectionalDragArea*>("hpDragArea");
1148+ Q_ASSERT(edgeDragArea != 0);
1149+ edgeDragArea->setRecognitionTimer(fakeTimer);
1150+ edgeDragArea->setTimeSource(fakeTimeSource);
1151+
1152+ QPointF touchPoint = calculateInitialTouchPos(edgeDragArea);
1153+
1154+ // Move less than the minimum needed for the drag gesture recognition
1155+ qreal desiredDragDistance = edgeDragArea->distanceThreshold()*0.5f;
1156+ QPointF dragDirectionVector(1., 0.); // horizontal positive
1157+
1158+ qreal movementStepDistance = edgeDragArea->distanceThreshold() * 0.1f;
1159+ QPointF touchMovement = dragDirectionVector * movementStepDistance;
1160+ int totalMovementSteps = qCeil(desiredDragDistance / movementStepDistance);
1161+ int movementTimeStepMs = (edgeDragArea->compositionTime() * 0.8f) / totalMovementSteps;
1162+
1163+ QTest::touchEvent(m_view, m_device).press(0, touchPoint.toPoint());
1164+
1165+ for (int i = 0; i < totalMovementSteps; ++i) {
1166+ touchPoint += touchMovement;
1167+ passTime(movementTimeStepMs);
1168+ QTest::touchEvent(m_view, m_device).move(0, touchPoint.toPoint());
1169+ }
1170+
1171+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Undecided);
1172+
1173+ // edgeDragArea should be an undecided candidate
1174+ {
1175+ auto touchInfo = touchRegistry->findTouchInfo(0);
1176+ QCOMPARE(touchInfo->candidates.size(), 1);
1177+ QCOMPARE(touchInfo->candidates.at(0).item.data(), edgeDragArea);
1178+ QCOMPARE(touchInfo->candidates.at(0).undecided, true);
1179+ }
1180+
1181+ // disable the dragArea while it's still recognizing a possible drag gesture.
1182+ QFETCH(bool, disable);
1183+ if (disable) {
1184+ edgeDragArea->setEnabled(false);
1185+ } else {
1186+ edgeDragArea->setVisible(false);
1187+ }
1188+
1189+ // edgeDragArea should no longer be a candidate
1190+ {
1191+ auto touchInfo = touchRegistry->findTouchInfo(0);
1192+ QCOMPARE(touchInfo->candidates.size(), 0);
1193+ }
1194+
1195+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch);
1196+
1197+ QTest::touchEvent(m_view, m_device).release(0, touchPoint.toPoint());
1198+}
1199+
1200+void tst_DirectionalDragArea::withdrawTouchOwnershipCandidacyIfDisabledDuringRecognition_data()
1201+{
1202+ QTest::addColumn<bool>("disable");
1203+
1204+ QTest::newRow("disabled") << true;
1205+ QTest::newRow("invisible") << false;
1206+}
1207+
1208+void tst_DirectionalDragArea::tappedSignal()
1209+{
1210+ DirectionalDragArea *edgeDragArea =
1211+ m_view->rootObject()->findChild<DirectionalDragArea*>("hpDragArea");
1212+ QVERIFY(edgeDragArea != 0);
1213+ edgeDragArea->setRecognitionTimer(fakeTimer);
1214+ edgeDragArea->setTimeSource(fakeTimeSource);
1215+
1216+ QFETCH(bool, immediateGestureRecognition);
1217+ if (immediateGestureRecognition) {
1218+ // Disable the minimum amount of constraints to ensure immediate recognition
1219+ edgeDragArea->setDistanceThreshold(0);
1220+ edgeDragArea->setCompositionTime(0);
1221+ }
1222+
1223+ QPoint touch0Pos(edgeDragArea->width()/2.0f, m_view->height()/2.0f);
1224+
1225+ QSignalSpy tappedSpy(edgeDragArea, SIGNAL(tapped()));
1226+
1227+ QTest::touchEvent(m_view, m_device).press(0, touch0Pos);
1228+ passTime(edgeDragArea->maxTapDuration() / 2);
1229+ QTest::touchEvent(m_view, m_device).release(0, touch0Pos);
1230+
1231+ QCOMPARE(tappedSpy.count(), 1);
1232+}
1233+
1234+void tst_DirectionalDragArea::tappedSignal_data()
1235+{
1236+ QTest::addColumn<bool>("immediateGestureRecognition");
1237+
1238+ QTest::newRow("immediate gesture recognition") << true;
1239+ QTest::newRow("default gesture recognition") << false;
1240+}
1241+
1242+void tst_DirectionalDragArea::gettingTouchOwnershipMakesMouseAreaBehindGetCanceled()
1243+{
1244+ DirectionalDragArea *edgeDragArea =
1245+ m_view->rootObject()->findChild<DirectionalDragArea*>("hpDragArea");
1246+ QVERIFY(edgeDragArea != nullptr);
1247+ edgeDragArea->setRecognitionTimer(fakeTimer);
1248+ edgeDragArea->setTimeSource(fakeTimeSource);
1249+
1250+ QQuickMouseArea *mouseArea =
1251+ m_view->rootObject()->findChild<QQuickMouseArea*>("mouseArea");
1252+ QVERIFY(mouseArea != nullptr);
1253+
1254+ MouseAreaSpy mouseAreaSpy(mouseArea);
1255+
1256+ QPointF initialTouchPos = calculateInitialTouchPos(edgeDragArea);
1257+ QPointF touchPoint = initialTouchPos;
1258+
1259+ qreal desiredDragDistance = edgeDragArea->distanceThreshold()*2;
1260+ QPointF dragDirectionVector(1.0f, 0.0f); // rightwards
1261+ qreal movementStepDistance = edgeDragArea->distanceThreshold() * 0.1f;
1262+ QPointF touchMovement = dragDirectionVector * movementStepDistance;
1263+ int totalMovementSteps = qCeil(desiredDragDistance / movementStepDistance);
1264+ int movementTimeStepMs = (edgeDragArea->compositionTime() * 1.5f) / totalMovementSteps;
1265+
1266+ QCOMPARE(mouseArea->pressed(), false);
1267+
1268+ QTest::touchEvent(m_view, m_device).press(0, touchPoint.toPoint());
1269+
1270+ // The TouchBegin passes through the DirectionalDragArea and reaches the MouseArea behind it,
1271+ // where it's converted to a MouseEvent by QQuickWindow and sent to the MouseArea which then
1272+ // accepts it. Making it get pressed.
1273+ QCOMPARE(mouseArea->pressed(), true);
1274+ QCOMPARE(mouseAreaSpy.canceledCount, 0);
1275+
1276+ for (int i = 0; i < totalMovementSteps; ++i) {
1277+ touchPoint += touchMovement;
1278+ passTime(movementTimeStepMs);
1279+ QTest::touchEvent(m_view, m_device).move(0, touchPoint.toPoint());
1280+ }
1281+
1282+ // As the DirectionalDragArea recognizes the gesture, it grabs the touch from the MouseArea,
1283+ // which should make the MouseArea get a cancelation event, which will then cause it to
1284+ // reset its state (going back to "unpressed"/"released").
1285+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Recognized);
1286+ QCOMPARE(mouseArea->pressed(), false);
1287+ QCOMPARE(mouseAreaSpy.canceledCount, 1);
1288+
1289+ QTest::touchEvent(m_view, m_device).release(0, touchPoint.toPoint());
1290+}
1291+
1292+////////////////////////// TouchMemento /////////////////////////////
1293+
1294+TouchMemento::TouchMemento(const QTouchEvent *touchEvent)
1295+ : touchPointStates(touchEvent->touchPointStates()), touchPoints(touchEvent->touchPoints())
1296+{
1297+
1298+}
1299+
1300+bool TouchMemento::containsTouchWithId(int touchId) const
1301+{
1302+ for (int i = 0; i < touchPoints.count(); ++i) {
1303+ if (touchPoints.at(i).id() == touchId) {
1304+ return true;
1305+ }
1306+ }
1307+ return false;
1308+}
1309+
1310+////////////////////////// DummyItem /////////////////////////////
1311+
1312+DummyItem::DummyItem(QQuickItem *parent)
1313+ : QQuickItem(parent)
1314+{
1315+ touchEventHandler = defaultTouchEventHandler;
1316+}
1317+
1318+void DummyItem::touchEvent(QTouchEvent *event)
1319+{
1320+ touchEvents.append(TouchMemento(event));
1321+ touchEventHandler(event);
1322+}
1323+
1324+void DummyItem::defaultTouchEventHandler(QTouchEvent *event)
1325+{
1326+ event->accept();
1327+}
1328+
852QTEST_MAIN(tst_DirectionalDragArea)
1329QTEST_MAIN(tst_DirectionalDragArea)
853
1330
854#include "tst_DirectionalDragArea.moc"
1331#include "tst_DirectionalDragArea.moc"
tests/plugins/Ubuntu/Gestures/tst_DirectionalDragArea.qml+1
@@ -24,6 +24,7 @@ Rectangle {
24
24
25 MouseArea {
25 MouseArea {
26 id: mouseArea
26 id: mouseArea
27+ objectName: "mouseArea"
27 anchors.fill: parent
28 anchors.fill: parent
28 onClicked: {
29 onClicked: {
29 hpLauncher.reset()
30 hpLauncher.reset()
tests/plugins/Ubuntu/Gestures/tst_TouchGate.cppnew file+345
@@ -0,0 +1,345 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include <qpa/qwindowsysteminterface.h>
18+#include <QtTest>
19+#include <QQmlEngine>
20+#include <QQuickView>
21+#include <QSharedPointer>
22+
23+// C++ std lib
24+#include <functional>
25+
26+#include <TouchGate.h>
27+#include <TouchRegistry.h>
28+
29+class TestItem : public QQuickItem
30+{
31+ Q_OBJECT
32+
33+public:
34+ QList<QSharedPointer<QTouchEvent>> touchEventsReceived;
35+
36+protected:
37+ void touchEvent(QTouchEvent *event) override;
38+};
39+
40+class CandidateItem : public QQuickItem
41+{
42+ Q_OBJECT
43+
44+public:
45+ std::function<void(QTouchEvent*)> touchEventHandler;
46+
47+protected:
48+ void touchEvent(QTouchEvent *event) override;
49+};
50+
51+class tst_TouchGate : public QObject
52+{
53+ Q_OBJECT
54+public:
55+ tst_TouchGate(QObject *parent = nullptr);
56+private Q_SLOTS:
57+ void initTestCase(); // will be called before the first test function is executed
58+ void cleanupTestCase(); // will be called after the last test function was executed.
59+
60+ void init(); // called right before each and every test function is executed
61+ void cleanup(); // called right after each and every test function is executed
62+
63+ void onlyCandidate_passThrough();
64+ void holdsEventsUntilGainsOwnership();
65+ void holdsEventsUntilGainsOwnership_data();
66+ void holdsEventsUntilGainsOwnership_repeatingTouchId();
67+ void dontDispacthToDisabledOrInvisibleTarget();
68+ void dontDispacthToDisabledOrInvisibleTarget_data();
69+
70+private:
71+ QQuickView *createView();
72+ TouchRegistry *touchRegistry;
73+ QQuickView *view;
74+ QTouchDevice *device;
75+};
76+
77+tst_TouchGate::tst_TouchGate(QObject *parent)
78+ : QObject(parent)
79+ , touchRegistry(nullptr)
80+ , view(nullptr)
81+ , device(nullptr)
82+{
83+}
84+
85+void tst_TouchGate::initTestCase()
86+{
87+ if (!device) {
88+ device = new QTouchDevice;
89+ device->setType(QTouchDevice::TouchScreen);
90+ QWindowSystemInterface::registerTouchDevice(device);
91+ }
92+}
93+
94+void tst_TouchGate::cleanupTestCase()
95+{
96+}
97+
98+void tst_TouchGate::init()
99+{
100+ touchRegistry = new TouchRegistry;
101+
102+ view = createView();
103+ view->installEventFilter(touchRegistry);
104+ view->setSource(QUrl::fromLocalFile("touchGateExample.qml"));
105+ view->show();
106+ QVERIFY(QTest::qWaitForWindowExposed(view));
107+ QVERIFY(view->rootObject() != 0);
108+ qApp->processEvents();
109+}
110+
111+void tst_TouchGate::cleanup()
112+{
113+ delete view;
114+ view = 0;
115+
116+ delete touchRegistry;
117+ touchRegistry = nullptr;
118+}
119+
120+QQuickView *tst_TouchGate::createView()
121+{
122+ QQuickView *window = new QQuickView(0);
123+ window->setResizeMode(QQuickView::SizeRootObjectToView);
124+ window->resize(720, 720);
125+ window->engine()->addImportPath(QLatin1String(UBUNTU_GESTURES_PLUGIN_DIR));
126+
127+ return window;
128+}
129+
130+/*
131+ Simplest case. TouchGate is the only candidate for the touch point, thus it
132+ will instantly gain ownership causing the event to pass through it directly to
133+ its child.
134+ */
135+void tst_TouchGate::onlyCandidate_passThrough()
136+{
137+ TouchGate *touchGate = view->rootObject()->findChild<TouchGate*>("touchGate");
138+ Q_ASSERT(touchGate);
139+
140+ TestItem *testItem = new TestItem;
141+ testItem->setWidth(touchGate->width());
142+ testItem->setHeight(touchGate->height());
143+ testItem->setParentItem(view->rootObject());
144+ testItem->setZ(0.0);
145+
146+ touchGate->setZ(1.0);
147+ touchGate->setTargetItem(testItem);
148+
149+ QTest::touchEvent(view, device)
150+ .press(0, QPoint(100,100));
151+
152+ QCOMPARE(testItem->touchEventsReceived.count(), 1);
153+ QVERIFY(touchGate->m_storedEvents.isEmpty());
154+}
155+
156+void tst_TouchGate::holdsEventsUntilGainsOwnership()
157+{
158+ TouchGate *touchGate = view->rootObject()->findChild<TouchGate*>("touchGate");
159+ Q_ASSERT(touchGate);
160+ QFETCH(bool, ownershipAfterTouchEnd);
161+
162+ TestItem *testItem = new TestItem;
163+ testItem->setWidth(touchGate->width());
164+ testItem->setHeight(touchGate->height());
165+ testItem->setParentItem(view->rootObject());
166+ testItem->setZ(0.0);
167+
168+ touchGate->setZ(1.0);
169+ touchGate->setTargetItem(testItem);
170+
171+ // Put it in front of touchGate
172+ CandidateItem *candidateItem = new CandidateItem;
173+ candidateItem->setWidth(touchGate->width());
174+ candidateItem->setHeight(touchGate->height());
175+ candidateItem->setParentItem(view->rootObject());
176+ candidateItem->setZ(2.0);
177+
178+ candidateItem->touchEventHandler = [&](QTouchEvent* event) {
179+ touchRegistry->addCandidateOwnerForTouch(0, candidateItem);
180+ event->ignore();
181+ };
182+
183+ QTest::touchEvent(view, device)
184+ .press(0, QPoint(100,100));
185+
186+ QCOMPARE(testItem->touchEventsReceived.count(), 0);
187+
188+ QTest::touchEvent(view, device)
189+ .move(0, QPoint(101,101));
190+
191+ QCOMPARE(testItem->touchEventsReceived.count(), 0);
192+
193+ if (!ownershipAfterTouchEnd) {
194+ touchRegistry->removeCandidateOwnerForTouch(0, candidateItem);
195+ // TouchGate should now open its flood gates and let testItem get all
196+ // events from touch 0 produced so far
197+ QCOMPARE(testItem->touchEventsReceived.count(), 2);
198+ }
199+
200+ QTest::touchEvent(view, device)
201+ .release(0, QPoint(101,101));
202+
203+ if (ownershipAfterTouchEnd) {
204+ QCOMPARE(testItem->touchEventsReceived.count(), 0);
205+ touchRegistry->removeCandidateOwnerForTouch(0, candidateItem);
206+ // TouchGate should now open its flood gates and let testItem get all
207+ // events from touch 0 produced so far
208+ }
209+
210+ QCOMPARE(testItem->touchEventsReceived.count(), 3);
211+
212+ QVERIFY(touchGate->m_storedEvents.isEmpty());
213+ QVERIFY(touchGate->m_touchInfoMap.isEmpty());
214+}
215+
216+void tst_TouchGate::holdsEventsUntilGainsOwnership_data()
217+{
218+ QTest::addColumn<bool>("ownershipAfterTouchEnd");
219+
220+ QTest::newRow("touch end after touch ownership") << false;
221+ QTest::newRow("touch ownership after touch end") << true;
222+}
223+
224+/*
225+ A touch point of id 0 starts and ends. Then the next touch point is also given
226+ id 0. Check that TouchGate behaves correcly recognizing them as two distinct touch
227+ points.
228+
229+ Note that this won't be a problem with Mir as it assigns unique ids for the touch
230+ points it produces.
231+ */
232+void tst_TouchGate::holdsEventsUntilGainsOwnership_repeatingTouchId()
233+{
234+ TouchGate *touchGate = view->rootObject()->findChild<TouchGate*>("touchGate");
235+ Q_ASSERT(touchGate);
236+
237+ TestItem *testItem = new TestItem;
238+ testItem->setWidth(touchGate->width());
239+ testItem->setHeight(touchGate->height());
240+ testItem->setParentItem(view->rootObject());
241+ testItem->setZ(0.0);
242+
243+ touchGate->setZ(1.0);
244+ touchGate->setTargetItem(testItem);
245+
246+ QTest::touchEvent(view, device)
247+ .press(0, QPoint(100,100));
248+ QTest::touchEvent(view, device)
249+ .move(0, QPoint(101,101));
250+ QTest::touchEvent(view, device)
251+ .release(0, QPoint(101,101));
252+
253+ QCOMPARE(testItem->touchEventsReceived.count(), 3);
254+ testItem->touchEventsReceived.clear();
255+
256+ // Put it in front of touchGate
257+ CandidateItem *candidateItem = new CandidateItem;
258+ candidateItem->setWidth(touchGate->width());
259+ candidateItem->setHeight(touchGate->height());
260+ candidateItem->setParentItem(view->rootObject());
261+ candidateItem->setZ(2.0);
262+
263+ candidateItem->touchEventHandler = [&](QTouchEvent* event) {
264+ touchRegistry->addCandidateOwnerForTouch(0, candidateItem);
265+ event->ignore();
266+ };
267+
268+ // A brand new touch, but with the same id as the previous one
269+ QTest::touchEvent(view, device)
270+ .press(0, QPoint(200,200));
271+ QTest::touchEvent(view, device)
272+ .move(0, QPoint(201,201));
273+ QTest::touchEvent(view, device)
274+ .release(0, QPoint(201,201));
275+
276+ QCOMPARE(testItem->touchEventsReceived.count(), 0);
277+
278+ touchRegistry->requestTouchOwnership(0, candidateItem);
279+
280+ QCOMPARE(testItem->touchEventsReceived.count(), 0);
281+
282+ QVERIFY(touchGate->m_storedEvents.isEmpty());
283+}
284+
285+void tst_TouchGate::dontDispacthToDisabledOrInvisibleTarget()
286+{
287+ TouchGate *touchGate = view->rootObject()->findChild<TouchGate*>("touchGate");
288+ Q_ASSERT(touchGate);
289+
290+ TestItem *testItem = new TestItem;
291+ testItem->setWidth(touchGate->width());
292+ testItem->setHeight(touchGate->height());
293+ testItem->setParentItem(view->rootObject());
294+ testItem->setZ(0.0);
295+
296+ QFETCH(bool, isEnabled);
297+ testItem->setEnabled(isEnabled);
298+
299+ QFETCH(bool, isVisible);
300+ testItem->setVisible(isVisible);
301+
302+ touchGate->setZ(1.0);
303+ touchGate->setTargetItem(testItem);
304+
305+ QTest::touchEvent(view, device)
306+ .press(0, QPoint(100,100));
307+
308+ QCOMPARE(testItem->touchEventsReceived.count(), 0);
309+ QVERIFY(touchGate->m_storedEvents.isEmpty());
310+}
311+
312+void tst_TouchGate::dontDispacthToDisabledOrInvisibleTarget_data()
313+{
314+ QTest::addColumn<bool>("isEnabled");
315+ QTest::addColumn<bool>("isVisible");
316+
317+ QTest::newRow("disabled visible") << false << true;
318+ QTest::newRow("enabled invisible") << true << false;
319+ QTest::newRow("disabled invisible") << false << false;
320+}
321+
322+///////////// TestItem /////////////////////////////////////////////////////////////
323+
324+void TestItem::touchEvent(QTouchEvent *event)
325+{
326+ QTouchEvent *clonedEvent = new QTouchEvent(event->type(),
327+ event->device(),
328+ event->modifiers(),
329+ event->touchPointStates(),
330+ event->touchPoints());
331+
332+ touchEventsReceived.append(QSharedPointer<QTouchEvent>(clonedEvent));
333+}
334+
335+///////////// CandidateItem /////////////////////////////////////////////////////////////
336+
337+void CandidateItem::touchEvent(QTouchEvent *event)
338+{
339+ touchEventHandler(event);
340+}
341+
342+
343+QTEST_MAIN(tst_TouchGate)
344+
345+#include "tst_TouchGate.moc"
tests/qmltests/Components/CMakeLists.txt+2−1
@@ -2,13 +2,14 @@
2
2
3include_directories(
3include_directories(
4 ${CMAKE_SOURCE_DIR}/plugins/Ubuntu/Gestures
4 ${CMAKE_SOURCE_DIR}/plugins/Ubuntu/Gestures
5+ ${CMAKE_SOURCE_DIR}/libs/UbuntuGestures
5 ${CMAKE_CURRENT_BINARY_DIR}
6 ${CMAKE_CURRENT_BINARY_DIR}
6 ${Qt5Gui_PRIVATE_INCLUDE_DIRS}
7 ${Qt5Gui_PRIVATE_INCLUDE_DIRS}
7 )
8 )
8
9
9add_executable(DragHandleTestExec tst_DragHandle.cpp)
10add_executable(DragHandleTestExec tst_DragHandle.cpp)
10qt5_use_modules(DragHandleTestExec Test Core Qml Gui Quick)
11qt5_use_modules(DragHandleTestExec Test Core Qml Gui Quick)
11−target_link_libraries(DragHandleTestExec UbuntuGestureQml)
12+target_link_libraries(DragHandleTestExec UbuntuGesturesQml UbuntuGestures)
12
13
13add_binary_qml_test(DragHandle ${CMAKE_BINARY_DIR}/plugins/Ubuntu/Gestures "" "")
14add_binary_qml_test(DragHandle ${CMAKE_BINARY_DIR}/plugins/Ubuntu/Gestures "" "")
14add_manual_qml_test(. DragHandle IMPORT_PATHS ${CMAKE_BINARY_DIR}/plugins)
15add_manual_qml_test(. DragHandle IMPORT_PATHS ${CMAKE_BINARY_DIR}/plugins)
tests/qmltests/Components/tst_DragHandle.cpp+12−15
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright (C) 2013 Canonical, Ltd.
2+ * Copyright (C) 2013-2014 Canonical, Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
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
5 * it under the terms of the GNU General Public License as published by
@@ -22,22 +22,12 @@
22#include <QtQml/QQmlEngine>
22#include <QtQml/QQmlEngine>
23
23
24#include <DirectionalDragArea.h>
24#include <DirectionalDragArea.h>
25+#include <Timer.h>
26+#include <TouchRegistry.h>
25
27
26−class FakeTimer : public UbuntuGestures::AbstractTimer
27−{
28− Q_OBJECT
29−public:
30− FakeTimer(QObject *parent = 0)
31− : UbuntuGestures::AbstractTimer(parent)
28+using namespace UbuntuGestures;
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−};
39
29
40−class FakeTimeSource : public UbuntuGestures::TimeSource
30+class FakeTimeSource : public TimeSource
41{
31{
42public:
32public:
43 FakeTimeSource() { m_msecsSinceReference = 0; }
33 FakeTimeSource() { m_msecsSinceReference = 0; }
@@ -77,6 +67,7 @@ private:
77
67
78 QQuickView *createView();
68 QQuickView *createView();
79 QQuickView *m_view;
69 QQuickView *m_view;
70+ TouchRegistry *m_touchRegistry;
80 QTouchDevice *m_device;
71 QTouchDevice *m_device;
81 FakeTimer *m_fakeTimer;
72 FakeTimer *m_fakeTimer;
82 QSharedPointer<FakeTimeSource> m_fakeTimeSource;
73 QSharedPointer<FakeTimeSource> m_fakeTimeSource;
@@ -100,7 +91,10 @@ void tst_DragHandle::cleanupTestCase()
100
91
101void tst_DragHandle::init()
92void tst_DragHandle::init()
102{
93{
94+ m_touchRegistry = new TouchRegistry;
95+
103 m_view = createView();
96 m_view = createView();
97+ m_view->installEventFilter(m_touchRegistry);
104 m_view->setSource(QUrl::fromLocalFile(TEST_DIR"/tst_DragHandle.qml"));
98 m_view->setSource(QUrl::fromLocalFile(TEST_DIR"/tst_DragHandle.qml"));
105 m_view->show();
99 m_view->show();
106 QVERIFY(QTest::qWaitForWindowExposed(m_view));
100 QVERIFY(QTest::qWaitForWindowExposed(m_view));
@@ -116,6 +110,9 @@ void tst_DragHandle::cleanup()
116 delete m_view;
110 delete m_view;
117 m_view = 0;
111 m_view = 0;
118
112
113+ delete m_touchRegistry;
114+ m_touchRegistry = 0;
115+
119 delete m_fakeTimer;
116 delete m_fakeTimer;
120 m_fakeTimer = 0;
117 m_fakeTimer = 0;
121
118
tests/qmltests/Greeter/tst_SingleGreeter.qml+1−1
@@ -87,7 +87,7 @@ Item {
87
87
88 function test_teasingArea(data) {
88 function test_teasingArea(data) {
89 teaseSpy.clear()
89 teaseSpy.clear()
90− mouseClick(greeter, data.posX, greeter.height - units.gu(1))
90+ tap(greeter, data.posX, greeter.height - units.gu(1))
91 teaseSpy.wait()
91 teaseSpy.wait()
92 tryCompare(teaseSpy, "count", 1)
92 tryCompare(teaseSpy, "count", 1)
93 }
93 }
tests/qmltests/Launcher/tst_Launcher.qml+118−102
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright 2013 Canonical Ltd.
2+ * Copyright 2013-2014 Canonical Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
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
5 * it under the terms of the GNU General Public License as published by
@@ -28,25 +28,49 @@ Item {
28 width: units.gu(50)
28 width: units.gu(50)
29 height: units.gu(55)
29 height: units.gu(55)
30
30
31+ Loader {
32+ id: launcherLoader
33+ anchors.fill: parent
34+ property bool itemDestroyed: false
35+ sourceComponent: Component {
31− Launcher {
36+ Launcher {
32− id: launcher
37+ id: launcher
33− x: 0
38+ x: 0
34− y: 0
39+ y: 0
35− width: units.gu(40)
40+ width: units.gu(40)
36− height: parent.height
41+ height: root.height
42+
43+ property string lastSelectedApplication
44+
45+ onLauncherApplicationSelected: {
46+ lastSelectedApplication = appId
47+ }
37
48
38− property string lastSelectedApplication
49+ property int showDashHome_count: 0
50+ onShowDashHome: {
51+ showDashHome_count++;
52+ }
39
53
40− onLauncherApplicationSelected: {
54+ property int maxPanelX: 0
41− lastSelectedApplication = appId
42− }
43
55
44− property int showDashHome_count: 0
56+ Connections {
45− onShowDashHome: {
57+ target: testCase.findChild(launcher, "launcherPanel")
46− showDashHome_count++;
58+
59+ onXChanged: {
60+ if (target.x > launcher.maxPanelX) {
61+ launcher.maxPanelX = target.x;
62+ }
63+ }
47− }
64+ }
48
65
49− property int maxPanelX: 0
66+ Component.onCompleted: {
67+ launcherLoader.itemDestroyed = false;
68+ }
69+ Component.onDestruction: {
70+ launcherLoader.itemDestroyed = true;
71+ }
72+ }
73+ }
50 }
74 }
51
75
52 SignalSpy {
76 SignalSpy {
@@ -54,19 +78,45 @@ Item {
54 target: LauncherModel
78 target: LauncherModel
55 }
79 }
56
80
57− Connections {
81+ UT.UnityTestCase {
58− target: testCase.findChild(launcher, "launcherPanel")
82+ id: testCase
83+ name: "Launcher"
84+ when: windowShown
59
85
60− onXChanged: {
86+ property Item launcher: launcherLoader.status === Loader.Ready ? launcherLoader.item : null
61− if (target.x > launcher.maxPanelX) {
87+ function cleanup() {
62− launcher.maxPanelX = target.x;
88+ launcherLoader.active = false;
63− }
89+ // Loader.status might be Loader.Null and Loader.item might be null but the Loader
90+ // item might still be alive. So if we set Loader.active back to true
91+ // again right now we will get the very same Shell instance back. So no reload
92+ // actually took place. Likely because Loader waits until the next event loop
93+ // iteration to do its work. So to ensure the reload, we will wait until the
94+ // Shell instance gets destroyed.
95+ tryCompare(launcherLoader, "itemDestroyed", true);
96+ launcherLoader.active = true;
64 }
97 }
65− }
98+ function init() {
99+ var listView = findChild(launcher, "launcherListView");
100+ // wait for it to settle before doing the flick. Otherwise the flick
101+ // might fail.
102+ // On startup that listView is already moving. maybe because of its contents
103+ // growing while populating it with icons etc.
104+ tryCompare(listView, "flicking", false);
66
105
67− UT.UnityTestCase {
106+ // Make sure noone changed the height of the window. The issue this test case
68− id: revealer
107+ // is verifying only happens on certain heights of the Launcher
69− when: windowShown
108+ compare(root.height, units.gu(55));
109+
110+ compare(listView.contentY, -listView.topMargin, "Launcher did not start up with first item unfolded");
111+
112+ // Now do check that snapping is in fact enabled
113+ compare(listView.snapMode, ListView.SnapToItem, "Snapping is not enabled");
114+
115+ // Tests can be run in a reaaaaally slow environment or machine. Thus ensure
116+ // the dismissTimer doesn't time out inadvertently.
117+ var dismissTimer = findInvisibleChild(launcher, "dismissTimer");
118+ dismissTimer.interval = 60 * 60 * 1000; // one hour
119+ }
70
120
71 function dragLauncherIntoView() {
121 function dragLauncherIntoView() {
72 var startX = launcher.dragAreaWidth/2;
122 var startX = launcher.dragAreaWidth/2;
@@ -87,22 +137,11 @@ Item {
87 var panel = findChild(launcher, "launcherPanel");
137 var panel = findChild(launcher, "launcherPanel");
88 tryCompare(panel, "x", -panel.width, 1000);
138 tryCompare(panel, "x", -panel.width, 1000);
89 }
139 }
90− }
91−
92− UT.UnityTestCase {
93− id: testCase
94− name: "Launcher"
95− when: windowShown && initTestCase.completed
96−
97− function cleanup() {
98− mouseClick(root, root.width / 2, root.height / 2);
99− revealer.waitUntilLauncherDisappears();
100− }
101
140
102 // Drag from the left edge of the screen rightwards and check that the launcher
141 // Drag from the left edge of the screen rightwards and check that the launcher
103 // appears (as if being dragged by the finger/pointer)
142 // appears (as if being dragged by the finger/pointer)
104 function test_dragLeftEdgeToRevealLauncherAndTapCenterToDismiss() {
143 function test_dragLeftEdgeToRevealLauncherAndTapCenterToDismiss() {
105− revealer.waitUntilLauncherDisappears();
144+ waitUntilLauncherDisappears();
106
145
107 var panel = findChild(launcher, "launcherPanel")
146 var panel = findChild(launcher, "launcherPanel")
108 verify(panel != undefined)
147 verify(panel != undefined)
@@ -110,7 +149,7 @@ Item {
110 // it starts out hidden just left of the left launcher edge
149 // it starts out hidden just left of the left launcher edge
111 compare(panel.x, -panel.width)
150 compare(panel.x, -panel.width)
112
151
113− revealer.dragLauncherIntoView()
152+ dragLauncherIntoView()
114
153
115 // tapping on the center of the screen should dismiss the launcher
154 // tapping on the center of the screen should dismiss the launcher
116 mouseClick(launcher, launcher.width/2, launcher.height/2)
155 mouseClick(launcher, launcher.width/2, launcher.height/2)
@@ -124,7 +163,7 @@ Item {
124 corresponding desktop file. E.g. clicking on phone icon should yield
163 corresponding desktop file. E.g. clicking on phone icon should yield
125 launcherApplicationSelected("[...]dialer-app.desktop") */
164 launcherApplicationSelected("[...]dialer-app.desktop") */
126 function test_clickingOnAppIconCausesSignalEmission() {
165 function test_clickingOnAppIconCausesSignalEmission() {
127− revealer.dragLauncherIntoView();
166+ dragLauncherIntoView();
128 launcher.lastSelectedApplication = ""
167 launcher.lastSelectedApplication = ""
129
168
130 var listView = findChild(launcher, "launcherListView");
169 var listView = findChild(launcher, "launcherListView");
@@ -140,7 +179,7 @@ Item {
140 "dialer-app")
179 "dialer-app")
141
180
142 // Tapping on an application icon also dismisses the launcher
181 // Tapping on an application icon also dismisses the launcher
143− revealer.waitUntilLauncherDisappears()
182+ waitUntilLauncherDisappears()
144 }
183 }
145
184
146 /* If I click on the dash icon on the launcher
185 /* If I click on the dash icon on the launcher
@@ -148,7 +187,7 @@ Item {
148 function test_clickingOnDashIconCausesSignalEmission() {
187 function test_clickingOnDashIconCausesSignalEmission() {
149 launcher.showDashHome_count = 0
188 launcher.showDashHome_count = 0
150
189
151− revealer.dragLauncherIntoView()
190+ dragLauncherIntoView()
152
191
153 var dashIcon = findChild(launcher, "dashItem")
192 var dashIcon = findChild(launcher, "dashItem")
154 verify(dashIcon != undefined)
193 verify(dashIcon != undefined)
@@ -158,7 +197,7 @@ Item {
158 tryCompare(launcher, "showDashHome_count", 1)
197 tryCompare(launcher, "showDashHome_count", 1)
159
198
160 // Tapping on the dash icon also dismisses the launcher
199 // Tapping on the dash icon also dismisses the launcher
161− revealer.waitUntilLauncherDisappears()
200+ waitUntilLauncherDisappears()
162 }
201 }
163
202
164 function test_teaseLauncher_data() {
203 function test_teaseLauncher_data() {
@@ -185,12 +224,12 @@ Item {
185 wait(100)
224 wait(100)
186 compare(launcher.maxPanelX, -launcher.panelWidth, "Launcher moved even if it shouldn't")
225 compare(launcher.maxPanelX, -launcher.panelWidth, "Launcher moved even if it shouldn't")
187 }
226 }
188− revealer.waitUntilLauncherDisappears();
227+ waitUntilLauncherDisappears();
189 launcher.available = true;
228 launcher.available = true;
190 }
229 }
191
230
192 function test_countEmblems() {
231 function test_countEmblems() {
193− revealer.dragLauncherIntoView();
232+ dragLauncherIntoView();
194 var launcherListView = findChild(launcher, "launcherListView");
233 var launcherListView = findChild(launcher, "launcherListView");
195 for (var i = 0; i < launcherListView.count; ++i) {
234 for (var i = 0; i < launcherListView.count; ++i) {
196 var delegate = findChild(launcherListView, "launcherDelegate" + i)
235 var delegate = findChild(launcherListView, "launcherDelegate" + i)
@@ -201,7 +240,7 @@ Item {
201 }
240 }
202
241
203 function test_progressOverlays() {
242 function test_progressOverlays() {
204− revealer.dragLauncherIntoView();
243+ dragLauncherIntoView();
205 var launcherListView = findChild(launcher, "launcherListView");
244 var launcherListView = findChild(launcher, "launcherListView");
206 for (var i = 0; i < launcherListView.count; ++i) {
245 for (var i = 0; i < launcherListView.count; ++i) {
207 var delegate = findChild(launcherListView, "launcherDelegate" + i)
246 var delegate = findChild(launcherListView, "launcherDelegate" + i)
@@ -210,35 +249,49 @@ Item {
210 }
249 }
211
250
212 function test_focusedHighlight() {
251 function test_focusedHighlight() {
213− revealer.dragLauncherIntoView();
252+ dragLauncherIntoView();
214 var launcherListView = findChild(launcher, "launcherListView");
253 var launcherListView = findChild(launcher, "launcherListView");
215 for (var i = 0; i < launcherListView.count; ++i) {
254 for (var i = 0; i < launcherListView.count; ++i) {
216 var delegate = findChild(launcherListView, "launcherDelegate" + i)
255 var delegate = findChild(launcherListView, "launcherDelegate" + i)
217 compare(findChild(delegate, "focusedHighlight").visible, LauncherModel.get(i).focused)
256 compare(findChild(delegate, "focusedHighlight").visible, LauncherModel.get(i).focused)
218 }
257 }
219 }
258 }
220− }
221−
222− UT.UnityTestCase {
223− id: clickFlickTestCase
224− when: windowShown && testCase.completed
225
259
226 function test_clickFlick_data() {
260 function test_clickFlick_data() {
227 var listView = findChild(launcher, "launcherListView");
261 var listView = findChild(launcher, "launcherListView");
228 return [
262 return [
263+ {tag: "unfolded top", positionViewAtBeginning: false,
229− {tag: "unfolded top", flickSpeed: units.gu(200), clickY: listView.topMargin + units.gu(2), expectFlick: false},
264+ clickY: listView.topMargin + units.gu(2),
265+ expectFlick: false},
266+
267+ {tag: "folded top", positionViewAtBeginning: true,
230− {tag: "folded top", flickSpeed: -units.gu(200), clickY: listView.topMargin + units.gu(2), expectFlick: true},
268+ clickY: listView.topMargin + units.gu(2),
269+ expectFlick: true},
270+
271+ {tag: "unfolded bottom", positionViewAtBeginning: true,
231− {tag: "unfolded bottom", flickSpeed: -units.gu(200), clickY: listView.height - listView.topMargin - units.gu(1), expectFlick: false},
272+ clickY: listView.height - listView.topMargin - units.gu(1),
273+ expectFlick: false},
274+
275+ {tag: "folded bottom", positionViewAtBeginning: false,
232− {tag: "folded bottom", flickSpeed: units.gu(200), clickY: listView.height - listView.topMargin - units.gu(1), expectFlick: true},
276+ clickY: listView.height - listView.topMargin - units.gu(1),
277+ expectFlick: true},
233 ];
278 ];
234 }
279 }
235
280
236 function test_clickFlick(data) {
281 function test_clickFlick(data) {
237 launcher.lastSelectedApplication = "";
282 launcher.lastSelectedApplication = "";
238− revealer.dragLauncherIntoView();
283+ dragLauncherIntoView();
239 var listView = findChild(launcher, "launcherListView");
284 var listView = findChild(launcher, "launcherListView");
240
285
241− listView.flick(0, data.flickSpeed);
286+ // flicking is unreliable. sometimes it works, sometimes the
287+ // list view moves just a tiny bit or not at all, making tests fail.
288+ // So for stability's sake we just put the listView in the position
289+ // we want to to actually start doing what this tests intends to check.
290+ if (data.positionViewAtBeginning) {
291+ listView.positionViewAtBeginning();
292+ } else {
293+ listView.positionViewAtEnd();
294+ }
242 tryCompare(listView, "flicking", false);
295 tryCompare(listView, "flicking", false);
243
296
244 var oldY = listView.contentY;
297 var oldY = listView.contentY;
@@ -253,44 +306,7 @@ Item {
253 verify(launcher.lastSelectedApplication != "");
306 verify(launcher.lastSelectedApplication != "");
254 compare(listView.contentY, oldY, "Launcher was flicked even though it should only launch an app");
307 compare(listView.contentY, oldY, "Launcher was flicked even though it should only launch an app");
255 }
308 }
256−
257− // Restore position on top
258− listView.flick(0, units.gu(200));
259− tryCompare(listView, "flicking", false)
260− // Click somewhere in the empty space to make it hide in case it isn't
261− mouseClick(launcher, launcher.width - units.gu(1), units.gu(1));
262− revealer.waitUntilLauncherDisappears();
263 }
309 }
264− }
265−
266− UT.UnityTestCase {
267− id: initTestCase
268− name: "LauncherInit"
269− when: windowShown
270−
271− function initTestCase() {
272− var listView = findChild(launcher, "launcherListView");
273− tryCompare(listView, "flicking", false)
274− }
275−
276− function test_initFirstUnfolded() {
277−
278− // Make sure noone changed the height of the window. The issue this test case
279− // is verifying only happens on certain heights of the Launcher
280− compare(root.height, units.gu(55));
281−
282− var listView = findChild(launcher, "launcherListView");
283− compare(listView.contentY, -listView.topMargin, "Launcher did not start up with first item unfolded");
284−
285− // Now do check that snapping is in fact enabled
286− compare(listView.snapMode, ListView.SnapToItem, "Snapping is not enabled");
287− }
288− }
289−
290− UT.UnityTestCase {
291− id: dndTestCase
292− name: "Drag and Drop"
293− when: windowShown && initTestCase.completed
294
310
295 function test_dragndrop_data() {
311 function test_dragndrop_data() {
296 return [
312 return [
@@ -301,7 +317,7 @@ Item {
301 }
317 }
302
318
303 function test_dragndrop(data) {
319 function test_dragndrop(data) {
304− revealer.dragLauncherIntoView();
320+ dragLauncherIntoView();
305 var draggedItem = findChild(launcher, "launcherDelegate4")
321 var draggedItem = findChild(launcher, "launcherDelegate4")
306 var item0 = findChild(launcher, "launcherDelegate0")
322 var item0 = findChild(launcher, "launcherDelegate0")
307 var fakeDragItem = findChild(launcher, "fakeDragItem")
323 var fakeDragItem = findChild(launcher, "fakeDragItem")
@@ -366,11 +382,11 @@ Item {
366
382
367 // Click somewhere in the empty space to make it hide in case it isn't
383 // Click somewhere in the empty space to make it hide in case it isn't
368 mouseClick(launcher, launcher.width - units.gu(1), units.gu(1));
384 mouseClick(launcher, launcher.width - units.gu(1), units.gu(1));
369− revealer.waitUntilLauncherDisappears();
385+ waitUntilLauncherDisappears();
370 }
386 }
371
387
372 function test_quicklist_dismiss() {
388 function test_quicklist_dismiss() {
373− revealer.dragLauncherIntoView();
389+ dragLauncherIntoView();
374 var draggedItem = findChild(launcher, "launcherDelegate5")
390 var draggedItem = findChild(launcher, "launcherDelegate5")
375 var item0 = findChild(launcher, "launcherDelegate0")
391 var item0 = findChild(launcher, "launcherDelegate0")
376 var fakeDragItem = findChild(launcher, "fakeDragItem")
392 var fakeDragItem = findChild(launcher, "fakeDragItem")
@@ -402,9 +418,9 @@ Item {
402
418
403 function test_quicklist_positioning(data) {
419 function test_quicklist_positioning(data) {
404 revealer.dragLauncherIntoView();
420 revealer.dragLauncherIntoView();
405− var quickList = findChild(launcher, "quickList");
421+ var quickList = findChild(launcher, "quickList")
406− var draggedItem = findChild(launcher, "launcherDelegate" + data.itemIndex);
422+ var draggedItem = findChild(launcher, "launcherDelegate" + data.itemIndex)
407− var quickListShape = findChild(launcher, "quickListShape");
423+ var quickListShape = findChild(launcher, "quickListShape")
408
424
409 // Position launcher to where we need it
425 // Position launcher to where we need it
410 var listView = findChild(launcher, "launcherListView");
426 var listView = findChild(launcher, "launcherListView");
@@ -439,7 +455,7 @@ Item {
439 }
455 }
440
456
441 function test_quicklist_click(data) {
457 function test_quicklist_click(data) {
442− revealer.dragLauncherIntoView();
458+ dragLauncherIntoView();
443 var clickedItem = findChild(launcher, "launcherDelegate5")
459 var clickedItem = findChild(launcher, "launcherDelegate5")
444 var quickList = findChild(launcher, "quickList")
460 var quickList = findChild(launcher, "quickList")
445 var quickListShape = findChild(launcher, "quickListShape")
461 var quickListShape = findChild(launcher, "quickListShape")
tests/qmltests/Stages/tst_ApplicationWindow.qml+2−2
@@ -51,6 +51,7 @@ Rectangle {
51 anchors.fill: parent
51 anchors.fill: parent
52 application: fakeApplication
52 application: fakeApplication
53 orientation: Qt.PortraitOrientation
53 orientation: Qt.PortraitOrientation
54+ interactive: true
54 }
55 }
55 }
56 }
56 Loader {
57 Loader {
@@ -375,8 +376,7 @@ Rectangle {
375
376
376 waitUntilTransitionsEnd();
377 waitUntilTransitionsEnd();
377
378
378− // Because doing stuff in C++ is a PITA we keep the counter in the interal qml impl.
379− var fakeSurface = findChild(fakeSession.surface, "fakeSurfaceQML");
379+ var fakeSurface = fakeSession.surface;
380 fakeSurface.touchPressCount = 0;
380 fakeSurface.touchPressCount = 0;
381 fakeSurface.touchReleaseCount = 0;
381 fakeSurface.touchReleaseCount = 0;
382
382
tests/qmltests/Stages/tst_PhoneStage.qml+2−1
@@ -178,7 +178,8 @@ Item {
178 var startX = phoneStage.width - 2;
178 var startX = phoneStage.width - 2;
179 var startY = phoneStage.height / 2;
179 var startY = phoneStage.height / 2;
180 var endY = startY;
180 var endY = startY;
181− var endX = spreadView.width - (spreadView.width * spreadView[data.positionMarker]) - data.offset;
181+ var endX = spreadView.width - (spreadView.width * spreadView[data.positionMarker]) - data.offset
182+ - phoneStage.dragAreaWidth;
182
183
183 var oldFocusedApp = ApplicationManager.get(0);
184 var oldFocusedApp = ApplicationManager.get(0);
184 var newFocusedApp = ApplicationManager.get(data.newFocusedIndex);
185 var newFocusedApp = ApplicationManager.get(data.newFocusedIndex);
tests/qmltests/Stages/tst_SurfaceContainer.qml+26−1
@@ -99,6 +99,9 @@ Rectangle {
99 name: "SurfaceContainer"
99 name: "SurfaceContainer"
100 when: windowShown
100 when: windowShown
101
101
102+ property var surfaceContainer:
103+ surfaceContainerLoader.status === Loader.Ready ? surfaceContainerLoader.item : null
104+
102 function cleanup() {
105 function cleanup() {
103 // reload our test subject to get it in a fresh state once again
106 // reload our test subject to get it in a fresh state once again
104 surfaceContainerLoader.active = false;
107 surfaceContainerLoader.active = false;
@@ -126,7 +129,6 @@ Rectangle {
126
129
127 function test_animateRemoval() {
130 function test_animateRemoval() {
128 surfaceCheckbox.checked = true;
131 surfaceCheckbox.checked = true;
129− var surfaceContainer = surfaceContainerLoader.item;
130
132
131 verify(surfaceContainer.surface !== null);
133 verify(surfaceContainer.surface !== null);
132
134
@@ -135,5 +137,28 @@ Rectangle {
135 compare(surfaceContainer.state, "zombie");
137 compare(surfaceContainer.state, "zombie");
136 tryCompare(surfaceContainer, "surface", null);
138 tryCompare(surfaceContainer, "surface", null);
137 }
139 }
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+ }
138 }
163 }
139}
164}
tests/qmltests/tst_Shell.qml+118−3
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright (C) 2013 Canonical, Ltd.
2+ * Copyright (C) 2013-2014 Canonical, Ltd.
3 *
3 *
4 * Authors:
4 * Authors:
5 * Daniel d'Andrada <daniel.dandrada@canonical.com>
5 * Daniel d'Andrada <daniel.dandrada@canonical.com>
@@ -98,6 +98,11 @@ Row {
98 }
98 }
99 }
99 }
100
100
101+ SignalSpy {
102+ id: launcherShowDashHomeSpy
103+ signalName: "showDashHome"
104+ }
105+
101 SignalSpy {
106 SignalSpy {
102 id: sessionSpy
107 id: sessionSpy
103 signalName: "sessionStarted"
108 signalName: "sessionStarted"
@@ -131,9 +136,14 @@ Row {
131
136
132 sessionSpy.target = findChild(shell, "greeter")
137 sessionSpy.target = findChild(shell, "greeter")
133 dashCommunicatorSpy.target = findInvisibleChild(shell, "dashCommunicator");
138 dashCommunicatorSpy.target = findInvisibleChild(shell, "dashCommunicator");
139+
140+ var launcher = findChild(shell, "launcher");
141+ launcherShowDashHomeSpy.target = launcher;
134 }
142 }
135
143
136 function cleanup() {
144 function cleanup() {
145+ launcherShowDashHomeSpy.target = null;
146+
137 shellLoader.itemDestroyed = false;
147 shellLoader.itemDestroyed = false;
138
148
139 shellLoader.active = false;
149 shellLoader.active = false;
@@ -404,8 +414,7 @@ Row {
404 touchFlick(launcherPanel, touchStartX, touchStartY, touchStartX, 0);
414 touchFlick(launcherPanel, touchStartX, touchStartY, touchStartX, 0);
405 tryCompare(launcherPanel, "moving", false);
415 tryCompare(launcherPanel, "moving", false);
406
416
407− // NB tapping (i.e., using touch events) doesn't activate the icon... go figure...
408− mouseClick(appIcon, appIcon.width / 2, appIcon.height / 2);
417+ tap(appIcon, appIcon.width / 2, appIcon.height / 2);
409 }
418 }
410
419
411 function showIndicators() {
420 function showIndicators() {
@@ -572,5 +581,111 @@ Row {
572
581
573 tryCompare(greeter, "showProgress", data.unlocked ? 0 : 1);
582 tryCompare(greeter, "showProgress", data.unlocked ? 0 : 1);
574 }
583 }
584+
585+ function test_tapOnRightEdgeReachesApplicationSurface() {
586+ var topmostSpreadDelegate = findChild(shell, "appDelegate0");
587+ var topmostSurface = findChild(topmostSpreadDelegate, "surfaceContainer").surface;
588+ var rightEdgeDragArea = findChild(shell, "spreadDragArea");
589+
590+ topmostSurface.touchPressCount = 0;
591+ topmostSurface.touchReleaseCount = 0;
592+
593+ var tapPoint = rightEdgeDragArea.mapToItem(shell, rightEdgeDragArea.width / 2,
594+ rightEdgeDragArea.height / 2);
595+
596+ tap(shell, tapPoint.x, tapPoint.y);
597+
598+ tryCompare(topmostSurface, "touchPressCount", 1);
599+ tryCompare(topmostSurface, "touchReleaseCount", 1);
600+ }
601+
602+ /*
603+ Perform a right edge drag over an application surface and check
604+ that no touch event was sent to it (ie, they were all consumed
605+ by the right-edge drag area)
606+ */
607+ function test_rightEdgeDragDoesNotReachApplicationSurface() {
608+ var topmostSpreadDelegate = findChild(shell, "appDelegate0");
609+ var topmostSurface = findChild(topmostSpreadDelegate, "surfaceContainer").surface;
610+ var rightEdgeDragArea = findChild(shell, "spreadDragArea");
611+
612+ topmostSurface.touchPressCount = 0;
613+ topmostSurface.touchReleaseCount = 0;
614+
615+ var gestureStartPoint = rightEdgeDragArea.mapToItem(shell, rightEdgeDragArea.width / 2,
616+ rightEdgeDragArea.height / 2);
617+
618+ touchFlick(shell,
619+ gestureStartPoint.x /* fromX */, gestureStartPoint.y /* fromY */,
620+ units.gu(1) /* toX */, gestureStartPoint.y /* toY */);
621+
622+ tryCompare(topmostSurface, "touchPressCount", 0);
623+ tryCompare(topmostSurface, "touchReleaseCount", 0);
624+ }
625+
626+ function waitUntilFocusedApplicationIsShowingItsSurface()
627+ {
628+ var spreadDelegate = findChild(shell, "appDelegate0");
629+ var appState = findInvisibleChild(spreadDelegate, "applicationWindowStateGroup");
630+ tryCompare(appState, "state", "surface");
631+ var transitions = appState.transitions;
632+ for (var i = 0; i < transitions.length; ++i) {
633+ var transition = transitions[i];
634+ tryCompare(transition, "running", false, 2000);
635+ }
636+ }
637+
638+ function swipeFromRightEdgeToShowAppSpread()
639+ {
640+ // perform a right-edge drag to show the spread
641+ var touchStartX = shell.width - (shell.edgeSize / 2)
642+ var touchStartY = shell.height / 2;
643+ touchFlick(shell, touchStartX, touchStartY, units.gu(1) /* endX */, touchStartY /* endY */);
644+
645+ // check if it's indeed showing the spread
646+ var stage = findChild(shell, "stage");
647+ var spreadView = findChild(stage, "spreadView");
648+ tryCompare(spreadView, "phase", 2);
649+ }
650+
651+ function test_tapUbuntuIconInLauncherOverAppSpread() {
652+
653+ waitUntilFocusedApplicationIsShowingItsSurface();
654+
655+ swipeFromRightEdgeToShowAppSpread();
656+
657+ var launcher = findChild(shell, "launcher");
658+
659+ // ensure the launcher dimissal timer never gets triggered during the test run
660+ var dismissTimer = findInvisibleChild(launcher, "dismissTimer");
661+ dismissTimer.interval = 60 * 60 * 1000;
662+
663+ dragLauncherIntoView();
664+
665+ // Emulate a tap with a finger, where the touch position drifts during the tap.
666+ {
667+ var buttonShowDashHome = findChild(launcher, "buttonShowDashHome");
668+ var startPos = buttonShowDashHome.mapToItem(shell,
669+ buttonShowDashHome.width * 0.2,
670+ buttonShowDashHome.height * 0.2);
671+ var endPos = buttonShowDashHome.mapToItem(shell,
672+ buttonShowDashHome.width * 0.8,
673+ buttonShowDashHome.height * 0.8);
674+ touchFlick(shell, startPos.x, startPos.y, endPos.x, endPos.y);
675+ }
676+
677+ compare(launcherShowDashHomeSpy.count, 1);
678+
679+ // check that the stage has left spread mode.
680+ {
681+ var stage = findChild(shell, "stage");
682+ var spreadView = findChild(stage, "spreadView");
683+ tryCompare(spreadView, "phase", 0);
684+ }
685+
686+ // check that the launcher got dismissed
687+ var launcherPanel = findChild(shell, "launcherPanel");
688+ tryCompare(launcherPanel, "x", -launcherPanel.width);
689+ }
575 }
690 }
576}
691}
tests/uqmlscene/CMakeLists.txt+2
@@ -10,6 +10,7 @@ pkg_check_modules(XCB REQUIRED xcb)
10include_directories(
10include_directories(
11 ${CMAKE_SOURCE_DIR}/src
11 ${CMAKE_SOURCE_DIR}/src
12 ${Qt5Gui_PRIVATE_INCLUDE_DIRS}
12 ${Qt5Gui_PRIVATE_INCLUDE_DIRS}
13+ ${CMAKE_SOURCE_DIR}/libs/UbuntuGestures
13)
14)
14
15
15if (NOT "${XCB_INCLUDE_DIRS}" STREQUAL "")
16if (NOT "${XCB_INCLUDE_DIRS}" STREQUAL "")
@@ -20,4 +21,5 @@ target_link_libraries(uqmlscene
20 ${Qt5Qml_LIBRARIES}
21 ${Qt5Qml_LIBRARIES}
21 ${Qt5Quick_LIBRARIES}
22 ${Qt5Quick_LIBRARIES}
22 ${XCB_LDFLAGS}
23 ${XCB_LDFLAGS}
24+ UbuntuGestures
23 )
25 )
tests/uqmlscene/README+3−1
@@ -1 +1,3 @@
1+Qt's qmlscene tool with the following modifications:
1−Qt's qmlscene tool modified to use MouseTouchAdaptor. That way we can interact with touch-only components
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 @@
65
65
66#include <MouseTouchAdaptor.h>
66#include <MouseTouchAdaptor.h>
67
67
68+// UbuntuGestures lib
69+#include <TouchRegistry.h>
70+
68#ifdef QML_RUNTIME_TESTING
71#ifdef QML_RUNTIME_TESTING
69class RenderStatistics
72class RenderStatistics
70{
73{
@@ -496,6 +499,12 @@ int main(int argc, char ** argv)
496 QQuickItem *contentItem = qobject_cast<QQuickItem *>(topLevel);
499 QQuickItem *contentItem = qobject_cast<QQuickItem *>(topLevel);
497 if (contentItem) {
500 if (contentItem) {
498 qxView = new QQuickView(&engine, nullptr);
501 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());
499 window = qxView;
508 window = qxView;
500 // Set window default properties; the qml can still override them
509 // Set window default properties; the qml can still override them
501 QString oname = contentItem->objectName();
510 QString oname = contentItem->objectName();
tests/utils/modules/Unity/Test/CMakeLists.txt+2
@@ -4,6 +4,7 @@ add_definitions(-DQT_NO_KEYWORDS)
4
4
5include_directories(
5include_directories(
6 ${Qt5Gui_PRIVATE_INCLUDE_DIRS}
6 ${Qt5Gui_PRIVATE_INCLUDE_DIRS}
7+ ${CMAKE_SOURCE_DIR}/libs/UbuntuGestures
7)
8)
8
9
9set(UnityTestQML_SOURCES
10set(UnityTestQML_SOURCES
@@ -13,6 +14,7 @@ set(UnityTestQML_SOURCES
13)
14)
14
15
15add_library(UnityTestQml MODULE ${UnityTestQML_SOURCES})
16add_library(UnityTestQml MODULE ${UnityTestQML_SOURCES})
17+target_link_libraries(UnityTestQml UbuntuGestures)
16
18
17qt5_use_modules(UnityTestQml Core Quick Test)
19qt5_use_modules(UnityTestQml Core Quick Test)
18
20
tests/utils/modules/Unity/Test/UnityTestCase.qml+2
@@ -335,6 +335,8 @@ TestCase {
335 }
335 }
336
336
337 Component.onCompleted: {
337 Component.onCompleted: {
338+ UT.Util.ensureTouchRegistryInstalled();
339+
338 var rootItem = parent;
340 var rootItem = parent;
339 while (rootItem.parent != undefined) {
341 while (rootItem.parent != undefined) {
340 rootItem = rootItem.parent;
342 rootItem = rootItem.parent;
tests/utils/modules/Unity/Test/testutil.cpp+41−2
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright (C) 2012, 2013 Canonical, Ltd.
2+ * Copyright (C) 2012, 2013, 2014 Canonical, Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
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
5 * it under the terms of the GNU General Public License as published by
@@ -19,11 +19,19 @@
19
19
20#include <qpa/qwindowsysteminterface.h>
20#include <qpa/qwindowsysteminterface.h>
21#include <QtGui/QGuiApplication>
21#include <QtGui/QGuiApplication>
22+#include <QQuickView>
23+
24+// UbuntuGestures lib
25+#include <TouchRegistry.h>
26+#include <Timer.h>
27+
28+using namespace UbuntuGestures;
22
29
23TestUtil::TestUtil(QObject *parent)
30TestUtil::TestUtil(QObject *parent)
24 : QObject(parent)
31 : QObject(parent)
25 , m_targetWindow(0)
32 , m_targetWindow(0)
26 , m_touchDevice(0)
33 , m_touchDevice(0)
34+ , m_installedTouchRegistry(false)
27{
35{
28}
36}
29
37
@@ -52,6 +60,7 @@ TouchEventSequenceWrapper *TestUtil::touchEvent()
52{
60{
53 ensureTargetWindow();
61 ensureTargetWindow();
54 ensureTouchDevice();
62 ensureTouchDevice();
63+ ensureTouchRegistryInstalled();
55
64
56 return new TouchEventSequenceWrapper(
65 return new TouchEventSequenceWrapper(
57 QTest::touchEvent(m_targetWindow, m_touchDevice, /* autoCommit */ false));
66 QTest::touchEvent(m_targetWindow, m_touchDevice, /* autoCommit */ false));
@@ -59,7 +68,7 @@ TouchEventSequenceWrapper *TestUtil::touchEvent()
59
68
60void TestUtil::ensureTargetWindow()
69void TestUtil::ensureTargetWindow()
61{
70{
62− if (!m_targetWindow)
71+ if (!m_targetWindow && !QGuiApplication::topLevelWindows().isEmpty())
63 m_targetWindow = QGuiApplication::topLevelWindows()[0];
72 m_targetWindow = QGuiApplication::topLevelWindows()[0];
64}
73}
65
74
@@ -71,3 +80,33 @@ void TestUtil::ensureTouchDevice()
71 QWindowSystemInterface::registerTouchDevice(m_touchDevice);
80 QWindowSystemInterface::registerTouchDevice(m_touchDevice);
72 }
81 }
73}
82}
83+
84+void TestUtil::ensureTouchRegistryInstalled()
85+{
86+ if (m_installedTouchRegistry)
87+ return;
88+
89+ TouchRegistry *touchRegistry;
90+ if (TouchRegistry::instance() == nullptr) {
91+ // Tests can be *very* slow to run and we don't want things timing out because
92+ // of that. So give it fake timers to use (they will never time out)
93+ touchRegistry = new TouchRegistry(this, new FakeTimerFactory);
94+ } else {
95+ touchRegistry = TouchRegistry::instance();
96+ if (touchRegistry->parent() != this) {
97+ // someone else created it. leave it alone
98+ m_installedTouchRegistry = true;
99+ }
100+ }
101+
102+ ensureTargetWindow();
103+
104+ if (m_targetWindow) {
105+ QQuickView *view = qobject_cast<QQuickView*>(m_targetWindow);
106+ if (view) {
107+ view->installEventFilter(touchRegistry);
108+ touchRegistry->setParent(view);
109+ m_installedTouchRegistry = true;
110+ }
111+ }
112+}
tests/utils/modules/Unity/Test/testutil.h+3−1
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright (C) 2012, 2013 Canonical, Ltd.
2+ * Copyright (C) 2012, 2013, 2014 Canonical, Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
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
5 * it under the terms of the GNU General Public License as published by
@@ -34,6 +34,7 @@ public:
34
34
35 Q_INVOKABLE bool isInstanceOf(QObject*, QString);
35 Q_INVOKABLE bool isInstanceOf(QObject*, QString);
36 Q_INVOKABLE TouchEventSequenceWrapper *touchEvent();
36 Q_INVOKABLE TouchEventSequenceWrapper *touchEvent();
37+ Q_INVOKABLE void ensureTouchRegistryInstalled();
37
38
38private:
39private:
39 void ensureTargetWindow();
40 void ensureTargetWindow();
@@ -41,6 +42,7 @@ private:
41
42
42 QWindow *m_targetWindow;
43 QWindow *m_targetWindow;
43 QTouchDevice *m_touchDevice;
44 QTouchDevice *m_touchDevice;
45+ bool m_installedTouchRegistry;
44};
46};
45
47
46QML_DECLARE_TYPE(TestUtil)
48QML_DECLARE_TYPE(TestUtil)