on xenial help sign in
Merge from trunk
72c4cdaMichael Terry
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/changelog+27
@@ -1,3 +1,30 @@
1+unity8 (8.00+14.10.20141013.2-0ubuntu1) utopic; urgency=low
2+
3+ [ Michael Terry ]
4+ * Don't show initial lockscreen during the edge demo. The user just
5+ set up their phone with a password, it's pointless to ask them
6+ again. (LP: #1358283)
7+ * Distinguish between incoming calls and other dialer-app opens. (LP:
8+ #1378218) (LP: #1378218)
9+
10+ [ Andrea Cimitan ]
11+ * Fix flickable speed to be resolution independent, by subclassing
12+ components (LP: #1348557)
13+
14+ [ Michał Sawicz ]
15+ * Fix the ShellWithPin test and some functionality.
16+
17+ [ Albert Astals ]
18+ * Move Base.qml to DashCategoryBase
19+ * Fix first item positioning when m_clipItem->y() is not 0 (LP:
20+ #1251597)
21+ * Fix some small qml warnings
22+
23+ [ Daniel d'Andrada ]
24+ * Add touch ownership logic on top of qt input handling
25+
26+ -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Mon, 13 Oct 2014 15:43:03 +0000
27+
1unity8 (8.00+14.10.20141009.4-0ubuntu1) utopic; urgency=low
28unity8 (8.00+14.10.20141009.4-0ubuntu1) utopic; urgency=low
2
29
3 [ Michał Sawicz ]
30 [ Michał Sawicz ]
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/Dash/listviewwithpageheader.cpp+1−1
@@ -851,7 +851,7 @@ ListViewWithPageHeader::ListItem *ListViewWithPageHeader::createItem(int modelIn
851 if (nextItem) {
851 if (nextItem) {
852 listItem->setY(nextItem->y() - listItem->height());
852 listItem->setY(nextItem->y() - listItem->height());
853 } else if (modelIndex == 0) {
853 } else if (modelIndex == 0) {
854− listItem->setY(m_headerItem ? m_headerItem->height() : 0);
854+ listItem->setY(-m_clipItem->y() + (m_headerItem ? m_headerItem->height() : 0));
855 } else if (!m_visibleItems.isEmpty()) {
855 } else if (!m_visibleItems.isEmpty()) {
856 lostItem = true;
856 lostItem = true;
857 }
857 }
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
po/ast.po+9−8
@@ -8,14 +8,14 @@ msgstr ""
8"Project-Id-Version: unity8\n"
8"Project-Id-Version: unity8\n"
9"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
9"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
10"POT-Creation-Date: 2014-10-07 11:37+0300\n"
10"POT-Creation-Date: 2014-10-07 11:37+0300\n"
11−"PO-Revision-Date: 2014-10-03 22:20+0000\n"
11+"PO-Revision-Date: 2014-10-09 09:49+0000\n"
12−"Last-Translator: Xuacu Saturio <xuacusk8@gmail.com>\n"
12+"Last-Translator: ivarela <ivarela@ubuntu.com>\n"
13"Language-Team: Asturian <ast@li.org>\n"
13"Language-Team: Asturian <ast@li.org>\n"
14"MIME-Version: 1.0\n"
14"MIME-Version: 1.0\n"
15"Content-Type: text/plain; charset=UTF-8\n"
15"Content-Type: text/plain; charset=UTF-8\n"
16"Content-Transfer-Encoding: 8bit\n"
16"Content-Transfer-Encoding: 8bit\n"
17"Plural-Forms: nplurals=2; plural=n != 1;\n"
17"Plural-Forms: nplurals=2; plural=n != 1;\n"
18−"X-Launchpad-Export-Date: 2014-10-09 06:36+0000\n"
18+"X-Launchpad-Export-Date: 2014-10-10 05:23+0000\n"
19"X-Generator: Launchpad (build 17196)\n"
19"X-Generator: Launchpad (build 17196)\n"
20
20
21#: plugins/LightDM/Greeter.cpp:130
21#: plugins/LightDM/Greeter.cpp:130
@@ -37,11 +37,12 @@ msgstr "Preséu bloquiáu"
37
37
38#: qml/Components/DelayedLockscreen.qml:62
38#: qml/Components/DelayedLockscreen.qml:62
39msgid "You have been locked out due to too many failed passphrase attempts."
39msgid "You have been locked out due to too many failed passphrase attempts."
40−msgstr ""
40+msgstr "Quedasti bloquiáu polos escesivos intentos fallíos de frase de pasu."
41
41
42#: qml/Components/DelayedLockscreen.qml:63
42#: qml/Components/DelayedLockscreen.qml:63
43msgid "You have been locked out due to too many failed passcode attempts."
43msgid "You have been locked out due to too many failed passcode attempts."
44msgstr ""
44msgstr ""
45+"Quedasti bloquiáu polos escesivos intentos fallíos de códigu de pasu."
45
46
46#: qml/Components/DelayedLockscreen.qml:72
47#: qml/Components/DelayedLockscreen.qml:72
47#, qt-format
48#, qt-format
@@ -213,11 +214,11 @@ msgstr "Unviar"
213
214
214#: qml/Dash/PullToRefreshScopeStyle.qml:55
215#: qml/Dash/PullToRefreshScopeStyle.qml:55
215msgid "Pull to refresh…"
216msgid "Pull to refresh…"
216−msgstr ""
217+msgstr "Tirar p'anovar..."
217
218
218#: qml/Dash/PullToRefreshScopeStyle.qml:60
219#: qml/Dash/PullToRefreshScopeStyle.qml:60
219msgid "Release to refresh…"
220msgid "Release to refresh…"
220−msgstr ""
221+msgstr "Soltar p'anovar..."
221
222
222#: qml/Dash/ScopesOverview.qml:206
223#: qml/Dash/ScopesOverview.qml:206
223msgid "Manage Scopes"
224msgid "Manage Scopes"
@@ -313,7 +314,7 @@ msgstr "Roaming"
313
314
314#: qml/Shell.qml:346
315#: qml/Shell.qml:346
315msgid "Enter passphrase"
316msgid "Enter passphrase"
316−msgstr ""
317+msgstr "Escribi la frase contraseña"
317
318
318#: qml/Shell.qml:347
319#: qml/Shell.qml:347
319msgid "Sorry, incorrect passphrase"
320msgid "Sorry, incorrect passphrase"
@@ -325,7 +326,7 @@ msgstr "Vuelvi a escribila"
325
326
326#: qml/Shell.qml:350
327#: qml/Shell.qml:350
327msgid "Enter passcode"
328msgid "Enter passcode"
328−msgstr ""
329+msgstr "Escribi'l códigu de pasu"
329
330
330#: qml/Shell.qml:351
331#: qml/Shell.qml:351
331msgid "Sorry, incorrect passcode"
332msgid "Sorry, incorrect passcode"
po/ca@valencia.ponew file+360
@@ -0,0 +1,360 @@
1+# Catalan (Valencian) translation for unity8
2+# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014
3+# This file is distributed under the same license as the unity8 package.
4+# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
5+#
6+msgid ""
7+msgstr ""
8+"Project-Id-Version: unity8\n"
9+"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
10+"POT-Creation-Date: 2014-10-07 11:37+0300\n"
11+"PO-Revision-Date: 2014-10-15 05:58+0000\n"
12+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13+"Language-Team: Catalan (Valencian) <ca@valencia@li.org>\n"
14+"MIME-Version: 1.0\n"
15+"Content-Type: text/plain; charset=UTF-8\n"
16+"Content-Transfer-Encoding: 8bit\n"
17+"Plural-Forms: nplurals=2; plural=n != 1;\n"
18+"X-Launchpad-Export-Date: 2014-10-15 06:15+0000\n"
19+"X-Generator: Launchpad (build 17196)\n"
20+
21+#: plugins/LightDM/Greeter.cpp:130
22+msgid "Password: "
23+msgstr ""
24+
25+#: plugins/Unity/Launcher/launcheritem.cpp:45
26+#: plugins/Unity/Launcher/launcheritem.cpp:91
27+msgid "Pin shortcut"
28+msgstr ""
29+
30+#: plugins/Unity/Launcher/launcheritem.cpp:91
31+msgid "Unpin shortcut"
32+msgstr ""
33+
34+#: qml/Components/DelayedLockscreen.qml:47
35+msgid "Device Locked"
36+msgstr ""
37+
38+#: qml/Components/DelayedLockscreen.qml:62
39+msgid "You have been locked out due to too many failed passphrase attempts."
40+msgstr ""
41+
42+#: qml/Components/DelayedLockscreen.qml:63
43+msgid "You have been locked out due to too many failed passcode attempts."
44+msgstr ""
45+
46+#: qml/Components/DelayedLockscreen.qml:72
47+#, qt-format
48+msgid "Please wait %1 minute and then try again…"
49+msgid_plural "Please wait %1 minutes and then try again…"
50+msgstr[0] ""
51+msgstr[1] ""
52+
53+#: qml/Components/Dialogs.qml:70
54+msgid "Log out"
55+msgstr ""
56+
57+#: qml/Components/Dialogs.qml:71
58+msgid "Are you sure you want to log out?"
59+msgstr ""
60+
61+#: qml/Components/Dialogs.qml:73 qml/Components/Dialogs.qml:97
62+#: qml/Components/Dialogs.qml:122
63+msgid "No"
64+msgstr ""
65+
66+#: qml/Components/Dialogs.qml:80 qml/Components/Dialogs.qml:104
67+#: qml/Components/Dialogs.qml:129
68+msgid "Yes"
69+msgstr ""
70+
71+#: qml/Components/Dialogs.qml:94
72+msgid "Shut down"
73+msgstr ""
74+
75+#: qml/Components/Dialogs.qml:95
76+msgid "Are you sure you want to shut down?"
77+msgstr ""
78+
79+#: qml/Components/Dialogs.qml:119
80+msgid "Reboot"
81+msgstr ""
82+
83+#: qml/Components/Dialogs.qml:120
84+msgid "Are you sure you want to reboot?"
85+msgstr ""
86+
87+#: qml/Components/Dialogs.qml:144
88+msgid "Power"
89+msgstr ""
90+
91+#: qml/Components/Dialogs.qml:145
92+msgid ""
93+"Are you sure you would like\n"
94+"to power off?"
95+msgstr ""
96+
97+#: qml/Components/Dialogs.qml:147
98+msgid "Power off"
99+msgstr ""
100+
101+#: qml/Components/Dialogs.qml:157
102+msgid "Restart"
103+msgstr ""
104+
105+#: qml/Components/Dialogs.qml:167
106+msgid "Cancel"
107+msgstr ""
108+
109+#: qml/Components/EdgeDemoOverlay.qml:151
110+msgid "Skip intro"
111+msgstr ""
112+
113+#: qml/Components/EdgeDemo.qml:112
114+msgid "Right edge"
115+msgstr ""
116+
117+#: qml/Components/EdgeDemo.qml:113
118+msgid "Try swiping from the right edge to unlock the phone"
119+msgstr ""
120+
121+#: qml/Components/EdgeDemo.qml:144
122+msgid "Top edge"
123+msgstr ""
124+
125+#: qml/Components/EdgeDemo.qml:145
126+msgid "Try swiping from the top edge to access the indicators"
127+msgstr ""
128+
129+#: qml/Components/EdgeDemo.qml:170
130+msgid "Close"
131+msgstr ""
132+
133+#: qml/Components/EdgeDemo.qml:171
134+msgid "Swipe up again to close the settings screen"
135+msgstr ""
136+
137+#: qml/Components/EdgeDemo.qml:198
138+msgid "Left edge"
139+msgstr ""
140+
141+#: qml/Components/EdgeDemo.qml:199
142+msgid "Swipe from the left to reveal the launcher for quick access to apps"
143+msgstr ""
144+
145+#: qml/Components/EdgeDemo.qml:226
146+msgid "Well done"
147+msgstr ""
148+
149+#: qml/Components/EdgeDemo.qml:227
150+msgid ""
151+"You have now mastered the edge gestures and can start using the "
152+"phone<br><br>Tap on the screen to start"
153+msgstr ""
154+
155+#: qml/Components/Lockscreen.qml:220
156+msgid "Emergency Call"
157+msgstr ""
158+
159+#: qml/Components/Lockscreen.qml:252
160+msgid "OK"
161+msgstr ""
162+
163+#: qml/Dash/GenericScopeView.qml:434 qml/Dash/GenericScopeView.qml:591
164+msgid "See less"
165+msgstr ""
166+
167+#: qml/Dash/GenericScopeView.qml:434
168+msgid "See all"
169+msgstr ""
170+
171+#: qml/Dash/GenericScopeView.qml:496 qml/Dash/PageHeader.qml:258
172+#: qml/Panel/SearchIndicator.qml:27
173+msgid "Search"
174+msgstr ""
175+
176+#: qml/Dash/PageHeader.qml:268
177+msgid "Settings"
178+msgstr ""
179+
180+#: qml/Dash/PageHeader.qml:275
181+msgid "Remove from Favorites"
182+msgstr ""
183+
184+#: qml/Dash/PageHeader.qml:275
185+msgid "Add to Favorites"
186+msgstr ""
187+
188+#: qml/Dash/Previews/PreviewActionCombo.qml:35
189+msgid "More..."
190+msgstr ""
191+
192+#: qml/Dash/Previews/PreviewActionCombo.qml:35
193+msgid "Less..."
194+msgstr ""
195+
196+#: qml/Dash/Previews/PreviewRatingInput.qml:81
197+msgid "Rate this"
198+msgstr ""
199+
200+#: qml/Dash/Previews/PreviewRatingInput.qml:126
201+msgid "Add a review"
202+msgstr ""
203+
204+#: qml/Dash/Previews/PreviewRatingInput.qml:166
205+msgid "Send"
206+msgstr ""
207+
208+#: qml/Dash/PullToRefreshScopeStyle.qml:55
209+msgid "Pull to refresh…"
210+msgstr ""
211+
212+#: qml/Dash/PullToRefreshScopeStyle.qml:60
213+msgid "Release to refresh…"
214+msgstr ""
215+
216+#: qml/Dash/ScopesOverview.qml:206
217+msgid "Manage Scopes"
218+msgstr ""
219+
220+#: qml/Dash/ScopesOverview.qml:428
221+msgid "Done"
222+msgstr ""
223+
224+#: qml/Dash/ScopesOverview.qml:454
225+msgid "Store"
226+msgstr ""
227+
228+#: qml/Dash/ScopesOverviewTab.qml:36
229+msgid "Favorites"
230+msgstr ""
231+
232+#: qml/Dash/ScopesOverviewTab.qml:54
233+msgid "All"
234+msgstr ""
235+
236+#: qml/Greeter/Greeter.qml:157
237+msgid "Unlock"
238+msgstr ""
239+
240+#: qml/Hud/HudParametrizedActionsPage.qml:139
241+msgid "Confirm"
242+msgstr ""
243+
244+#: qml/Hud/Hud.qml:61 qml/Hud/Hud.qml:99 qml/Hud/Hud.qml:372
245+msgid "Type or say a command"
246+msgstr ""
247+
248+#: qml/Hud/Hud.qml:84
249+msgid "Loading. Please Wait..."
250+msgstr ""
251+
252+#: qml/Hud/Hud.qml:89
253+msgid "Speak Now..."
254+msgstr ""
255+
256+#: qml/Hud/Hud.qml:94
257+msgid "Speaking..."
258+msgstr ""
259+
260+#: qml/Notifications/NotificationMenuItemFactory.qml:116
261+msgid "Show password"
262+msgstr ""
263+
264+#: qml/Panel/ActiveCallHint.qml:77
265+msgid "Tap to return to call..."
266+msgstr ""
267+
268+#: qml/Panel/ActiveCallHint.qml:90
269+msgid "Conference"
270+msgstr ""
271+
272+#: qml/Panel/Indicators/MenuItemFactory.qml:600
273+msgid "Nothing is playing"
274+msgstr ""
275+
276+#: qml/Panel/Indicators/MenuItemFactory.qml:748
277+msgid "In queue…"
278+msgstr ""
279+
280+#: qml/Panel/Indicators/MenuItemFactory.qml:752
281+msgid "Downloading"
282+msgstr ""
283+
284+#: qml/Panel/Indicators/MenuItemFactory.qml:754
285+msgid "Paused, tap to resume"
286+msgstr ""
287+
288+#: qml/Panel/Indicators/MenuItemFactory.qml:756
289+msgid "Canceled"
290+msgstr ""
291+
292+#: qml/Panel/Indicators/MenuItemFactory.qml:758
293+msgid "Finished"
294+msgstr ""
295+
296+#: qml/Panel/Indicators/MenuItemFactory.qml:760
297+msgid "Failed, tap to retry"
298+msgstr ""
299+
300+#: qml/Panel/Indicators/ModemInfoItem.qml:105
301+msgid "Unlock SIM"
302+msgstr ""
303+
304+#: qml/Panel/Indicators/RoamingIndication.qml:27
305+msgid "Roaming"
306+msgstr ""
307+
308+#: qml/Shell.qml:346
309+msgid "Enter passphrase"
310+msgstr ""
311+
312+#: qml/Shell.qml:347
313+msgid "Sorry, incorrect passphrase"
314+msgstr ""
315+
316+#: qml/Shell.qml:348
317+msgid "Please re-enter"
318+msgstr ""
319+
320+#: qml/Shell.qml:350
321+msgid "Enter passcode"
322+msgstr ""
323+
324+#: qml/Shell.qml:351
325+msgid "Sorry, incorrect passcode"
326+msgstr ""
327+
328+#: qml/Shell.qml:354
329+#, qt-format
330+msgid "Enter %1"
331+msgstr ""
332+
333+#: qml/Shell.qml:355
334+#, qt-format
335+msgid "Sorry, incorrect %1"
336+msgstr ""
337+
338+#: qml/Shell.qml:390
339+msgid "Sorry, incorrect passphrase."
340+msgstr ""
341+
342+#: qml/Shell.qml:391
343+msgid "Sorry, incorrect passcode."
344+msgstr ""
345+
346+#: qml/Shell.qml:392
347+msgid "This will be your last attempt."
348+msgstr ""
349+
350+#: qml/Shell.qml:394
351+msgid ""
352+"If passphrase is entered incorrectly, your phone will conduct a factory "
353+"reset and all personal data will be deleted."
354+msgstr ""
355+
356+#: qml/Shell.qml:395
357+msgid ""
358+"If passcode is entered incorrectly, your phone will conduct a factory reset "
359+"and all personal data will be deleted."
360+msgstr ""
po/en_AU.po+16−22
@@ -8,14 +8,14 @@ msgstr ""
8"Project-Id-Version: unity\n"
8"Project-Id-Version: unity\n"
9"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
9"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
10"POT-Creation-Date: 2014-10-07 11:37+0300\n"
10"POT-Creation-Date: 2014-10-07 11:37+0300\n"
11−"PO-Revision-Date: 2014-09-23 01:24+0000\n"
11+"PO-Revision-Date: 2014-10-12 09:41+0000\n"
12"Last-Translator: Jared Norris <jarednorris@ubuntu.com>\n"
12"Last-Translator: Jared Norris <jarednorris@ubuntu.com>\n"
13"Language-Team: English (Australia) <en_AU@li.org>\n"
13"Language-Team: English (Australia) <en_AU@li.org>\n"
14"MIME-Version: 1.0\n"
14"MIME-Version: 1.0\n"
15"Content-Type: text/plain; charset=UTF-8\n"
15"Content-Type: text/plain; charset=UTF-8\n"
16"Content-Transfer-Encoding: 8bit\n"
16"Content-Transfer-Encoding: 8bit\n"
17"Plural-Forms: nplurals=2; plural=n != 1;\n"
17"Plural-Forms: nplurals=2; plural=n != 1;\n"
18−"X-Launchpad-Export-Date: 2014-10-09 06:37+0000\n"
18+"X-Launchpad-Export-Date: 2014-10-13 06:15+0000\n"
19"X-Generator: Launchpad (build 17196)\n"
19"X-Generator: Launchpad (build 17196)\n"
20
20
21#: plugins/LightDM/Greeter.cpp:130
21#: plugins/LightDM/Greeter.cpp:130
@@ -33,22 +33,22 @@ msgstr "Unpin shortcut"
33
33
34#: qml/Components/DelayedLockscreen.qml:47
34#: qml/Components/DelayedLockscreen.qml:47
35msgid "Device Locked"
35msgid "Device Locked"
36−msgstr ""
36+msgstr "Device Locked"
37
37
38#: qml/Components/DelayedLockscreen.qml:62
38#: qml/Components/DelayedLockscreen.qml:62
39msgid "You have been locked out due to too many failed passphrase attempts."
39msgid "You have been locked out due to too many failed passphrase attempts."
40−msgstr ""
40+msgstr "You have been locked out due to too many failed passphrase attempts."
41
41
42#: qml/Components/DelayedLockscreen.qml:63
42#: qml/Components/DelayedLockscreen.qml:63
43msgid "You have been locked out due to too many failed passcode attempts."
43msgid "You have been locked out due to too many failed passcode attempts."
44−msgstr ""
44+msgstr "You have been locked out due to too many failed passcode attempts."
45
45
46#: qml/Components/DelayedLockscreen.qml:72
46#: qml/Components/DelayedLockscreen.qml:72
47#, qt-format
47#, qt-format
48msgid "Please wait %1 minute and then try again…"
48msgid "Please wait %1 minute and then try again…"
49msgid_plural "Please wait %1 minutes and then try again…"
49msgid_plural "Please wait %1 minutes and then try again…"
50−msgstr[0] ""
50+msgstr[0] "Please wait %1 minute and then try again…"
51−msgstr[1] ""
51+msgstr[1] "Please wait %1 minutes and then try again…"
52
52
53#: qml/Components/Dialogs.qml:70
53#: qml/Components/Dialogs.qml:70
54msgid "Log out"
54msgid "Log out"
@@ -211,15 +211,15 @@ msgstr "Send"
211
211
212#: qml/Dash/PullToRefreshScopeStyle.qml:55
212#: qml/Dash/PullToRefreshScopeStyle.qml:55
213msgid "Pull to refresh…"
213msgid "Pull to refresh…"
214−msgstr ""
214+msgstr "Pull to refresh…"
215
215
216#: qml/Dash/PullToRefreshScopeStyle.qml:60
216#: qml/Dash/PullToRefreshScopeStyle.qml:60
217msgid "Release to refresh…"
217msgid "Release to refresh…"
218−msgstr ""
218+msgstr "Release to refresh…"
219
219
220#: qml/Dash/ScopesOverview.qml:206
220#: qml/Dash/ScopesOverview.qml:206
221msgid "Manage Scopes"
221msgid "Manage Scopes"
222−msgstr ""
222+msgstr "Manage Scopes"
223
223
224#: qml/Dash/ScopesOverview.qml:428
224#: qml/Dash/ScopesOverview.qml:428
225msgid "Done"
225msgid "Done"
@@ -239,7 +239,7 @@ msgstr "All"
239
239
240#: qml/Greeter/Greeter.qml:157
240#: qml/Greeter/Greeter.qml:157
241msgid "Unlock"
241msgid "Unlock"
242−msgstr ""
242+msgstr "Unlock"
243
243
244#: qml/Hud/HudParametrizedActionsPage.qml:139
244#: qml/Hud/HudParametrizedActionsPage.qml:139
245msgid "Confirm"
245msgid "Confirm"
@@ -275,7 +275,7 @@ msgstr "Conference"
275
275
276#: qml/Panel/Indicators/MenuItemFactory.qml:600
276#: qml/Panel/Indicators/MenuItemFactory.qml:600
277msgid "Nothing is playing"
277msgid "Nothing is playing"
278−msgstr ""
278+msgstr "Nothing is playing"
279
279
280#: qml/Panel/Indicators/MenuItemFactory.qml:748
280#: qml/Panel/Indicators/MenuItemFactory.qml:748
281msgid "In queue…"
281msgid "In queue…"
@@ -311,7 +311,7 @@ msgstr "Roaming"
311
311
312#: qml/Shell.qml:346
312#: qml/Shell.qml:346
313msgid "Enter passphrase"
313msgid "Enter passphrase"
314−msgstr ""
314+msgstr "Enter passphrase"
315
315
316#: qml/Shell.qml:347
316#: qml/Shell.qml:347
317msgid "Sorry, incorrect passphrase"
317msgid "Sorry, incorrect passphrase"
@@ -319,11 +319,11 @@ msgstr "Sorry, incorrect passphrase"
319
319
320#: qml/Shell.qml:348
320#: qml/Shell.qml:348
321msgid "Please re-enter"
321msgid "Please re-enter"
322−msgstr ""
322+msgstr "Please re-enter"
323
323
324#: qml/Shell.qml:350
324#: qml/Shell.qml:350
325msgid "Enter passcode"
325msgid "Enter passcode"
326−msgstr ""
326+msgstr "Enter passcode"
327
327
328#: qml/Shell.qml:351
328#: qml/Shell.qml:351
329msgid "Sorry, incorrect passcode"
329msgid "Sorry, incorrect passcode"
@@ -332,7 +332,7 @@ msgstr "Sorry, incorrect passcode"
332#: qml/Shell.qml:354
332#: qml/Shell.qml:354
333#, qt-format
333#, qt-format
334msgid "Enter %1"
334msgid "Enter %1"
335−msgstr ""
335+msgstr "Enter %1"
336
336
337#: qml/Shell.qml:355
337#: qml/Shell.qml:355
338#, qt-format
338#, qt-format
@@ -366,9 +366,3 @@ msgid ""
366msgstr ""
366msgstr ""
367"If passcode is entered incorrectly, your phone will conduct a factory reset "
367"If passcode is entered incorrectly, your phone will conduct a factory reset "
368"and all personal data will be deleted."
368"and all personal data will be deleted."
369−
370−#~ msgid "Sorry, incorrect PIN"
371−#~ msgstr "Sorry, incorrect PIN"
372−
373−#~ msgid "Enter SIM PIN"
374−#~ msgstr "Enter SIM PIN"
po/es.po+5−5
@@ -8,14 +8,14 @@ msgstr ""
8"Project-Id-Version: unity\n"
8"Project-Id-Version: unity\n"
9"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
9"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
10"POT-Creation-Date: 2014-10-07 11:37+0300\n"
10"POT-Creation-Date: 2014-10-07 11:37+0300\n"
11−"PO-Revision-Date: 2014-10-05 08:50+0000\n"
11+"PO-Revision-Date: 2014-10-09 07:43+0000\n"
12−"Last-Translator: Paco Molinero <paco@byasl.com>\n"
12+"Last-Translator: Adolfo Jayme <fitoschido@gmail.com>\n"
13"Language-Team: Spanish <es@li.org>\n"
13"Language-Team: Spanish <es@li.org>\n"
14"MIME-Version: 1.0\n"
14"MIME-Version: 1.0\n"
15"Content-Type: text/plain; charset=UTF-8\n"
15"Content-Type: text/plain; charset=UTF-8\n"
16"Content-Transfer-Encoding: 8bit\n"
16"Content-Transfer-Encoding: 8bit\n"
17"Plural-Forms: nplurals=2; plural=n != 1;\n"
17"Plural-Forms: nplurals=2; plural=n != 1;\n"
18−"X-Launchpad-Export-Date: 2014-10-09 06:36+0000\n"
18+"X-Launchpad-Export-Date: 2014-10-10 05:23+0000\n"
19"X-Generator: Launchpad (build 17196)\n"
19"X-Generator: Launchpad (build 17196)\n"
20
20
21#: plugins/LightDM/Greeter.cpp:130
21#: plugins/LightDM/Greeter.cpp:130
@@ -218,11 +218,11 @@ msgstr "Enviar"
218
218
219#: qml/Dash/PullToRefreshScopeStyle.qml:55
219#: qml/Dash/PullToRefreshScopeStyle.qml:55
220msgid "Pull to refresh…"
220msgid "Pull to refresh…"
221−msgstr ""
221+msgstr "Deslice para actualizar…"
222
222
223#: qml/Dash/PullToRefreshScopeStyle.qml:60
223#: qml/Dash/PullToRefreshScopeStyle.qml:60
224msgid "Release to refresh…"
224msgid "Release to refresh…"
225−msgstr ""
225+msgstr "Suelte para actualizar…"
226
226
227#: qml/Dash/ScopesOverview.qml:206
227#: qml/Dash/ScopesOverview.qml:206
228msgid "Manage Scopes"
228msgid "Manage Scopes"
po/fa.po+4−10
@@ -8,14 +8,14 @@ msgstr ""
8"Project-Id-Version: unity8\n"
8"Project-Id-Version: unity8\n"
9"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
9"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
10"POT-Creation-Date: 2014-10-07 11:37+0300\n"
10"POT-Creation-Date: 2014-10-07 11:37+0300\n"
11−"PO-Revision-Date: 2014-10-02 23:07+0000\n"
11+"PO-Revision-Date: 2014-10-11 23:41+0000\n"
12"Last-Translator: Danial Behzadi <dani.behzi@gmail.com>\n"
12"Last-Translator: Danial Behzadi <dani.behzi@gmail.com>\n"
13"Language-Team: Persian <fa@li.org>\n"
13"Language-Team: Persian <fa@li.org>\n"
14"MIME-Version: 1.0\n"
14"MIME-Version: 1.0\n"
15"Content-Type: text/plain; charset=UTF-8\n"
15"Content-Type: text/plain; charset=UTF-8\n"
16"Content-Transfer-Encoding: 8bit\n"
16"Content-Transfer-Encoding: 8bit\n"
17"Plural-Forms: nplurals=1; plural=0;\n"
17"Plural-Forms: nplurals=1; plural=0;\n"
18−"X-Launchpad-Export-Date: 2014-10-09 06:36+0000\n"
18+"X-Launchpad-Export-Date: 2014-10-12 06:55+0000\n"
19"X-Generator: Launchpad (build 17196)\n"
19"X-Generator: Launchpad (build 17196)\n"
20
20
21#: plugins/LightDM/Greeter.cpp:130
21#: plugins/LightDM/Greeter.cpp:130
@@ -211,11 +211,11 @@ msgstr "ارسال"
211
211
212#: qml/Dash/PullToRefreshScopeStyle.qml:55
212#: qml/Dash/PullToRefreshScopeStyle.qml:55
213msgid "Pull to refresh…"
213msgid "Pull to refresh…"
214−msgstr ""
214+msgstr "برای تازه‌سازی بکشید…"
215
215
216#: qml/Dash/PullToRefreshScopeStyle.qml:60
216#: qml/Dash/PullToRefreshScopeStyle.qml:60
217msgid "Release to refresh…"
217msgid "Release to refresh…"
218−msgstr ""
218+msgstr "برای تازه‌سازی رها کنید…"
219
219
220#: qml/Dash/ScopesOverview.qml:206
220#: qml/Dash/ScopesOverview.qml:206
221msgid "Manage Scopes"
221msgid "Manage Scopes"
@@ -366,9 +366,3 @@ msgid ""
366msgstr ""
366msgstr ""
367"اگر رمزعبور نادرست وارد شود، تلفن شما بازنشانی کارخانه خواهد شد و تمامی "
367"اگر رمزعبور نادرست وارد شود، تلفن شما بازنشانی کارخانه خواهد شد و تمامی "
368"اطّلاعات شخصی حذف خواهند شد."
368"اطّلاعات شخصی حذف خواهند شد."
369−
370−#~ msgid "Sorry, incorrect PIN"
371−#~ msgstr "متأسّفیم، PIN نادرست بود"
372−
373−#~ msgid "Enter SIM PIN"
374−#~ msgstr "PIN سیم‌کارت را وارد کنید"
po/fr.po+5−5
@@ -8,14 +8,14 @@ msgstr ""
8"Project-Id-Version: unity\n"
8"Project-Id-Version: unity\n"
9"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
9"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
10"POT-Creation-Date: 2014-10-07 11:37+0300\n"
10"POT-Creation-Date: 2014-10-07 11:37+0300\n"
11−"PO-Revision-Date: 2014-10-06 19:06+0000\n"
11+"PO-Revision-Date: 2014-10-09 19:00+0000\n"
12−"Last-Translator: Jean Marc <Unknown>\n"
12+"Last-Translator: Anne <anneonyme017@gmail.com>\n"
13"Language-Team: French <fr@li.org>\n"
13"Language-Team: French <fr@li.org>\n"
14"MIME-Version: 1.0\n"
14"MIME-Version: 1.0\n"
15"Content-Type: text/plain; charset=UTF-8\n"
15"Content-Type: text/plain; charset=UTF-8\n"
16"Content-Transfer-Encoding: 8bit\n"
16"Content-Transfer-Encoding: 8bit\n"
17"Plural-Forms: nplurals=2; plural=n > 1;\n"
17"Plural-Forms: nplurals=2; plural=n > 1;\n"
18−"X-Launchpad-Export-Date: 2014-10-09 06:36+0000\n"
18+"X-Launchpad-Export-Date: 2014-10-10 05:23+0000\n"
19"X-Generator: Launchpad (build 17196)\n"
19"X-Generator: Launchpad (build 17196)\n"
20
20
21#: plugins/LightDM/Greeter.cpp:130
21#: plugins/LightDM/Greeter.cpp:130
@@ -222,11 +222,11 @@ msgstr "Envoyer"
222
222
223#: qml/Dash/PullToRefreshScopeStyle.qml:55
223#: qml/Dash/PullToRefreshScopeStyle.qml:55
224msgid "Pull to refresh…"
224msgid "Pull to refresh…"
225−msgstr ""
225+msgstr "Tirer pour actualiser..."
226
226
227#: qml/Dash/PullToRefreshScopeStyle.qml:60
227#: qml/Dash/PullToRefreshScopeStyle.qml:60
228msgid "Release to refresh…"
228msgid "Release to refresh…"
229−msgstr ""
229+msgstr "Relâcher pour actualiser..."
230
230
231#: qml/Dash/ScopesOverview.qml:206
231#: qml/Dash/ScopesOverview.qml:206
232msgid "Manage Scopes"
232msgid "Manage Scopes"
po/gd.po+4−4
@@ -8,7 +8,7 @@ msgstr ""
8"Project-Id-Version: unity8\n"
8"Project-Id-Version: unity8\n"
9"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
9"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
10"POT-Creation-Date: 2014-10-07 11:37+0300\n"
10"POT-Creation-Date: 2014-10-07 11:37+0300\n"
11−"PO-Revision-Date: 2014-10-03 19:54+0000\n"
11+"PO-Revision-Date: 2014-10-09 18:09+0000\n"
12"Last-Translator: GunChleoc <Unknown>\n"
12"Last-Translator: GunChleoc <Unknown>\n"
13"Language-Team: Fòram na Gàidhlig\n"
13"Language-Team: Fòram na Gàidhlig\n"
14"MIME-Version: 1.0\n"
14"MIME-Version: 1.0\n"
@@ -16,7 +16,7 @@ msgstr ""
16"Content-Transfer-Encoding: 8bit\n"
16"Content-Transfer-Encoding: 8bit\n"
17"Plural-Forms: nplurals=4; plural=(n==1 || n==11) ? 0 : (n==2 || n==12) ? 1 : "
17"Plural-Forms: nplurals=4; plural=(n==1 || n==11) ? 0 : (n==2 || n==12) ? 1 : "
18"(n > 2 && n < 20) ? 2 : 3;\n"
18"(n > 2 && n < 20) ? 2 : 3;\n"
19−"X-Launchpad-Export-Date: 2014-10-09 06:36+0000\n"
19+"X-Launchpad-Export-Date: 2014-10-10 05:23+0000\n"
20"X-Generator: Launchpad (build 17196)\n"
20"X-Generator: Launchpad (build 17196)\n"
21"Language: gd\n"
21"Language: gd\n"
22
22
@@ -225,11 +225,11 @@ msgstr "Cuir"
225
225
226#: qml/Dash/PullToRefreshScopeStyle.qml:55
226#: qml/Dash/PullToRefreshScopeStyle.qml:55
227msgid "Pull to refresh…"
227msgid "Pull to refresh…"
228−msgstr ""
228+msgstr "Tarraing airson ath-nuadhachadh…"
229
229
230#: qml/Dash/PullToRefreshScopeStyle.qml:60
230#: qml/Dash/PullToRefreshScopeStyle.qml:60
231msgid "Release to refresh…"
231msgid "Release to refresh…"
232−msgstr ""
232+msgstr "Leig às airson ath-nuadhachadh…"
233
233
234#: qml/Dash/ScopesOverview.qml:206
234#: qml/Dash/ScopesOverview.qml:206
235msgid "Manage Scopes"
235msgid "Manage Scopes"
po/hu.po+3−9
@@ -8,14 +8,14 @@ msgstr ""
8"Project-Id-Version: unity\n"
8"Project-Id-Version: unity\n"
9"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
9"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
10"POT-Creation-Date: 2014-10-07 11:37+0300\n"
10"POT-Creation-Date: 2014-10-07 11:37+0300\n"
11−"PO-Revision-Date: 2014-10-08 08:13+0000\n"
11+"PO-Revision-Date: 2014-10-11 10:50+0000\n"
12"Last-Translator: Richard Somlói <ricsipontaz@gmail.com>\n"
12"Last-Translator: Richard Somlói <ricsipontaz@gmail.com>\n"
13"Language-Team: Hungarian <hu@li.org>\n"
13"Language-Team: Hungarian <hu@li.org>\n"
14"MIME-Version: 1.0\n"
14"MIME-Version: 1.0\n"
15"Content-Type: text/plain; charset=UTF-8\n"
15"Content-Type: text/plain; charset=UTF-8\n"
16"Content-Transfer-Encoding: 8bit\n"
16"Content-Transfer-Encoding: 8bit\n"
17"Plural-Forms: nplurals=2; plural=n != 1;\n"
17"Plural-Forms: nplurals=2; plural=n != 1;\n"
18−"X-Launchpad-Export-Date: 2014-10-09 06:36+0000\n"
18+"X-Launchpad-Export-Date: 2014-10-12 06:55+0000\n"
19"X-Generator: Launchpad (build 17196)\n"
19"X-Generator: Launchpad (build 17196)\n"
20
20
21#: plugins/LightDM/Greeter.cpp:130
21#: plugins/LightDM/Greeter.cpp:130
@@ -321,7 +321,7 @@ msgstr "Sajnáljuk, helytelen jelmondat"
321
321
322#: qml/Shell.qml:348
322#: qml/Shell.qml:348
323msgid "Please re-enter"
323msgid "Please re-enter"
324−msgstr "Kérjük adja meg újra"
324+msgstr "Kérjük próbálja újra"
325
325
326#: qml/Shell.qml:350
326#: qml/Shell.qml:350
327msgid "Enter passcode"
327msgid "Enter passcode"
@@ -368,9 +368,3 @@ msgid ""
368msgstr ""
368msgstr ""
369"Ha helytelen jelkódot ad meg, a telefonja visszaállítja a gyári "
369"Ha helytelen jelkódot ad meg, a telefonja visszaállítja a gyári "
370"beállításokat és törli minden személyes adatát."
370"beállításokat és törli minden személyes adatát."
371−
372−#~ msgid "Sorry, incorrect PIN"
373−#~ msgstr "Sajnáljuk, helytelen PIN"
374−
375−#~ msgid "Enter SIM PIN"
376−#~ msgstr "Adja meg a SIM PIN kódját"
po/is.po+9−9
@@ -10,14 +10,14 @@ msgstr ""
10"Project-Id-Version: po_unity8-is\n"
10"Project-Id-Version: po_unity8-is\n"
11"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
11"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
12"POT-Creation-Date: 2014-10-07 11:37+0300\n"
12"POT-Creation-Date: 2014-10-07 11:37+0300\n"
13−"PO-Revision-Date: 2014-10-05 08:44+0000\n"
13+"PO-Revision-Date: 2014-10-09 10:49+0000\n"
14"Last-Translator: Sveinn í Felli <sv1@fellsnet.is>\n"
14"Last-Translator: Sveinn í Felli <sv1@fellsnet.is>\n"
15"Language-Team: Icelandic <translation-team-is@lists.sourceforge.net>\n"
15"Language-Team: Icelandic <translation-team-is@lists.sourceforge.net>\n"
16"MIME-Version: 1.0\n"
16"MIME-Version: 1.0\n"
17"Content-Type: text/plain; charset=UTF-8\n"
17"Content-Type: text/plain; charset=UTF-8\n"
18"Content-Transfer-Encoding: 8bit\n"
18"Content-Transfer-Encoding: 8bit\n"
19"Plural-Forms: nplurals=2; plural=n != 1;\n"
19"Plural-Forms: nplurals=2; plural=n != 1;\n"
20−"X-Launchpad-Export-Date: 2014-10-09 06:36+0000\n"
20+"X-Launchpad-Export-Date: 2014-10-10 05:23+0000\n"
21"X-Generator: Launchpad (build 17196)\n"
21"X-Generator: Launchpad (build 17196)\n"
22"Language: is\n"
22"Language: is\n"
23
23
@@ -36,7 +36,7 @@ msgstr "Losa flýtilykil"
36
36
37#: qml/Components/DelayedLockscreen.qml:47
37#: qml/Components/DelayedLockscreen.qml:47
38msgid "Device Locked"
38msgid "Device Locked"
39−msgstr ""
39+msgstr "Tæki læst"
40
40
41#: qml/Components/DelayedLockscreen.qml:62
41#: qml/Components/DelayedLockscreen.qml:62
42msgid "You have been locked out due to too many failed passphrase attempts."
42msgid "You have been locked out due to too many failed passphrase attempts."
@@ -50,8 +50,8 @@ msgstr ""
50#, qt-format
50#, qt-format
51msgid "Please wait %1 minute and then try again…"
51msgid "Please wait %1 minute and then try again…"
52msgid_plural "Please wait %1 minutes and then try again…"
52msgid_plural "Please wait %1 minutes and then try again…"
53−msgstr[0] ""
53+msgstr[0] "Bíddu í %1 mínútu og reyndu svo aftur"
54−msgstr[1] ""
54+msgstr[1] "Bíddu í %1 mínútur og reyndu svo aftur"
55
55
56#: qml/Components/Dialogs.qml:70
56#: qml/Components/Dialogs.qml:70
57msgid "Log out"
57msgid "Log out"
@@ -219,7 +219,7 @@ msgstr ""
219
219
220#: qml/Dash/PullToRefreshScopeStyle.qml:60
220#: qml/Dash/PullToRefreshScopeStyle.qml:60
221msgid "Release to refresh…"
221msgid "Release to refresh…"
222−msgstr ""
222+msgstr "Slepptu til að endurlesa..."
223
223
224#: qml/Dash/ScopesOverview.qml:206
224#: qml/Dash/ScopesOverview.qml:206
225msgid "Manage Scopes"
225msgid "Manage Scopes"
@@ -243,7 +243,7 @@ msgstr "Allt"
243
243
244#: qml/Greeter/Greeter.qml:157
244#: qml/Greeter/Greeter.qml:157
245msgid "Unlock"
245msgid "Unlock"
246−msgstr ""
246+msgstr "Aflæsa"
247
247
248#: qml/Hud/HudParametrizedActionsPage.qml:139
248#: qml/Hud/HudParametrizedActionsPage.qml:139
249msgid "Confirm"
249msgid "Confirm"
@@ -323,7 +323,7 @@ msgstr "Því miður, rangt lykilorð"
323
323
324#: qml/Shell.qml:348
324#: qml/Shell.qml:348
325msgid "Please re-enter"
325msgid "Please re-enter"
326−msgstr ""
326+msgstr "Settu aftur inn"
327
327
328#: qml/Shell.qml:350
328#: qml/Shell.qml:350
329msgid "Enter passcode"
329msgid "Enter passcode"
@@ -336,7 +336,7 @@ msgstr "Því miður, rangur aðgangskóði"
336#: qml/Shell.qml:354
336#: qml/Shell.qml:354
337#, qt-format
337#, qt-format
338msgid "Enter %1"
338msgid "Enter %1"
339−msgstr ""
339+msgstr "Settu inn %1"
340
340
341#: qml/Shell.qml:355
341#: qml/Shell.qml:355
342#, qt-format
342#, qt-format
qml/Components/Carousel.qml+2−3
@@ -17,6 +17,7 @@
17import QtQuick 2.0
17import QtQuick 2.0
18import Ubuntu.Components 0.1
18import Ubuntu.Components 0.1
19import "carousel.js" as CarouselJS
19import "carousel.js" as CarouselJS
20+import "Flickables" as Flickables
20
21
21/*! The Carousel component presents the items of a model in a carousel view. It's similar to a
22/*! The Carousel component presents the items of a model in a carousel view. It's similar to a
22 cover flow. But it stops at it's boundaries (therefore no PathView is used).
23 cover flow. But it stops at it's boundaries (therefore no PathView is used).
@@ -63,7 +64,7 @@ Item {
63 and footer item to compensate that.
64 and footer item to compensate that.
64
65
65 The scaling of the items is controlled by the variable continuousIndex, described below. */
66 The scaling of the items is controlled by the variable continuousIndex, described below. */
66− ListView {
67+ Flickables.ListView {
67 id: listView
68 id: listView
68 objectName: "listView"
69 objectName: "listView"
69
70
@@ -135,8 +136,6 @@ Item {
135
136
136 boundsBehavior: Flickable.DragOverBounds
137 boundsBehavior: Flickable.DragOverBounds
137 cacheBuffer: carousel.cacheBuffer
138 cacheBuffer: carousel.cacheBuffer
138− flickDeceleration: Math.max(1500 * Math.pow(realWidth / referenceWidth, 1.5), 1500) // 1500 is platform default
139− maximumFlickVelocity: Math.max(2500 * Math.pow(realWidth / referenceWidth, 1.5), 2500) // 2500 is platform default
140 orientation: ListView.Horizontal
139 orientation: ListView.Horizontal
141
140
142 function getXFromContinuousIndex(index) {
141 function getXFromContinuousIndex(index) {
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/Flickables/Flickable.qmlnew file+23
@@ -0,0 +1,23 @@
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.3 as QtQuick
18+import Ubuntu.Components 1.1
19+
20+QtQuick.Flickable {
21+ flickDeceleration: 1500 * units.gridUnit / 8
22+ maximumFlickVelocity: 2500 * units.gridUnit / 8
23+}
qml/Components/Flickables/GridView.qmlnew file+25
@@ -0,0 +1,25 @@
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.3 as QtQuick
18+import Ubuntu.Components 1.1
19+
20+QtQuick.GridView {
21+ // Attached components and usages like GridView.onRemove are known not to work
22+ // please use GridView directly from QtQuick if needed
23+ flickDeceleration: 1500 * units.gridUnit / 8
24+ maximumFlickVelocity: 2500 * units.gridUnit / 8
25+}
qml/Components/Flickables/ListView.qmlnew file+25
@@ -0,0 +1,25 @@
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.3 as QtQuick
18+import Ubuntu.Components 1.1
19+
20+QtQuick.ListView {
21+ // Attached components and usages like ListView.onRemove are known not to work
22+ // please use ListView directly from QtQuick if needed
23+ flickDeceleration: 1500 * units.gridUnit / 8
24+ maximumFlickVelocity: 2500 * units.gridUnit / 8
25+}
qml/Components/Flickables/ListViewWithPageHeader.qmlnew file+23
@@ -0,0 +1,23 @@
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 Dash 0.1
18+import Ubuntu.Components 1.1
19+
20+ListViewWithPageHeader {
21+ flickDeceleration: 1500 * units.gridUnit / 8
22+ maximumFlickVelocity: 2500 * units.gridUnit / 8
23+}
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/Components/ListItems/Base.qmldeleted−116
@@ -1,116 +0,0 @@
1−/*
2− * Copyright (C) 2012 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.Components 0.1
19−
20−Item {
21− id: emptyListItem
22− width: parent ? parent.width : units.gu(31)
23− height: body.height + bottomDividerLine.height
24−
25− /*!
26− \preliminary
27− Specifies whether the list item is selected.
28− */
29− property bool selected: false
30−
31− /*!
32− \preliminary
33− Set to show or hide the thin bottom divider line (drawn by the \l ThinDivider component).
34− This line is shown by default except in cases where this item is the delegate of a ListView.
35− */
36− property bool showDivider: __showDivider()
37−
38− /*!
39− \internal
40− Method to automatically determine if the bottom divider line should be drawn.
41− This always returns true, unless item is a delegate in a ListView. If in a ListView
42− it will return false only when:
43− + if this is the final item in the list, and ListView.footer is set (again as thin
44− divider line won't look well with footer below it)
45− */
46− function __showDivider() {
47− // if we're not in ListView, always show a thin dividing line at the bottom
48− var model = null;
49− if (typeof ListViewWithPageHeader !== 'undefined') {
50− if (typeof ListViewWithPageHeader.model !== 'undefined') {
51− model = ListViewWithPageHeader.model;
52− }
53− } else if (ListView.view !== null) {
54− model = ListView.view.model;
55− }
56− // if we're last item in ListView don't show divider
57− if (model && index === model.count - 1) return false;
58−
59− return true;
60− }
61−
62− /* Relevant really only for ListViewWithPageHeader case: specify how many pixels we can overlap with the section header */
63− readonly property int allowedOverlap: units.dp(1)
64−
65− property real __heightToClip: {
66− // Check this is in position where clipping is needed
67− if (typeof ListViewWithPageHeader !== 'undefined') {
68− if (typeof heightToClip !== 'undefined') {
69− if (heightToClip >= allowedOverlap) {
70− return heightToClip - allowedOverlap;
71− }
72− }
73− }
74− return 0;
75− }
76−
77− /*!
78− \internal
79− Reparent so that the visuals of the children does not
80− occlude the bottom divider line.
81− */
82− default property alias children: body.children
83−
84− Item {
85− id: clippingContainer
86− height: parent.height - __heightToClip
87− anchors { left: parent.left; right: parent.right; bottom: parent.bottom }
88− clip: __heightToClip > 0
89−
90− Item {
91− id: body
92− anchors {
93− left: parent.left
94− right: parent.right
95− bottom: bottomDividerLine.top
96− }
97− height: childrenRect.height
98− }
99−
100− ThinDivider {
101− id: bottomDividerLine
102− anchors.bottom: parent.bottom
103− visible: showDivider
104− }
105−
106− Highlight {
107− anchors {
108− top: parent.top
109− left: parent.left
110− right: parent.right
111− bottom: bottomDividerLine.top
112− }
113− pressed: emptyListItem.selected
114− }
115− }
116−}
qml/Components/ListItems/Highlight.qmldeleted−46
@@ -1,46 +0,0 @@
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−import QtQuick 2.0
18−
19−Rectangle {
20− id: highlight
21−
22− property bool pressed: false
23− property real pressedOpacity: 0.3
24−
25− color: "black"
26− opacity: 0
27− state: pressed ? "pressed" : ""
28− states: [
29− State {
30− name: "pressed"
31− PropertyChanges { target: highlight; opacity: pressedOpacity}
32− }
33− ]
34− transitions: [
35− Transition {
36− from: ""
37− to: "pressed"
38− NumberAnimation { target: highlight; property: "opacity"; duration: 50}
39− },
40− Transition {
41− from: "pressed"
42− to: ""
43− NumberAnimation { target: highlight; property: "opacity"; duration: 100}
44− }
45− ]
46−}
qml/Components/ResponsiveGridView.qml+2−1
@@ -16,6 +16,7 @@
16
16
17import QtQuick 2.3
17import QtQuick 2.3
18import Ubuntu.Components 0.1
18import Ubuntu.Components 0.1
19+import "Flickables" as Flickables
19
20
20/*
21/*
21 Essentially a GridView where you can specify the maximum number of columns it can have.
22 Essentially a GridView where you can specify the maximum number of columns it can have.
@@ -55,7 +56,7 @@ Item {
55 return rows * height
56 return rows * height
56 }
57 }
57
58
58− GridView {
59+ Flickables.GridView {
59 id: gridView
60 id: gridView
60 objectName: "responsiveGridViewGrid"
61 objectName: "responsiveGridViewGrid"
61 anchors {
62 anchors {
qml/Components/ZoomableImage.qml+2−2
@@ -16,7 +16,7 @@
16
16
17import QtQuick 2.0
17import QtQuick 2.0
18import Ubuntu.Components 0.1
18import Ubuntu.Components 0.1
19−import "../Components"
19+import "Flickables" as Flickables
20
20
21/*! \brief Zoomable for image.
21/*! \brief Zoomable for image.
22
22
@@ -32,7 +32,7 @@ Item {
32 property alias scaleTo: lazyImage.scaleTo
32 property alias scaleTo: lazyImage.scaleTo
33 property alias asynchronous: lazyImage.asynchronous
33 property alias asynchronous: lazyImage.asynchronous
34
34
35− Flickable {
35+ Flickables.Flickable {
36 id: flickable
36 id: flickable
37 objectName: "flickable"
37 objectName: "flickable"
38 clip: true
38 clip: true
qml/Dash/CardHorizontalList.qml+2−1
@@ -17,6 +17,7 @@
17import QtQuick 2.2
17import QtQuick 2.2
18import Ubuntu.Components 1.1
18import Ubuntu.Components 1.1
19import "../Components"
19import "../Components"
20+import "../Components/Flickables" as Flickables
20
21
21DashRenderer {
22DashRenderer {
22 id: root
23 id: root
@@ -25,7 +26,7 @@ DashRenderer {
25 collapsedHeight: expandedHeight
26 collapsedHeight: expandedHeight
26 clip: true
27 clip: true
27
28
28− ListView {
29+ Flickables.ListView {
29 id: listView
30 id: listView
30 anchors {
31 anchors {
31 fill: parent
32 fill: parent
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/DashCategoryBase.qmlnew file+62
@@ -0,0 +1,62 @@
1+/*
2+ * Copyright (C) 2012-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.Components 0.1
19+
20+Item {
21+ width: parent.width
22+ height: body.height
23+
24+ /* Relevant really only for ListViewWithPageHeader case: specify how many pixels we can overlap with the section header */
25+ readonly property int allowedOverlap: units.dp(1)
26+
27+ property real __heightToClip: {
28+ // Check this is in position where clipping is needed
29+ if (typeof ListViewWithPageHeader !== 'undefined') {
30+ if (typeof heightToClip !== 'undefined') {
31+ if (heightToClip >= allowedOverlap) {
32+ return heightToClip - allowedOverlap;
33+ }
34+ }
35+ }
36+ return 0;
37+ }
38+
39+ /*!
40+ \internal
41+ Reparent so that the visuals of the children does not
42+ occlude the bottom divider line.
43+ */
44+ default property alias children: body.children
45+
46+ Item {
47+ id: clippingContainer
48+ height: parent.height - __heightToClip
49+ anchors { left: parent.left; right: parent.right; bottom: parent.bottom }
50+ clip: __heightToClip > 0
51+
52+ Item {
53+ id: body
54+ anchors {
55+ left: parent.left
56+ right: parent.right
57+ bottom: parent.bottom
58+ }
59+ height: childrenRect.height
60+ }
61+ }
62+}
qml/Dash/DashContent.qml+6−4
@@ -19,10 +19,12 @@ import Ubuntu.Components 0.1
19import Unity 0.2
19import Unity 0.2
20import Utils 0.1
20import Utils 0.1
21import "../Components"
21import "../Components"
22+import "../Components/Flickables" as Flickables
22
23
23Item {
24Item {
24 id: dashContent
25 id: dashContent
25
26
27+ property bool forceNonInteractive: false
26 property alias scopes: dashContentList.model
28 property alias scopes: dashContentList.model
27 readonly property alias currentIndex: dashContentList.currentIndex
29 readonly property alias currentIndex: dashContentList.currentIndex
28 readonly property string currentScopeId: dashContentList.currentItem ? dashContentList.currentItem.scopeId : ""
30 readonly property string currentScopeId: dashContentList.currentItem ? dashContentList.currentItem.scopeId : ""
@@ -97,16 +99,15 @@ Item {
97 anchors.fill: parent
99 anchors.fill: parent
98 }
100 }
99
101
100− ListView {
102+ Flickables.ListView {
101 id: dashContentList
103 id: dashContentList
102 objectName: "dashContentList"
104 objectName: "dashContentList"
103
105
106+ interactive: !dashContent.forceNonInteractive && dashContent.scopes.loaded && currentItem
104− interactive: dashContent.scopes.loaded && currentItem && !currentItem.moving && !currentItem.navigationShown && !currentItem.subPageShown
107+ && !currentItem.moving && !currentItem.navigationShown && !currentItem.subPageShown
105 anchors.fill: parent
108 anchors.fill: parent
106 orientation: ListView.Horizontal
109 orientation: ListView.Horizontal
107 boundsBehavior: Flickable.DragAndOvershootBounds
110 boundsBehavior: Flickable.DragAndOvershootBounds
108− flickDeceleration: units.gu(625)
109− maximumFlickVelocity: width * 5
110 snapMode: ListView.SnapOneItem
111 snapMode: ListView.SnapOneItem
111 highlightMoveDuration: 250
112 highlightMoveDuration: 250
112 highlightRangeMode: ListView.StrictlyEnforceRange
113 highlightRangeMode: ListView.StrictlyEnforceRange
@@ -180,6 +181,7 @@ Item {
180 item.paginationCount = Qt.binding(function() { return dashContentList.count } )
181 item.paginationCount = Qt.binding(function() { return dashContentList.count } )
181 item.paginationIndex = Qt.binding(function() { return dashContentList.currentIndex } )
182 item.paginationIndex = Qt.binding(function() { return dashContentList.currentIndex } )
182 item.holdingList = dashContentList;
183 item.holdingList = dashContentList;
184+ item.forceNonInteractive = Qt.binding(function() { return dashContent.forceNonInteractive } )
183 }
185 }
184 Connections {
186 Connections {
185 target: isCurrent ? scope : null
187 target: isCurrent ? scope : null
qml/Dash/DashNavigationButton.qml+2−1
@@ -16,6 +16,7 @@
16
16
17import QtQuick 2.2
17import QtQuick 2.2
18import Ubuntu.Components 1.1
18import Ubuntu.Components 1.1
19+import "../Components/Flickables" as Flickables
19
20
20AbstractButton {
21AbstractButton {
21 id: root
22 id: root
@@ -75,7 +76,7 @@ AbstractButton {
75 }
76 }
76
77
77 // navigationListView is outside root
78 // navigationListView is outside root
78− ListView {
79+ Flickables.ListView {
79 id: navigationListView
80 id: navigationListView
80 objectName: "navigationListView"
81 objectName: "navigationListView"
81 visible: height > 0
82 visible: height > 0
qml/Dash/DashNavigationList.qml+2−1
@@ -17,6 +17,7 @@
17import QtQuick 2.2
17import QtQuick 2.2
18import Ubuntu.Components 1.1
18import Ubuntu.Components 1.1
19import "../Components"
19import "../Components"
20+import "../Components/Flickables" as Flickables
20
21
21Item {
22Item {
22 id: root
23 id: root
@@ -45,7 +46,7 @@ Item {
45 }
46 }
46 }
47 }
47
48
48− Flickable {
49+ Flickables.Flickable {
49 id: flickable
50 id: flickable
50
51
51 anchors.fill: parent
52 anchors.fill: parent
qml/Dash/GenericScopeView.qml+4−3
@@ -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
@@ -210,10 +212,9 @@ FocusScope {
210 }
212 }
211 }
213 }
212
214
213− delegate: ListItems.Base {
215+ delegate: DashCategoryBase {
214 id: baseItem
216 id: baseItem
215 objectName: "dashCategory" + category
217 objectName: "dashCategory" + category
216− showDivider: false
217
218
218 property Item seeAllButton: seeAll
219 property Item seeAllButton: seeAll
219
220
@@ -236,7 +237,7 @@ FocusScope {
236 CardTool {
237 CardTool {
237 id: cardTool
238 id: cardTool
238 objectName: "cardTool"
239 objectName: "cardTool"
239− count: results.count
240+ count: results ? results.count : 0
240 template: model.renderer
241 template: model.renderer
241 components: model.components
242 components: model.components
242 viewWidth: parent.width
243 viewWidth: parent.width
qml/Dash/PageHeader.qml+2−1
@@ -21,6 +21,7 @@ import Ubuntu.Components.Popups 1.0
21import Ubuntu.Components.ListItems 1.0
21import Ubuntu.Components.ListItems 1.0
22import "../Components"
22import "../Components"
23import "../Components/SearchHistoryModel"
23import "../Components/SearchHistoryModel"
24+import "../Components/Flickables" as Flickables
24
25
25Item {
26Item {
26 id: root
27 id: root
@@ -127,7 +128,7 @@ Item {
127 }
128 }
128 }
129 }
129
130
130− Flickable {
131+ Flickables.Flickable {
131 id: headerContainer
132 id: headerContainer
132 objectName: "headerContainer"
133 objectName: "headerContainer"
133 clip: contentY < height
134 clip: contentY < height
qml/Dash/PreviewListView.qml+2−3
@@ -18,6 +18,7 @@ import QtQuick 2.0
18import Ubuntu.Components 0.1
18import Ubuntu.Components 0.1
19import Unity 0.2
19import Unity 0.2
20import "../Components"
20import "../Components"
21+import "../Components/Flickables" as Flickables
21import "Previews" as Previews
22import "Previews" as Previews
22
23
23Item {
24Item {
@@ -52,7 +53,7 @@ Item {
52 onBackClicked: root.backClicked()
53 onBackClicked: root.backClicked()
53 }
54 }
54
55
55− ListView {
56+ Flickables.ListView {
56 id: previewListView
57 id: previewListView
57 objectName: "listView"
58 objectName: "listView"
58 anchors {
59 anchors {
@@ -66,8 +67,6 @@ Item {
66 snapMode: ListView.SnapOneItem
67 snapMode: ListView.SnapOneItem
67 boundsBehavior: Flickable.DragAndOvershootBounds
68 boundsBehavior: Flickable.DragAndOvershootBounds
68 highlightMoveDuration: 250
69 highlightMoveDuration: 250
69− flickDeceleration: units.gu(625)
70− maximumFlickVelocity: width * 5
71 cacheBuffer: 0
70 cacheBuffer: 0
72
71
73 property bool open: false
72 property bool open: false
qml/Dash/Previews/Preview.qml+2−1
@@ -16,6 +16,7 @@
16
16
17import QtQuick 2.0
17import QtQuick 2.0
18import Ubuntu.Components 0.1
18import Ubuntu.Components 0.1
19+import "../../Components/Flickables" as Flickables
19
20
20/*! \brief This component constructs the Preview UI.
21/*! \brief This component constructs the Preview UI.
21 *
22 *
@@ -63,7 +64,7 @@ Item {
63 Repeater {
64 Repeater {
64 model: previewModel
65 model: previewModel
65
66
66− delegate: ListView {
67+ delegate: Flickables.ListView {
67 id: column
68 id: column
68 anchors { top: parent.top; bottom: parent.bottom }
69 anchors { top: parent.top; bottom: parent.bottom }
69 width: row.columnWidth
70 width: row.columnWidth
qml/Dash/Previews/PreviewImageGallery.qml+3−2
@@ -17,6 +17,7 @@
17import QtQuick 2.0
17import QtQuick 2.0
18import Ubuntu.Components 1.1
18import Ubuntu.Components 1.1
19import "../../Components"
19import "../../Components"
20+import "../../Components/Flickables" as Flickables
20
21
21/*! This preview widget shows a horizontal list of images.
22/*! This preview widget shows a horizontal list of images.
22 * The URIs for the images should be an array in widgetData["sources"].
23 * The URIs for the images should be an array in widgetData["sources"].
@@ -28,7 +29,7 @@ PreviewWidget {
28
29
29 property Item rootItem: QuickUtils.rootItem(root)
30 property Item rootItem: QuickUtils.rootItem(root)
30
31
31− ListView {
32+ Flickables.ListView {
32 id: previewImageListView
33 id: previewImageListView
33 objectName: "previewImageListView"
34 objectName: "previewImageListView"
34 spacing: units.gu(1)
35 spacing: units.gu(1)
@@ -102,7 +103,7 @@ PreviewWidget {
102 UbuntuNumberAnimation { duration: UbuntuAnimation.FastDuration }
103 UbuntuNumberAnimation { duration: UbuntuAnimation.FastDuration }
103 }
104 }
104
105
105− ListView {
106+ Flickables.ListView {
106 id: slideShowListView
107 id: slideShowListView
107 objectName: "slideShowListView"
108 objectName: "slideShowListView"
108 anchors.fill: parent
109 anchors.fill: parent
qml/Dash/Previews/PreviewTextSummary.qml+1−1
@@ -63,7 +63,7 @@ PreviewWidget {
63 lineHeight: 1.2
63 lineHeight: 1.2
64 color: root.scopeStyle ? root.scopeStyle.foreground : Theme.palette.normal.baseText
64 color: root.scopeStyle ? root.scopeStyle.foreground : Theme.palette.normal.baseText
65 opacity: .8
65 opacity: .8
66− text: widgetData["text"]
66+ text: widgetData["text"] || ""
67 wrapMode: Text.Wrap
67 wrapMode: Text.Wrap
68
68
69 Behavior on height {
69 Behavior on height {
qml/Dash/ScopeListView.qml+3−6
@@ -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
@@ -14,11 +14,8 @@
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−import QtQuick 2.0
17+import "../Components/Flickables" as Flickables
18−import Dash 0.1
19
18
20−ListViewWithPageHeader {
19+Flickables.ListViewWithPageHeader {
21− maximumFlickVelocity: height * 10
22− flickDeceleration: height * 2
23 cacheBuffer: Number.MAX_VALUE
20 cacheBuffer: Number.MAX_VALUE
24}
21}
qml/Dash/ScopeSettingsPage.qml+2−1
@@ -18,6 +18,7 @@ import QtQuick 2.2
18import Ubuntu.Components 1.1
18import Ubuntu.Components 1.1
19import Unity 0.2
19import Unity 0.2
20import "../Components"
20import "../Components"
21+import "../Components/Flickables" as Flickables
21import "ScopeSettings"
22import "ScopeSettings"
22
23
23Item {
24Item {
@@ -39,7 +40,7 @@ Item {
39 onBackClicked: root.backClicked()
40 onBackClicked: root.backClicked()
40 }
41 }
41
42
42− ListView {
43+ Flickables.ListView {
43 id: scopeSettings
44 id: scopeSettings
44 objectName: "scopeSettings"
45 objectName: "scopeSettings"
45 anchors {
46 anchors {
qml/Dash/ScopesOverviewAll.qml+2−1
@@ -16,8 +16,9 @@
16
16
17import QtQuick 2.0
17import QtQuick 2.0
18import Ubuntu.Components 0.1
18import Ubuntu.Components 0.1
19+import "../Components/Flickables" as Flickables
19
20
20−Flickable {
21+Flickables.Flickable {
21 id: root
22 id: root
22
23
23 property alias model: cardGrid.model
24 property alias model: cardGrid.model
qml/Dash/ScopesOverviewFavorites.qml+4−2
@@ -14,9 +14,11 @@
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−import QtQuick 2.0
17+import QtQuick 2.3
18+import Ubuntu.Components 1.1
19+import "../Components/Flickables" as Flickables
18
20
19−Flickable {
21+Flickables.Flickable {
20 id: root
22 id: root
21
23
22 signal clicked(int index, var result, var itemModel)
24 signal clicked(int index, var result, var itemModel)
qml/Greeter/LoginList.qml+2−2
@@ -18,6 +18,7 @@ import QtQuick 2.0
18import Ubuntu.Components 0.1
18import Ubuntu.Components 0.1
19import LightDM 0.1 as LightDM
19import LightDM 0.1 as LightDM
20import "../Components"
20import "../Components"
21+import "../Components/Flickables" as Flickables
21
22
22Item {
23Item {
23 id: root
24 id: root
@@ -68,7 +69,7 @@ Item {
68 antialiasing: true
69 antialiasing: true
69 }
70 }
70
71
71− ListView {
72+ Flickables.ListView {
72 id: userList
73 id: userList
73 objectName: "userList"
74 objectName: "userList"
74
75
@@ -78,7 +79,6 @@ Item {
78 preferredHighlightEnd: userList.height / 2 - root.highlightedHeight / 2
79 preferredHighlightEnd: userList.height / 2 - root.highlightedHeight / 2
79 highlightRangeMode: ListView.StrictlyEnforceRange
80 highlightRangeMode: ListView.StrictlyEnforceRange
80 highlightMoveDuration: root.moveDuration
81 highlightMoveDuration: root.moveDuration
81− flickDeceleration: 10000
82
82
83 readonly property bool movingInternally: moveTimer.running || userList.moving
83 readonly property bool movingInternally: moveTimer.running || userList.moving
84
84
qml/Hud/Hud.qml+2−1
@@ -19,6 +19,7 @@ import Ubuntu.Components 0.1
19import HudClient 0.1
19import HudClient 0.1
20
20
21import "../Components"
21import "../Components"
22+import "../Components/Flickables" as Flickables
22
23
23Item {
24Item {
24 id: hud
25 id: hud
@@ -400,7 +401,7 @@ Item {
400 }
401 }
401 progress: MathUtils.clamp((y - hudShowable.y + height - units.gu(8))/elementsYSliding, 0, 1)
402 progress: MathUtils.clamp((y - hudShowable.y + height - units.gu(8))/elementsYSliding, 0, 1)
402
403
403− Flickable {
404+ Flickables.Flickable {
404 anchors.left: parent.left
405 anchors.left: parent.left
405 anchors.right: parent.right
406 anchors.right: parent.right
406 contentHeight: resultList.height
407 contentHeight: resultList.height
qml/Hud/HudParametrizedActionsPage.qml+2−1
@@ -18,6 +18,7 @@ import QtQuick 2.0
18import Ubuntu.Components 1.1
18import Ubuntu.Components 1.1
19import Ubuntu.Components.Themes.Ambiance 1.1
19import Ubuntu.Components.Themes.Ambiance 1.1
20import "../Components"
20import "../Components"
21+import "../Components/Flickables" as Flickables
21
22
22Item {
23Item {
23 property alias header: header.title
24 property alias header: header.title
@@ -74,7 +75,7 @@ Item {
74 return values
75 return values
75 }
76 }
76
77
77− Flickable {
78+ Flickables.Flickable {
78 anchors.top: parent.top
79 anchors.top: parent.top
79 anchors.bottom: buttons.top
80 anchors.bottom: buttons.top
80 anchors.left: parent.left
81 anchors.left: parent.left
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+3−1
@@ -21,6 +21,7 @@ import Unity.Launcher 0.1
21import Ubuntu.Components.Popups 0.1
21import Ubuntu.Components.Popups 0.1
22import "../Components/ListItems"
22import "../Components/ListItems"
23import "../Components/"
23import "../Components/"
24+import "../Components/Flickables" as Flickables
24
25
25Rectangle {
26Rectangle {
26 id: root
27 id: root
@@ -45,6 +46,7 @@ Rectangle {
45 }
46 }
46
47
47 Rectangle {
48 Rectangle {
49+ objectName: "buttonShowDashHome"
48 width: parent.width
50 width: parent.width
49 height: units.gu(7)
51 height: units.gu(7)
50 color: UbuntuColors.orange
52 color: UbuntuColors.orange
@@ -74,7 +76,7 @@ Rectangle {
74 anchors.fill: parent
76 anchors.fill: parent
75 clip: true
77 clip: true
76
78
77− ListView {
79+ Flickables.ListView {
78 id: launcherListView
80 id: launcherListView
79 objectName: "launcherListView"
81 objectName: "launcherListView"
80 anchors {
82 anchors {
qml/Notifications/Notification.qml+2−1
@@ -20,6 +20,7 @@ import Ubuntu.Components 1.1
20import Unity.Notifications 1.0
20import Unity.Notifications 1.0
21import QMenuModel 0.1
21import QMenuModel 0.1
22import Utils 0.1
22import Utils 0.1
23+import "../Components/Flickables" as Flickables
23
24
24import Ubuntu.Components.ListItems 0.1 as ListItem
25import Ubuntu.Components.ListItems 0.1 as ListItem
25
26
@@ -455,7 +456,7 @@ Item {
455 onClicked: notification.notification.invokeAction(comboRepeater.itemAt(2).actionId)
456 onClicked: notification.notification.invokeAction(comboRepeater.itemAt(2).actionId)
456 expanded: false
457 expanded: false
457 expandedHeight: (comboRepeater.count - 2) * units.gu(4) + units.gu(.5)
458 expandedHeight: (comboRepeater.count - 2) * units.gu(4) + units.gu(.5)
458− comboList: Flickable {
459+ comboList: Flickables.Flickable {
459 // this has to be wrapped inside a flickable
460 // this has to be wrapped inside a flickable
460 // to work around a feature/bug? of the
461 // to work around a feature/bug? of the
461 // ComboButton SDK-element, making a regular
462 // ComboButton SDK-element, making a regular
qml/Notifications/Notifications.qml+2−1
@@ -19,8 +19,9 @@ import Ubuntu.Components 0.1
19import Unity.Notifications 1.0 as UnityNotifications
19import Unity.Notifications 1.0 as UnityNotifications
20import Utils 0.1
20import Utils 0.1
21import "../Components"
21import "../Components"
22+import "../Components/Flickables" as Flickables
22
23
23−ListView {
24+Flickables.ListView {
24 id: notificationList
25 id: notificationList
25
26
26 objectName: "notificationList"
27 objectName: "notificationList"
qml/Panel/IndicatorRow.qml+2−1
@@ -18,6 +18,7 @@ import QtQuick 2.0
18import Ubuntu.Components 0.1
18import Ubuntu.Components 0.1
19import Unity.Indicators 0.1 as Indicators
19import Unity.Indicators 0.1 as Indicators
20import "../Components"
20import "../Components"
21+import "../Components/Flickables" as Flickables
21
22
22Item {
23Item {
23 id: indicatorRow
24 id: indicatorRow
@@ -60,7 +61,7 @@ Item {
60 }
61 }
61 }
62 }
62
63
63− ListView {
64+ Flickables.ListView {
64 id: itemView
65 id: itemView
65 objectName: "indicatorRowItems"
66 objectName: "indicatorRowItems"
66 interactive: false
67 interactive: false
qml/Panel/Indicators/DefaultIndicatorPage.qml+2−1
@@ -21,6 +21,7 @@
21import QtQuick 2.0
21import QtQuick 2.0
22import Ubuntu.Components 0.1 as Components
22import Ubuntu.Components 0.1 as Components
23import Unity.Indicators 0.1 as Indicators
23import Unity.Indicators 0.1 as Indicators
24+import "../../Components/Flickables" as Flickables
24
25
25IndicatorBase {
26IndicatorBase {
26 id: main
27 id: main
@@ -65,7 +66,7 @@ IndicatorBase {
65 }
66 }
66 }
67 }
67
68
68− ListView {
69+ Flickables.ListView {
69 id: mainMenu
70 id: mainMenu
70 objectName: "mainMenu"
71 objectName: "mainMenu"
71 model: menuStack.rootMenu
72 model: menuStack.rootMenu
qml/Panel/Indicators/client/IndicatorsList.qml+2−1
@@ -22,6 +22,7 @@ import Ubuntu.Components 0.1
22import Ubuntu.Components.ListItems 0.1 as ListItem
22import Ubuntu.Components.ListItems 0.1 as ListItem
23import Unity.Indicators 0.1 as Indicators
23import Unity.Indicators 0.1 as Indicators
24import "../.."
24import "../.."
25+import "../../../Components/Flickables" as Flickables
25
26
26Page {
27Page {
27 id: page
28 id: page
@@ -35,7 +36,7 @@ Page {
35 Component.onCompleted: load(profile)
36 Component.onCompleted: load(profile)
36 }
37 }
37
38
38− ListView {
39+ Flickables.ListView {
39 id: mainMenu
40 id: mainMenu
40 objectName: "mainMenu"
41 objectName: "mainMenu"
41 anchors.fill: parent
42 anchors.fill: parent
qml/Panel/Indicators/client/IndicatorsTree.qml+2−1
@@ -22,6 +22,7 @@ import Ubuntu.Components 0.1
22import Unity.Indicators 0.1 as Indicators
22import Unity.Indicators 0.1 as Indicators
23import QMenuModel 0.1
23import QMenuModel 0.1
24import Ubuntu.Components.ListItems 0.1 as ListItem
24import Ubuntu.Components.ListItems 0.1 as ListItem
25+import "../../../Components/Flickables" as Flickables
25
26
26Page {
27Page {
27 id: page
28 id: page
@@ -52,7 +53,7 @@ Page {
52 model: unityModel
53 model: unityModel
53 }
54 }
54
55
55− Flickable {
56+ Flickables.Flickable {
56 anchors.fill: parent
57 anchors.fill: parent
57 contentHeight: all_data.height
58 contentHeight: all_data.height
58 clip:true
59 clip:true
qml/Panel/MenuContent.qml+3−2
@@ -20,6 +20,7 @@ import Ubuntu.Components 1.1
20import Unity.Indicators 0.1 as Indicators
20import Unity.Indicators 0.1 as Indicators
21import Utils 0.1
21import Utils 0.1
22import "../Components"
22import "../Components"
23+import "../Components/Flickables" as Flickables
23import "Indicators"
24import "Indicators"
24
25
25Rectangle {
26Rectangle {
@@ -39,7 +40,7 @@ Rectangle {
39 listViewHeader.currentIndex = index;
40 listViewHeader.currentIndex = index;
40 }
41 }
41
42
42− ListView {
43+ Flickables.ListView {
43 id: listViewHeader
44 id: listViewHeader
44 objectName: "indicatorsHeaderListView"
45 objectName: "indicatorsHeaderListView"
45 model: content.indicatorsModel
46 model: content.indicatorsModel
@@ -80,7 +81,7 @@ Rectangle {
80 }
81 }
81 }
82 }
82
83
83− ListView {
84+ Flickables.ListView {
84 id: listViewContent
85 id: listViewContent
85 objectName: "indicatorsContentListView"
86 objectName: "indicatorsContentListView"
86 anchors {
87 anchors {
qml/Shell.qml+55−21
@@ -53,7 +53,9 @@ Item {
53 property url background
53 property url background
54 readonly property real panelHeight: panel.panelHeight
54 readonly property real panelHeight: panel.panelHeight
55
55
56− readonly property bool locked: LightDM.Greeter.active && !LightDM.Greeter.authenticated
56+ readonly property bool locked: LightDM.Greeter.active && !LightDM.Greeter.authenticated && !forcedUnlock
57+ readonly property bool forcedUnlock: edgeDemo.running
58+ onForcedUnlockChanged: if (forcedUnlock) lockscreen.hide()
57
59
58 property bool sideStageEnabled: shell.width >= units.gu(100)
60 property bool sideStageEnabled: shell.width >= units.gu(100)
59 readonly property string focusedApplicationId: ApplicationManager.focusedApplicationId
61 readonly property string focusedApplicationId: ApplicationManager.focusedApplicationId
@@ -87,11 +89,13 @@ Item {
87 }
89 }
88 }
90 }
89
91
90− function setLockedApp(app) {
92+ function startLockedApp(app) {
91− if (shell.locked) {
93+ if (!shell.locked) {
92− greeter.lockedApp = app
94+ console.warn("Called startLockedApp(%1) when not locked, ignoring".arg(app))
93− lockscreen.hide()
95+ return
94 }
96 }
97+ greeter.lockedApp = app
98+ shell.activateApplication(app)
95 }
99 }
96
100
97 Binding {
101 Binding {
@@ -170,12 +174,18 @@ Item {
170 target: ApplicationManager
174 target: ApplicationManager
171 onFocusRequested: {
175 onFocusRequested: {
172 if (greeter.narrowMode) {
176 if (greeter.narrowMode) {
173− if (appId === "dialer-app") {
177+ if (appId === "dialer-app" && callManager.hasCalls) {
174− // Always let the dialer-app through. Either user asked
178+ // If we are in the middle of a call, make dialer lockedApp and show it.
175− // for an emergency call or accepted an incoming call.
179+ // This can happen if user backs out of dialer back to greeter, then
176− setLockedApp(appId)
180+ // launches dialer again.
177− } else if (greeter.hasLockedApp && greeter.lockedApp !== appId) {
181+ greeter.lockedApp = appId;
182+ }
183+ if (greeter.hasLockedApp) {
184+ if (appId === greeter.lockedApp) {
185+ lockscreen.hide() // show locked app
186+ } else {
178− greeter.startUnlock()
187+ greeter.startUnlock() // show lockscreen if necessary
188+ }
179 }
189 }
180 greeter.hide();
190 greeter.hide();
181 } else {
191 } else {
@@ -196,10 +206,8 @@ Item {
196 if (greeter.shown && appId != "unity8-dash") {
206 if (greeter.shown && appId != "unity8-dash") {
197 greeter.startUnlock()
207 greeter.startUnlock()
198 }
208 }
199− if (greeter.narrowMode && appId === "dialer-app") {
209+ if (greeter.narrowMode && greeter.hasLockedApp && appId === greeter.lockedApp) {
200− // Always let the dialer-app through. Either user asked
210+ lockscreen.hide() // show locked app
201− // for an emergency call or accepted an incoming call.
202− setLockedApp(appId)
203 }
211 }
204 launcher.hide();
212 launcher.hide();
205 }
213 }
@@ -325,10 +333,16 @@ Item {
325
333
326 onEntered: LightDM.Greeter.respond(passphrase);
334 onEntered: LightDM.Greeter.respond(passphrase);
327 onCancel: greeter.show()
335 onCancel: greeter.show()
328− onEmergencyCall: shell.activateApplication("dialer-app") // will automatically enter locked-app mode
336+ onEmergencyCall: startLockedApp("dialer-app")
329
337
330 onShownChanged: if (shown) greeter.lockedApp = ""
338 onShownChanged: if (shown) greeter.lockedApp = ""
331
339
340+ function maybeShow() {
341+ if (!shell.forcedUnlock) {
342+ show()
343+ }
344+ }
345+
332 Timer {
346 Timer {
333 id: forcedDelayTimer
347 id: forcedDelayTimer
334 interval: 1000 * 60
348 interval: 1000 * 60
@@ -375,7 +389,7 @@ Item {
375 lockscreen.errorText = i18n.tr("Sorry, incorrect %1").arg(text.toLowerCase())
389 lockscreen.errorText = i18n.tr("Sorry, incorrect %1").arg(text.toLowerCase())
376 }
390 }
377
391
378− lockscreen.show();
392+ lockscreen.maybeShow();
379 }
393 }
380 }
394 }
381
395
@@ -388,7 +402,7 @@ Item {
388 lockscreen.hide()
402 lockscreen.hide()
389 } else {
403 } else {
390 lockscreen.reset();
404 lockscreen.reset();
391− lockscreen.show();
405+ lockscreen.maybeShow();
392 }
406 }
393 }
407 }
394 }
408 }
@@ -468,14 +482,14 @@ Item {
468 // Wait until the greeter is completely covering lockscreen before resetting it.
482 // Wait until the greeter is completely covering lockscreen before resetting it.
469 if (greeter.narrowMode && fullyShown && !LightDM.Greeter.authenticated) {
483 if (greeter.narrowMode && fullyShown && !LightDM.Greeter.authenticated) {
470 lockscreen.reset();
484 lockscreen.reset();
471− lockscreen.show();
485+ lockscreen.maybeShow();
472 }
486 }
473 }
487 }
474
488
475 readonly property real showProgress: MathUtils.clamp((1 - x/width) + greeter.showProgress - 1, 0, 1)
489 readonly property real showProgress: MathUtils.clamp((1 - x/width) + greeter.showProgress - 1, 0, 1)
476 onShowProgressChanged: {
490 onShowProgressChanged: {
477 if (showProgress === 0) {
491 if (showProgress === 0) {
478− if (LightDM.Greeter.promptless && LightDM.Greeter.authenticated) {
492+ if ((LightDM.Greeter.promptless && LightDM.Greeter.authenticated) || shell.forcedUnlock) {
479 greeter.login()
493 greeter.login()
480 } else if (greeter.narrowMode) {
494 } else if (greeter.narrowMode) {
481 lockscreen.clear(false) // to reset focus if necessary
495 lockscreen.clear(false) // to reset focus if necessary
@@ -509,7 +523,7 @@ Item {
509 function startUnlock() {
523 function startUnlock() {
510 if (narrowMode) {
524 if (narrowMode) {
511 if (!LightDM.Greeter.authenticated) {
525 if (!LightDM.Greeter.authenticated) {
512− lockscreen.show()
526+ lockscreen.maybeShow()
513 }
527 }
514 hide()
528 hide()
515 } else {
529 } else {
@@ -569,6 +583,25 @@ Item {
569 }
583 }
570 }
584 }
571
585
586+ Connections {
587+ id: callConnection
588+ target: callManager
589+
590+ onHasCallsChanged: {
591+ if (shell.locked && callManager.hasCalls) {
592+ // We just received an incoming call while locked. The
593+ // indicator will have already launched dialer-app for us, but
594+ // there is a race between "hasCalls" changing and the dialer
595+ // starting up. So in case we lose that race, we'll start/
596+ // focus the dialer ourselves here too. Even if the indicator
597+ // didn't launch the dialer for some reason (or maybe a call
598+ // started via some other means), if an active call is
599+ // happening, we want to be in the dialer.
600+ startLockedApp("dialer-app")
601+ }
602+ }
603+ }
604+
572 Connections {
605 Connections {
573 id: powerConnection
606 id: powerConnection
574 target: Powerd
607 target: Powerd
@@ -738,6 +771,7 @@ Item {
738
771
739 EdgeDemo {
772 EdgeDemo {
740 id: edgeDemo
773 id: edgeDemo
774+ objectName: "edgeDemo"
741 z: alphaDisclaimerLabel.z + 10
775 z: alphaDisclaimerLabel.z + 10
742 paused: Powerd.status === Powerd.Off // Saves power
776 paused: Powerd.status === Powerd.Off // Saves power
743 greeter: greeter
777 greeter: greeter
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+16−6
@@ -21,6 +21,7 @@ import Unity.Application 0.1
21import Unity.Session 0.1
21import Unity.Session 0.1
22import Utils 0.1
22import Utils 0.1
23import "../Components"
23import "../Components"
24+import "../Components/Flickables" as Flickables
24
25
25Rectangle {
26Rectangle {
26 id: root
27 id: root
@@ -120,7 +121,7 @@ Rectangle {
120
121
121 }
122 }
122
123
123− Flickable {
124+ Flickables.Flickable {
124 id: spreadView
125 id: spreadView
125 objectName: "spreadView"
126 objectName: "spreadView"
126 anchors.fill: parent
127 anchors.fill: parent
@@ -131,7 +132,7 @@ Rectangle {
131
132
132 // This indicates when the spreadView is active. That means, all the animations
133 // This indicates when the spreadView is active. That means, all the animations
133 // are activated and tiles need to line up for the spread.
134 // are activated and tiles need to line up for the spread.
134− readonly property bool active: shiftedContentX > 0 || spreadDragArea.dragging
135+ readonly property bool active: shiftedContentX > 0 || spreadDragArea.status === DirectionalDragArea.Recognized
135
136
136 // The flickable needs to fill the screen in order to get touch events all over.
137 // 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
138 // However, we don't want to the user to be able to scroll back all the way. For
@@ -418,6 +419,7 @@ Rectangle {
418
419
419 EdgeDragArea {
420 EdgeDragArea {
420 id: spreadDragArea
421 id: spreadDragArea
422+ objectName: "spreadDragArea"
421 direction: Direction.Leftwards
423 direction: Direction.Leftwards
422 enabled: spreadView.phase != 2 && root.spreadEnabled
424 enabled: spreadView.phase != 2 && root.spreadEnabled
423
425
@@ -437,9 +439,10 @@ Rectangle {
437 spreadView.phase = 0;
439 spreadView.phase = 0;
438 spreadView.contentX = -spreadView.shift;
440 spreadView.contentX = -spreadView.shift;
439 }
441 }
440− if (dragging && attachedToView) {
442+ if (dragging && status == DirectionalDragArea.Recognized && attachedToView) {
441 // Gesture recognized. Let's move the spreadView with the finger
443 // Gesture recognized. Let's move the spreadView with the finger
444+ var finalX = Math.min(touchX + width, width);
442− spreadView.contentX = -touchX + spreadDragArea.width - spreadView.shift;
445+ spreadView.contentX = -finalX + spreadDragArea.width - spreadView.shift;
443 }
446 }
444 if (attachedToView && spreadView.shiftedContentX >= spreadView.width * spreadView.positionMarker3) {
447 if (attachedToView && spreadView.shiftedContentX >= spreadView.width * spreadView.positionMarker3) {
445 // We passed positionMarker3. Detach from spreadView and snap it.
448 // We passed positionMarker3. Detach from spreadView and snap it.
@@ -449,7 +452,13 @@ Rectangle {
449 gesturePoints.push(touchX);
452 gesturePoints.push(touchX);
450 }
453 }
451
454
455+ property int previousStatus: -1
456+ property int currentStatus: DirectionalDragArea.WaitingForTouch
457+
452 onStatusChanged: {
458 onStatusChanged: {
459+ previousStatus = currentStatus;
460+ currentStatus = status;
461+
453 if (status == DirectionalDragArea.Recognized) {
462 if (status == DirectionalDragArea.Recognized) {
454 attachedToView = true;
463 attachedToView = true;
455 }
464 }
@@ -457,7 +466,7 @@ Rectangle {
457
466
458 onDraggingChanged: {
467 onDraggingChanged: {
459 if (dragging) {
468 if (dragging) {
460− // Gesture recognized. Start recording this gesture
469+ // A potential edge-drag gesture has started. Start recording it
461 gesturePoints = [];
470 gesturePoints = [];
462 return;
471 return;
463 }
472 }
@@ -474,7 +483,8 @@ Rectangle {
474 }
483 }
475 gesturePoints = [];
484 gesturePoints = [];
476
485
486+ if (previousStatus == DirectionalDragArea.Recognized &&
477− if (oneWayFlick && spreadView.shiftedContentX > units.gu(2) &&
487+ oneWayFlick && spreadView.shiftedContentX > units.gu(2) &&
478 spreadView.shiftedContentX < spreadView.positionMarker1 * spreadView.width) {
488 spreadView.shiftedContentX < spreadView.positionMarker1 * spreadView.width) {
479 // If it was a short one-way movement, do the Alt+Tab switch
489 // If it was a short one-way movement, do the Alt+Tab switch
480 // no matter if we didn't cross positionMarker1 yet.
490 // 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: [
qml/Stages/TabletStage.qml+2−1
@@ -20,6 +20,7 @@ import Ubuntu.Gestures 0.1
20import Unity.Application 0.1
20import Unity.Application 0.1
21import Utils 0.1
21import Utils 0.1
22import "../Components"
22import "../Components"
23+import "../Components/Flickables" as Flickables
23
24
24Rectangle {
25Rectangle {
25 id: root
26 id: root
@@ -153,7 +154,7 @@ Rectangle {
153 }
154 }
154 }
155 }
155
156
156− Flickable {
157+ Flickables.Flickable {
157 id: spreadView
158 id: spreadView
158 anchors.fill: parent
159 anchors.fill: parent
159 interactive: (spreadDragArea.status == DirectionalDragArea.Recognized || phase > 1)
160 interactive: (spreadDragArea.status == DirectionalDragArea.Recognized || phase > 1)
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/autopilot/unity8/shell/emulators/dash.py+1−1
@@ -206,7 +206,7 @@ class GenericScopeView(emulators.UnityEmulatorBase):
206 def _get_category_element(self, category):
206 def _get_category_element(self, category):
207 try:
207 try:
208 return self.wait_select_single(
208 return self.wait_select_single(
209− 'Base', objectName='dashCategory{}'.format(category))
209+ 'DashCategoryBase', objectName='dashCategory{}'.format(category))
210 except dbus.StateNotFoundError:
210 except dbus.StateNotFoundError:
211 raise emulators.UnityEmulatorException(
211 raise emulators.UnityEmulatorException(
212 'No category found with name {}'.format(category))
212 'No category found with name {}'.format(category))
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/AccountsService/AccountsService.cpp+5−3
@@ -26,7 +26,8 @@ AccountsService::AccountsService(QObject* parent)
26 m_enableIndicatorsWhileLocked(true),
26 m_enableIndicatorsWhileLocked(true),
27 m_backgroundFile(qmlDirectory() + "graphics/phone_background.jpg"),
27 m_backgroundFile(qmlDirectory() + "graphics/phone_background.jpg"),
28 m_statsWelcomeScreen(true),
28 m_statsWelcomeScreen(true),
29− m_failedLogins(0)
29+ m_failedLogins(0),
30+ m_demoEdges(false)
30{
31{
31}
32}
32
33
@@ -44,12 +45,13 @@ void AccountsService::setUser(const QString &user)
44
45
45bool AccountsService::demoEdges() const
46bool AccountsService::demoEdges() const
46{
47{
47− return false;
48+ return m_demoEdges;
48}
49}
49
50
50void AccountsService::setDemoEdges(bool demoEdges)
51void AccountsService::setDemoEdges(bool demoEdges)
51{
52{
52− Q_UNUSED(demoEdges)
53+ m_demoEdges = demoEdges;
54+ Q_EMIT demoEdgesChanged();
53}
55}
54
56
55bool AccountsService::enableLauncherWhileLocked() const
57bool AccountsService::enableLauncherWhileLocked() const
tests/mocks/AccountsService/AccountsService.h+1
@@ -101,6 +101,7 @@ private:
101 QString m_user;
101 QString m_user;
102 bool m_statsWelcomeScreen;
102 bool m_statsWelcomeScreen;
103 uint m_failedLogins;
103 uint m_failedLogins;
104+ bool m_demoEdges;
104};
105};
105
106
106#endif
107#endif
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+117−101
@@ -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")
@@ -401,7 +417,7 @@ Item {
401 }
417 }
402
418
403 function test_quicklist_positioning(data) {
419 function test_quicklist_positioning(data) {
404− revealer.dragLauncherIntoView();
420+ 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")
@@ -428,7 +444,7 @@ Item {
428
444
429 // Click somewhere in the empty space to dismiss the launcher
445 // Click somewhere in the empty space to dismiss the launcher
430 mouseClick(launcher, launcher.width - units.gu(1), units.gu(1));
446 mouseClick(launcher, launcher.width - units.gu(1), units.gu(1));
431− revealer.waitUntilLauncherDisappears()
447+ waitUntilLauncherDisappears()
432 }
448 }
433
449
434 function test_quicklist_click_data() {
450 function test_quicklist_click_data() {
@@ -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+117−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"
@@ -133,10 +138,14 @@ Row {
133
138
134 sessionSpy.target = findChild(shell, "greeter")
139 sessionSpy.target = findChild(shell, "greeter")
135 dashCommunicatorSpy.target = findInvisibleChild(shell, "dashCommunicator");
140 dashCommunicatorSpy.target = findInvisibleChild(shell, "dashCommunicator");
141+
142+ var launcher = findChild(shell, "launcher");
143+ launcherShowDashHomeSpy.target = launcher;
136 }
144 }
137
145
138 function cleanup() {
146 function cleanup() {
139 tryCompare(shell, "enabled", true); // make sure greeter didn't leave us in disabled state
147 tryCompare(shell, "enabled", true); // make sure greeter didn't leave us in disabled state
148+ launcherShowDashHomeSpy.target = null;
140
149
141 shellLoader.itemDestroyed = false;
150 shellLoader.itemDestroyed = false;
142
151
@@ -408,8 +417,7 @@ Row {
408 touchFlick(launcherPanel, touchStartX, touchStartY, touchStartX, 0);
417 touchFlick(launcherPanel, touchStartX, touchStartY, touchStartX, 0);
409 tryCompare(launcherPanel, "moving", false);
418 tryCompare(launcherPanel, "moving", false);
410
419
411− // NB tapping (i.e., using touch events) doesn't activate the icon... go figure...
412− mouseClick(appIcon, appIcon.width / 2, appIcon.height / 2);
420+ tap(appIcon, appIcon.width / 2, appIcon.height / 2);
413 }
421 }
414
422
415 function showIndicators() {
423 function showIndicators() {
@@ -576,5 +584,111 @@ Row {
576
584
577 tryCompare(greeter, "showProgress", data.unlocked ? 0 : 1);
585 tryCompare(greeter, "showProgress", data.unlocked ? 0 : 1);
578 }
586 }
587+
588+ function test_tapOnRightEdgeReachesApplicationSurface() {
589+ var topmostSpreadDelegate = findChild(shell, "appDelegate0");
590+ var topmostSurface = findChild(topmostSpreadDelegate, "surfaceContainer").surface;
591+ var rightEdgeDragArea = findChild(shell, "spreadDragArea");
592+
593+ topmostSurface.touchPressCount = 0;
594+ topmostSurface.touchReleaseCount = 0;
595+
596+ var tapPoint = rightEdgeDragArea.mapToItem(shell, rightEdgeDragArea.width / 2,
597+ rightEdgeDragArea.height / 2);
598+
599+ tap(shell, tapPoint.x, tapPoint.y);
600+
601+ tryCompare(topmostSurface, "touchPressCount", 1);
602+ tryCompare(topmostSurface, "touchReleaseCount", 1);
603+ }
604+
605+ /*
606+ Perform a right edge drag over an application surface and check
607+ that no touch event was sent to it (ie, they were all consumed
608+ by the right-edge drag area)
609+ */
610+ function test_rightEdgeDragDoesNotReachApplicationSurface() {
611+ var topmostSpreadDelegate = findChild(shell, "appDelegate0");
612+ var topmostSurface = findChild(topmostSpreadDelegate, "surfaceContainer").surface;
613+ var rightEdgeDragArea = findChild(shell, "spreadDragArea");
614+
615+ topmostSurface.touchPressCount = 0;
616+ topmostSurface.touchReleaseCount = 0;
617+
618+ var gestureStartPoint = rightEdgeDragArea.mapToItem(shell, rightEdgeDragArea.width / 2,
619+ rightEdgeDragArea.height / 2);
620+
621+ touchFlick(shell,
622+ gestureStartPoint.x /* fromX */, gestureStartPoint.y /* fromY */,
623+ units.gu(1) /* toX */, gestureStartPoint.y /* toY */);
624+
625+ tryCompare(topmostSurface, "touchPressCount", 0);
626+ tryCompare(topmostSurface, "touchReleaseCount", 0);
627+ }
628+
629+ function waitUntilFocusedApplicationIsShowingItsSurface()
630+ {
631+ var spreadDelegate = findChild(shell, "appDelegate0");
632+ var appState = findInvisibleChild(spreadDelegate, "applicationWindowStateGroup");
633+ tryCompare(appState, "state", "surface");
634+ var transitions = appState.transitions;
635+ for (var i = 0; i < transitions.length; ++i) {
636+ var transition = transitions[i];
637+ tryCompare(transition, "running", false, 2000);
638+ }
639+ }
640+
641+ function swipeFromRightEdgeToShowAppSpread()
642+ {
643+ // perform a right-edge drag to show the spread
644+ var touchStartX = shell.width - (shell.edgeSize / 2)
645+ var touchStartY = shell.height / 2;
646+ touchFlick(shell, touchStartX, touchStartY, units.gu(1) /* endX */, touchStartY /* endY */);
647+
648+ // check if it's indeed showing the spread
649+ var stage = findChild(shell, "stage");
650+ var spreadView = findChild(stage, "spreadView");
651+ tryCompare(spreadView, "phase", 2);
652+ }
653+
654+ function test_tapUbuntuIconInLauncherOverAppSpread() {
655+
656+ waitUntilFocusedApplicationIsShowingItsSurface();
657+
658+ swipeFromRightEdgeToShowAppSpread();
659+
660+ var launcher = findChild(shell, "launcher");
661+
662+ // ensure the launcher dimissal timer never gets triggered during the test run
663+ var dismissTimer = findInvisibleChild(launcher, "dismissTimer");
664+ dismissTimer.interval = 60 * 60 * 1000;
665+
666+ dragLauncherIntoView();
667+
668+ // Emulate a tap with a finger, where the touch position drifts during the tap.
669+ {
670+ var buttonShowDashHome = findChild(launcher, "buttonShowDashHome");
671+ var startPos = buttonShowDashHome.mapToItem(shell,
672+ buttonShowDashHome.width * 0.2,
673+ buttonShowDashHome.height * 0.2);
674+ var endPos = buttonShowDashHome.mapToItem(shell,
675+ buttonShowDashHome.width * 0.8,
676+ buttonShowDashHome.height * 0.8);
677+ touchFlick(shell, startPos.x, startPos.y, endPos.x, endPos.y);
678+ }
679+
680+ compare(launcherShowDashHomeSpy.count, 1);
681+
682+ // check that the stage has left spread mode.
683+ {
684+ var stage = findChild(shell, "stage");
685+ var spreadView = findChild(stage, "spreadView");
686+ tryCompare(spreadView, "phase", 0);
687+ }
688+
689+ // check that the launcher got dismissed
690+ var launcherPanel = findChild(shell, "launcherPanel");
691+ tryCompare(launcherPanel, "x", -launcherPanel.width);
692+ }
579 }
693 }
580}
694}
tests/qmltests/tst_ShellWithPin.qml+90−43
@@ -21,15 +21,17 @@ import AccountsService 0.1
21import GSettings 1.0
21import GSettings 1.0
22import LightDM 0.1 as LightDM
22import LightDM 0.1 as LightDM
23import Ubuntu.SystemImage 0.1
23import Ubuntu.SystemImage 0.1
24+import Ubuntu.Telephony 0.1 as Telephony
24import Unity.Application 0.1
25import Unity.Application 0.1
25import Unity.Test 0.1 as UT
26import Unity.Test 0.1 as UT
26import Powerd 0.1
27import Powerd 0.1
27
28
28import "../../qml"
29import "../../qml"
29
30
30−Row {
31+Item {
31 id: root
32 id: root
32− spacing: 0
33+ width: contentRow.width
34+ height: contentRow.height
33
35
34 QtObject {
36 QtObject {
35 id: applicationArguments
37 id: applicationArguments
@@ -47,34 +49,36 @@ Row {
47 }
49 }
48 }
50 }
49
51
50− Loader {
52+ Row {
51− id: shellLoader
53+ id: contentRow
52
54
53− width: units.gu(40)
55+ Loader {
54− height: units.gu(71)
56+ id: shellLoader
55
57
58+ width: units.gu(40)
59+ height: units.gu(71)
60+
56− property bool itemDestroyed: false
61+ property bool itemDestroyed: false
57− sourceComponent: Component {
62+ sourceComponent: Component {
58− Shell {
63+ Shell {
59− Component.onDestruction: {
64+ Component.onDestruction: {
60− shellLoader.itemDestroyed = true
65+ shellLoader.itemDestroyed = true
66+ }
67+ maxFailedLogins: maxRetriesTextField.text
61 }
68 }
62− maxFailedLogins: maxRetriesTextField.text
63 }
69 }
64 }
70 }
65− }
66
71
67− Rectangle {
72+ Rectangle {
68− color: "white"
73+ color: "white"
69− width: units.gu(30)
74+ width: units.gu(30)
70− height: shellLoader.height
75+ height: shellLoader.height
71
76
72− Column {
77+ Column {
73− anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
78+ anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
74− spacing: units.gu(1)
79+ spacing: units.gu(1)
75− Row {
76− anchors { left: parent.left; right: parent.right }
77 Button {
80 Button {
81+ anchors { left: parent.left; right: parent.right }
78 text: "Show Greeter"
82 text: "Show Greeter"
79 onClicked: {
83 onClicked: {
80 if (shellLoader.status !== Loader.Ready)
84 if (shellLoader.status !== Loader.Ready)
@@ -86,15 +90,15 @@ Row {
86 }
90 }
87 }
91 }
88 }
92 }
89− }
90
93
91− Label {
94+ Label {
92− text: "Max retries:"
95+ text: "Max retries:"
93− color: "black"
96+ color: "black"
94− }
97+ }
95− TextField {
98+ TextField {
96− id: maxRetriesTextField
99+ id: maxRetriesTextField
97− text: "-1"
100+ text: "-1"
101+ }
98 }
102 }
99 }
103 }
100 }
104 }
@@ -110,7 +114,13 @@ Row {
110 signalName: "resettingDevice"
114 signalName: "resettingDevice"
111 }
115 }
112
116
117+ Telephony.CallEntry {
118+ id: phoneCall
119+ phoneNumber: "+447812221111"
120+ }
121+
113 UT.UnityTestCase {
122 UT.UnityTestCase {
123+ id: testCase
114 name: "ShellWithPin"
124 name: "ShellWithPin"
115 when: windowShown
125 when: windowShown
116
126
@@ -120,10 +130,9 @@ Row {
120 tryCompare(shell, "enabled", true); // will be enabled when greeter is all ready
130 tryCompare(shell, "enabled", true); // will be enabled when greeter is all ready
121 sessionSpy.target = findChild(shell, "greeter")
131 sessionSpy.target = findChild(shell, "greeter")
122 swipeAwayGreeter()
132 swipeAwayGreeter()
133+ waitForLockscreen()
123 shell.failedLoginsDelayAttempts = -1
134 shell.failedLoginsDelayAttempts = -1
124 maxRetriesTextField.text = "-1"
135 maxRetriesTextField.text = "-1"
125− AccountsService.enableLauncherWhileLocked = true
126− AccountsService.enableIndicatorsWhileLocked = true
127 }
136 }
128
137
129 function cleanup() {
138 function cleanup() {
@@ -146,6 +155,10 @@ Row {
146 // kill all (fake) running apps
155 // kill all (fake) running apps
147 killApps()
156 killApps()
148
157
158+ AccountsService.enableLauncherWhileLocked = true
159+ AccountsService.enableIndicatorsWhileLocked = true
160+ AccountsService.demoEdges = false
161+
149 // reload our test subject to get it in a fresh state once again
162 // reload our test subject to get it in a fresh state once again
150 shellLoader.active = true
163 shellLoader.active = true
151
164
@@ -172,8 +185,9 @@ Row {
172
185
173 // wait until the animation has finished
186 // wait until the animation has finished
174 tryCompare(greeter, "showProgress", 0);
187 tryCompare(greeter, "showProgress", 0);
188+ }
175
189
176− // and for pin to be ready
190+ function waitForLockscreen() {
177 var lockscreen = findChild(shell, "lockscreen");
191 var lockscreen = findChild(shell, "lockscreen");
178 var pinPadLoader = findChild(lockscreen, "pinPadLoader");
192 var pinPadLoader = findChild(lockscreen, "pinPadLoader");
179 tryCompare(pinPadLoader, "status", Loader.Ready)
193 tryCompare(pinPadLoader, "status", Loader.Ready)
@@ -185,10 +199,20 @@ Row {
185 for (var i = 0; i < pin.length; ++i) {
199 for (var i = 0; i < pin.length; ++i) {
186 var character = pin.charAt(i)
200 var character = pin.charAt(i)
187 var button = findChild(shell, "pinPadButton" + character)
201 var button = findChild(shell, "pinPadButton" + character)
188− mouseClick(button, units.gu(1), units.gu(1))
202+ tap(button)
189 }
203 }
190 }
204 }
191
205
206+ function confirmLockedApp(app) {
207+ var greeter = findChild(shell, "greeter")
208+ var lockscreen = findChild(shell, "lockscreen")
209+ tryCompare(greeter, "shown", false)
210+ tryCompare(lockscreen, "shown", false)
211+ tryCompare(greeter, "hasLockedApp", true)
212+ tryCompare(greeter, "lockedApp", app)
213+ tryCompare(ApplicationManager, "focusedApplicationId", app)
214+ }
215+
192 function test_login() {
216 function test_login() {
193 sessionSpy.clear()
217 sessionSpy.clear()
194 tryCompare(sessionSpy, "count", 0)
218 tryCompare(sessionSpy, "count", 0)
@@ -196,6 +220,25 @@ Row {
196 tryCompare(sessionSpy, "count", 1)
220 tryCompare(sessionSpy, "count", 1)
197 }
221 }
198
222
223+ function test_edgeDemoHidesLockscreen() {
224+ LightDM.Greeter.showGreeter()
225+ sessionSpy.clear()
226+ var lockscreen = findChild(shell, "lockscreen")
227+
228+ tryCompare(lockscreen, "shown", true)
229+ AccountsService.demoEdges = true
230+ tryCompare(lockscreen, "shown", false)
231+
232+ swipeAwayGreeter()
233+ tryCompare(sessionSpy, "count", 1)
234+
235+ // Lockscreen is only hidden by the edge demo, so if we turn that
236+ // off and show greeter again, lockscreen should appear
237+ AccountsService.demoEdges = false
238+ LightDM.Greeter.showGreeter()
239+ tryCompare(lockscreen, "shown", true)
240+ }
241+
199 function test_disabledEdges() {
242 function test_disabledEdges() {
200 var launcher = findChild(shell, "launcher")
243 var launcher = findChild(shell, "launcher")
201 tryCompare(launcher, "available", true)
244 tryCompare(launcher, "available", true)
@@ -217,7 +260,7 @@ Row {
217 var launcher = findChild(shell, "launcher")
260 var launcher = findChild(shell, "launcher")
218 var stage = findChild(shell, "stage")
261 var stage = findChild(shell, "stage")
219
262
220− mouseClick(emergencyButton, units.gu(1), units.gu(1))
263+ tap(emergencyButton)
221
264
222 tryCompare(greeter, "lockedApp", "dialer-app")
265 tryCompare(greeter, "lockedApp", "dialer-app")
223 tryCompare(greeter, "hasLockedApp", true)
266 tryCompare(greeter, "hasLockedApp", true)
@@ -244,7 +287,7 @@ Row {
244 function test_emergencyCallCrash() {
287 function test_emergencyCallCrash() {
245 var lockscreen = findChild(shell, "lockscreen")
288 var lockscreen = findChild(shell, "lockscreen")
246 var emergencyButton = findChild(lockscreen, "emergencyCallLabel")
289 var emergencyButton = findChild(lockscreen, "emergencyCallLabel")
247− mouseClick(emergencyButton, units.gu(1), units.gu(1))
290+ tap(emergencyButton)
248
291
249 tryCompare(lockscreen, "shown", false)
292 tryCompare(lockscreen, "shown", false)
250 killApps() // kill dialer-app, as if it crashed
293 killApps() // kill dialer-app, as if it crashed
@@ -254,7 +297,7 @@ Row {
254 function test_emergencyCallAppLaunch() {
297 function test_emergencyCallAppLaunch() {
255 var lockscreen = findChild(shell, "lockscreen")
298 var lockscreen = findChild(shell, "lockscreen")
256 var emergencyButton = findChild(lockscreen, "emergencyCallLabel")
299 var emergencyButton = findChild(lockscreen, "emergencyCallLabel")
257− mouseClick(emergencyButton, units.gu(1), units.gu(1))
300+ tap(emergencyButton)
258
301
259 tryCompare(lockscreen, "shown", false)
302 tryCompare(lockscreen, "shown", false)
260 ApplicationManager.startApplication("gallery-app", ApplicationManager.NoFlag)
303 ApplicationManager.startApplication("gallery-app", ApplicationManager.NoFlag)
@@ -299,7 +342,7 @@ Row {
299
342
300 var dialog = findChild(root, "infoPopup")
343 var dialog = findChild(root, "infoPopup")
301 var button = findChild(dialog, "infoPopupOkButton")
344 var button = findChild(dialog, "infoPopupOkButton")
302− mouseClick(button, units.gu(1), units.gu(1))
345+ tap(button)
303 tryCompareFunction(function() {return findChild(root, "infoPopup")}, null)
346 tryCompareFunction(function() {return findChild(root, "infoPopup")}, null)
304
347
305 tryCompare(resetSpy, "count", 0)
348 tryCompare(resetSpy, "count", 0)
@@ -320,11 +363,9 @@ Row {
320 tryCompare(shell, "sideStageEnabled", false)
363 tryCompare(shell, "sideStageEnabled", false)
321 tryCompare(applicationsDisplayLoader, "tabletMode", false)
364 tryCompare(applicationsDisplayLoader, "tabletMode", false)
322
365
323− var app = ApplicationManager.startApplication("dialer-app")
366+ var lockscreen = findChild(shell, "lockscreen")
324−
367+ lockscreen.emergencyCall()
325− var greeter = findChild(shell, "greeter")
368+ confirmLockedApp("dialer-app")
326− tryCompare(greeter, "showProgress", 0)
327− tryCompare(greeter, "hasLockedApp", true)
328
369
329 // OK, we're in. Now try (but fail) to switch to tablet mode
370 // OK, we're in. Now try (but fail) to switch to tablet mode
330 shell.tablet = true
371 shell.tablet = true
@@ -333,9 +374,15 @@ Row {
333
374
334 // And when we kill the app, we go back to locked tablet mode
375 // And when we kill the app, we go back to locked tablet mode
335 killApps()
376 killApps()
377+ var greeter = findChild(shell, "greeter")
336 tryCompare(greeter, "showProgress", 1)
378 tryCompare(greeter, "showProgress", 1)
337 tryCompare(shell, "sideStageEnabled", true)
379 tryCompare(shell, "sideStageEnabled", true)
338 tryCompare(applicationsDisplayLoader, "tabletMode", true)
380 tryCompare(applicationsDisplayLoader, "tabletMode", true)
339 }
381 }
382+
383+ function test_emergencyDialerIncoming() {
384+ callManager.foregroundCall = phoneCall
385+ confirmLockedApp("dialer-app")
386+ }
340 }
387 }
341}
388}
tests/qmltests/tst_TabletShell.qml+10−2
@@ -16,6 +16,7 @@
16
16
17import QtQuick 2.0
17import QtQuick 2.0
18import QtTest 1.0
18import QtTest 1.0
19+import AccountsService 0.1
19import GSettings 1.0
20import GSettings 1.0
20import LightDM 0.1 as LightDM
21import LightDM 0.1 as LightDM
21import Ubuntu.Components 1.1
22import Ubuntu.Components 1.1
@@ -145,6 +146,7 @@ Row {
145 killApps()
146 killApps()
146
147
147 unlockAllModemsSpy.clear()
148 unlockAllModemsSpy.clear()
149+ AccountsService.demoEdges = false
148
150
149 // reload our test subject to get it in a fresh state once again
151 // reload our test subject to get it in a fresh state once again
150 shellLoader.active = true
152 shellLoader.active = true
@@ -277,13 +279,19 @@ Row {
277
279
278 function test_leftEdgeDrag_data() {
280 function test_leftEdgeDrag_data() {
279 return [
281 return [
280− {tag: "without password", user: "no-password", loggedIn: true},
282+ {tag: "without password", user: "no-password", loggedIn: true, demo: false},
281− {tag: "with password", user: "has-password", loggedIn: false},
283+ {tag: "with password", user: "has-password", loggedIn: false, demo: false},
284+ {tag: "with demo", user: "has-password", loggedIn: true, demo: true},
282 ]
285 ]
283 }
286 }
284
287
285 function test_leftEdgeDrag(data) {
288 function test_leftEdgeDrag(data) {
286 selectUser(data.user)
289 selectUser(data.user)
290+
291+ AccountsService.demoEdges = data.demo
292+ var edgeDemo = findChild(shell, "edgeDemo")
293+ tryCompare(edgeDemo, "running", data.demo)
294+
287 swipeFromLeftEdge(shell.width * 0.75)
295 swipeFromLeftEdge(shell.width * 0.75)
288 wait(500) // to give time to handle dash() signal from Launcher
296 wait(500) // to give time to handle dash() signal from Launcher
289 confirmLoggedIn(data.loggedIn)
297 confirmLoggedIn(data.loggedIn)
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+11
@@ -321,7 +321,16 @@ TestCase {
321 event.commit()
321 event.commit()
322 }
322 }
323
323
324+ /*! \brief Tap the item with a touch event.
325+
326+ \param item The item to be tapped
327+ \param x The x coordinate of the tap, defaults to horizontal center
328+ \param y The y coordinate of the tap, defaults to vertical center
329+ */
324 function tap(item, x, y) {
330 function tap(item, x, y) {
331+ if (typeof x !== "number") x = item.width / 2;
332+ if (typeof y !== "number") y = item.height / 2;
333+
325 var root = fetchRootItem(item)
334 var root = fetchRootItem(item)
326 var rootPoint = item.mapToItem(root, x, y)
335 var rootPoint = item.mapToItem(root, x, y)
327
336
@@ -335,6 +344,8 @@ TestCase {
335 }
344 }
336
345
337 Component.onCompleted: {
346 Component.onCompleted: {
347+ UT.Util.ensureTouchRegistryInstalled();
348+
338 var rootItem = parent;
349 var rootItem = parent;
339 while (rootItem.parent != undefined) {
350 while (rootItem.parent != undefined) {
340 rootItem = rootItem.parent;
351 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)