on xenial help sign in
Sidestage load/unload redesign
bc2da59Nick Dedekind
CMakeLists.txt+1−1
@@ -57,7 +57,7 @@ find_package(Qt5DBus 5.4 REQUIRED)
57find_package(Qt5Concurrent 5.4 REQUIRED)
57find_package(Qt5Concurrent 5.4 REQUIRED)
58find_package(Qt5Sql 5.4 REQUIRED)
58find_package(Qt5Sql 5.4 REQUIRED)
59
59
60−pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=13)
60+pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=14)
61pkg_check_modules(GIO REQUIRED gio-2.0>=2.32)
61pkg_check_modules(GIO REQUIRED gio-2.0>=2.32)
62pkg_check_modules(GLIB REQUIRED glib-2.0>=2.32)
62pkg_check_modules(GLIB REQUIRED glib-2.0>=2.32)
63pkg_check_modules(QMENUMODEL REQUIRED qmenumodel)
63pkg_check_modules(QMENUMODEL REQUIRED qmenumodel)
CODING+2
@@ -54,6 +54,8 @@ $ ./run.sh -p OR -k
54Notes
54Notes
55-----
55-----
56- to navigate, utilize the mouse left button as you would your finger
56- to navigate, utilize the mouse left button as you would your finger
57+- a three point drag (for side stage operations) can be performed by using
58+ the Shift+Control+Alt (all together) keyboard modifiers while using the mouse.
57- to get the translations work, currently you have to do make install to
59- to get the translations work, currently you have to do make install to
58 put the *.mo files into correct structure. We'll look for a better solution
60 put the *.mo files into correct structure. We'll look for a better solution
59 later.
61 later.
debian/control+4−4
@@ -29,7 +29,7 @@ Build-Depends: cmake,
29 libqt5xmlpatterns5-dev,
29 libqt5xmlpatterns5-dev,
30 libsystemsettings-dev,
30 libsystemsettings-dev,
31 libudev-dev,
31 libudev-dev,
32− libunity-api-dev (>= 7.107),
32+ libunity-api-dev (>= 7.108),
33 libusermetricsoutput1-dev,
33 libusermetricsoutput1-dev,
34# Need those X11 libs touch emulation from mouse events in manual QML tests on a X11 desktop
34# Need those X11 libs touch emulation from mouse events in manual QML tests on a X11 desktop
35 libx11-dev[!armhf],
35 libx11-dev[!armhf],
@@ -101,7 +101,7 @@ Depends: dmz-cursor-theme,
101 qml-module-qt-labs-folderlistmodel,
101 qml-module-qt-labs-folderlistmodel,
102 qml-module-qtquick-xmllistmodel,
102 qml-module-qtquick-xmllistmodel,
103 qml-module-qtsysteminfo,
103 qml-module-qtsysteminfo,
104− qtdeclarative5-qtmir-plugin (>= 0.4.5),
104+ qtdeclarative5-qtmir-plugin (>= 0.4.8),
105 qtdeclarative5-ubuntu-telephony0.1,
105 qtdeclarative5-ubuntu-telephony0.1,
106 qtdeclarative5-ubuntu-web-plugin,
106 qtdeclarative5-ubuntu-web-plugin,
107 ubuntu-system-settings,
107 ubuntu-system-settings,
@@ -130,7 +130,7 @@ Depends: qml-module-qtquick-layouts,
130 qtdeclarative5-ubuntu-ui-toolkit-plugin (>= 1.3.1845) | qtdeclarative5-ubuntu-ui-toolkit-plugin-gles (>= 1.3.1845),
130 qtdeclarative5-ubuntu-ui-toolkit-plugin (>= 1.3.1845) | qtdeclarative5-ubuntu-ui-toolkit-plugin-gles (>= 1.3.1845),
131 qtdeclarative5-unity-notifications-plugin (>= 0.1.2) | unity-notifications-impl,
131 qtdeclarative5-unity-notifications-plugin (>= 0.1.2) | unity-notifications-impl,
132 ubuntu-thumbnailer-impl-0,
132 ubuntu-thumbnailer-impl-0,
133− unity-application-impl-13,
133+ unity-application-impl-14,
134 unity-notifications-impl-3,
134 unity-notifications-impl-3,
135 unity-plugin-scopes | unity-scopes-impl,
135 unity-plugin-scopes | unity-scopes-impl,
136 unity-scopes-impl-10,
136 unity-scopes-impl-10,
@@ -176,7 +176,7 @@ Pre-Depends: ${misc:Pre-Depends},
176Depends: ${misc:Depends},
176Depends: ${misc:Depends},
177 ${shlibs:Depends},
177 ${shlibs:Depends},
178Provides: unity-application-impl,
178Provides: unity-application-impl,
179− unity-application-impl-13,
179+ unity-application-impl-14,
180Replaces: unity8-autopilot (<< 8.02+15.04.20150422-0ubuntu1)
180Replaces: unity8-autopilot (<< 8.02+15.04.20150422-0ubuntu1)
181Description: Fake environment for running Unity 8 shell
181Description: Fake environment for running Unity 8 shell
182 Provides fake implementations of some QML modules used by Unity 8 shell
182 Provides fake implementations of some QML modules used by Unity 8 shell
libs/UbuntuGestures/Timer.h+5
@@ -35,6 +35,11 @@ public:
35 virtual int interval() const = 0;
35 virtual int interval() const = 0;
36 virtual void setInterval(int msecs) = 0;
36 virtual void setInterval(int msecs) = 0;
37 virtual void start() { m_isRunning = true; }
37 virtual void start() { m_isRunning = true; }
38+ virtual void start(int msecs)
39+ {
40+ setInterval(msecs);
41+ start();
42+ }
38 virtual void stop() { m_isRunning = false; }
43 virtual void stop() { m_isRunning = false; }
39 bool isRunning() const { return m_isRunning; }
44 bool isRunning() const { return m_isRunning; }
40 virtual bool isSingleShot() const = 0;
45 virtual bool isSingleShot() const = 0;
libs/UbuntuGestures/TouchRegistry.cpp+15−10
@@ -359,10 +359,6 @@ void TouchRegistry::removeCandidateOwnerForTouchByIndex(Pool<TouchRegistry::Touc
359
359
360void TouchRegistry::requestTouchOwnership(int id, QQuickItem *candidate)
360void TouchRegistry::requestTouchOwnership(int id, QQuickItem *candidate)
361{
361{
362− #if TOUCHREGISTRY_DEBUG
363− UG_DEBUG << "requestTouchOwnership id " << id << "candidate" << candidate;
364− #endif
365−
366 Pool<TouchInfo>::Iterator touchInfo = findTouchInfo(id);
362 Pool<TouchInfo>::Iterator touchInfo = findTouchInfo(id);
367 if (!touchInfo) { qFatal("TouchRegistry: Failed to find TouchInfo"); }
363 if (!touchInfo) { qFatal("TouchRegistry: Failed to find TouchInfo"); }
368
364
@@ -379,6 +375,9 @@ void TouchRegistry::requestTouchOwnership(int id, QQuickItem *candidate)
379 break;
375 break;
380 }
376 }
381 }
377 }
378+ #if TOUCHREGISTRY_DEBUG
379+ UG_DEBUG << "requestTouchOwnership id " << id << "candidate" << candidate << "index: " << candidateIndex;
380+ #endif
382
381
383 // add it as a candidate if not present yet
382 // add it as a candidate if not present yet
384 if (candidateIndex < 0) {
383 if (candidateIndex < 0) {
@@ -533,16 +532,22 @@ void TouchRegistry::TouchInfo::notifyCandidatesOfOwnershipResolution()
533 << " gained) to candidate" << candidates[0].item;
532 << " gained) to candidate" << candidates[0].item;
534 #endif
533 #endif
535
534
536− TouchOwnershipEvent gainedOwnershipEvent(id, true /*gained*/);
535+ // need to take a copy of the item list in case
537− QCoreApplication::sendEvent(candidates[0].item, &gainedOwnershipEvent);
536+ // we call back in to remove candidate during the lost ownership event.
537+ QList<QPointer<QQuickItem>> items;
538+ Q_FOREACH(const CandidateInfo& info, candidates) {
539+ items << info.item;
540+ }
538
541
542+ TouchOwnershipEvent gainedOwnershipEvent(id, true /*gained*/);
543+ QCoreApplication::sendEvent(items[0], &gainedOwnershipEvent);
539
544
540 TouchOwnershipEvent lostOwnershipEvent(id, false /*gained*/);
545 TouchOwnershipEvent lostOwnershipEvent(id, false /*gained*/);
541− for (int i = 1; i < candidates.count(); ++i) {
546+ for (int i = 1; i < items.count(); ++i) {
542 #if TOUCHREGISTRY_DEBUG
547 #if TOUCHREGISTRY_DEBUG
543− UG_DEBUG << "sending TouchWonershipEvent(id =" << id << " lost) to candidate"
548+ UG_DEBUG << "sending TouchOwnershipEvent(id =" << id << " lost) to candidate"
544− << candidates[i].item;
549+ << items[i];
545 #endif
550 #endif
546− QCoreApplication::sendEvent(candidates[i].item, &lostOwnershipEvent);
551+ QCoreApplication::sendEvent(items[i], &lostOwnershipEvent);
547 }
552 }
548}
553}
plugins/Cursor/Cursor.qmltypesnew file+78
@@ -0,0 +1,78 @@
1+import QtQuick.tooling 1.1
2+
3+// This file describes the plugin-supplied types contained in the library.
4+// It is used for QML tooling purposes only.
5+//
6+// This file was auto-generated by:
7+// 'qmlplugindump -notrelocatable Cursor 1.0 plugins'
8+
9+Module {
10+ Component {
11+ name: "MirMousePointerInterface"
12+ defaultProperty: "data"
13+ prototype: "QQuickItem"
14+ Property { name: "cursorName"; type: "string"; isReadonly: true }
15+ Property { name: "themeName"; type: "string"; isReadonly: true }
16+ Property { name: "hotspotX"; type: "double"; isReadonly: true }
17+ Property { name: "hotspotY"; type: "double"; isReadonly: true }
18+ Signal {
19+ name: "cursorNameChanged"
20+ Parameter { name: "name"; type: "string" }
21+ }
22+ Signal {
23+ name: "themeNameChanged"
24+ Parameter { name: "name"; type: "string" }
25+ }
26+ Signal {
27+ name: "hotspotXChanged"
28+ Parameter { name: "value"; type: "double" }
29+ }
30+ Signal {
31+ name: "hotspotYChanged"
32+ Parameter { name: "value"; type: "double" }
33+ }
34+ Method {
35+ name: "handleMouseEvent"
36+ Parameter { name: "timestamp"; type: "ulong" }
37+ Parameter { name: "movement"; type: "QPointF" }
38+ Parameter { name: "buttons"; type: "Qt::MouseButtons" }
39+ Parameter { name: "modifiers"; type: "Qt::KeyboardModifiers" }
40+ }
41+ Method {
42+ name: "handleWheelEvent"
43+ Parameter { name: "timestamp"; type: "ulong" }
44+ Parameter { name: "angleDelta"; type: "QPoint" }
45+ Parameter { name: "modifiers"; type: "Qt::KeyboardModifiers" }
46+ }
47+ }
48+ Component {
49+ name: "MousePointer"
50+ defaultProperty: "data"
51+ prototype: "MirMousePointerInterface"
52+ exports: ["Cursor/MousePointer 1.0"]
53+ exportMetaObjectRevisions: [0]
54+ Signal {
55+ name: "pushedLeftBoundary"
56+ Parameter { name: "amount"; type: "double" }
57+ Parameter { name: "buttons"; type: "Qt::MouseButtons" }
58+ }
59+ Signal {
60+ name: "pushedRightBoundary"
61+ Parameter { name: "amount"; type: "double" }
62+ Parameter { name: "buttons"; type: "Qt::MouseButtons" }
63+ }
64+ Method {
65+ name: "handleMouseEvent"
66+ Parameter { name: "timestamp"; type: "ulong" }
67+ Parameter { name: "movement"; type: "QPointF" }
68+ Parameter { name: "buttons"; type: "Qt::MouseButtons" }
69+ Parameter { name: "modifiers"; type: "Qt::KeyboardModifiers" }
70+ }
71+ Method {
72+ name: "handleWheelEvent"
73+ Parameter { name: "timestamp"; type: "ulong" }
74+ Parameter { name: "angleDelta"; type: "QPoint" }
75+ Parameter { name: "modifiers"; type: "Qt::KeyboardModifiers" }
76+ }
77+ }
78+}
plugins/GlobalShortcut/GlobalShortcut.qmltypesnew file+31
@@ -0,0 +1,31 @@
1+import QtQuick.tooling 1.1
2+
3+// This file describes the plugin-supplied types contained in the library.
4+// It is used for QML tooling purposes only.
5+//
6+// This file was auto-generated by:
7+// 'qmlplugindump -notrelocatable GlobalShortcut 1.0 plugins'
8+
9+Module {
10+ Component {
11+ name: "GlobalShortcut"
12+ defaultProperty: "data"
13+ prototype: "QQuickItem"
14+ exports: ["GlobalShortcut/GlobalShortcut 1.0"]
15+ exportMetaObjectRevisions: [0]
16+ Property { name: "shortcut"; type: "QVariant" }
17+ Property { name: "active"; type: "bool" }
18+ Signal {
19+ name: "shortcutChanged"
20+ Parameter { name: "shortcut"; type: "QVariant" }
21+ }
22+ Signal {
23+ name: "triggered"
24+ Parameter { name: "shortcut"; type: "string" }
25+ }
26+ Signal {
27+ name: "activeChanged"
28+ Parameter { name: "active"; type: "bool" }
29+ }
30+ }
31+}
plugins/Ubuntu/Gestures/CMakeLists.txt+1
@@ -11,6 +11,7 @@ set(UbuntuGesturesQml_SOURCES
11 PressedOutsideNotifier.cpp
11 PressedOutsideNotifier.cpp
12 TouchDispatcher.cpp
12 TouchDispatcher.cpp
13 TouchGate.cpp
13 TouchGate.cpp
14+ TouchGestureArea.cpp
14)
15)
15
16
16add_definitions(-DUBUNTUGESTURESQML_LIBRARY)
17add_definitions(-DUBUNTUGESTURESQML_LIBRARY)
plugins/Ubuntu/Gestures/DirectionalDragArea.cpp+3
@@ -606,6 +606,9 @@ void DirectionalDragAreaPrivate::setStatus(Status newStatus)
606 Q_EMIT q->pressedChanged(true);
606 Q_EMIT q->pressedChanged(true);
607 break;
607 break;
608 case Recognized:
608 case Recognized:
609+ if (oldStatus == WaitingForTouch) { // for immediate recognition
610+ Q_EMIT q->pressedChanged(true);
611+ }
609 Q_EMIT q->draggingChanged(true);
612 Q_EMIT q->draggingChanged(true);
610 break;
613 break;
611 default:
614 default:
plugins/Ubuntu/Gestures/Gestures.qmltypes+86−45
@@ -34,7 +34,8 @@ Module {
34 "Leftwards": 1,
34 "Leftwards": 1,
35 "Downwards": 2,
35 "Downwards": 2,
36 "Upwards": 3,
36 "Upwards": 3,
37− "Horizontal": 4
37+ "Horizontal": 4,
38+ "Vertical": 5
38 }
39 }
39 }
40 }
40 Method {
41 Method {
@@ -59,14 +60,6 @@ Module {
59 prototype: "QQuickItem"
60 prototype: "QQuickItem"
60 exports: ["Ubuntu.Gestures/DirectionalDragArea 0.1"]
61 exports: ["Ubuntu.Gestures/DirectionalDragArea 0.1"]
61 exportMetaObjectRevisions: [0]
62 exportMetaObjectRevisions: [0]
62− Enum {
63− name: "Status"
64− values: {
65− "WaitingForTouch": 0,
66− "Undecided": 1,
67− "Recognized": 2
68− }
69− }
70 Property { name: "direction"; type: "Direction::Type" }
63 Property { name: "direction"; type: "Direction::Type" }
71 Property { name: "distance"; type: "double"; isReadonly: true }
64 Property { name: "distance"; type: "double"; isReadonly: true }
72 Property { name: "sceneDistance"; type: "double"; isReadonly: true }
65 Property { name: "sceneDistance"; type: "double"; isReadonly: true }
@@ -74,24 +67,19 @@ Module {
74 Property { name: "touchY"; type: "double"; isReadonly: true }
67 Property { name: "touchY"; type: "double"; isReadonly: true }
75 Property { name: "touchSceneX"; type: "double"; isReadonly: true }
68 Property { name: "touchSceneX"; type: "double"; isReadonly: true }
76 Property { name: "touchSceneY"; type: "double"; isReadonly: true }
69 Property { name: "touchSceneY"; type: "double"; isReadonly: true }
77− Property { name: "status"; type: "Status"; isReadonly: true }
78 Property { name: "dragging"; type: "bool"; isReadonly: true }
70 Property { name: "dragging"; type: "bool"; isReadonly: true }
79− Property { name: "maxDeviation"; type: "double" }
71+ Property { name: "pressed"; type: "bool"; isReadonly: true }
80− Property { name: "wideningAngle"; type: "double" }
81− Property { name: "distanceThreshold"; type: "double" }
72+ Property { name: "immediateRecognition"; type: "bool" }
82− Property { name: "minSpeed"; type: "double" }
83− Property { name: "maxSilenceTime"; type: "int" }
84− Property { name: "compositionTime"; type: "int" }
85 Signal {
73 Signal {
86 name: "directionChanged"
74 name: "directionChanged"
87 Parameter { name: "direction"; type: "Direction::Type" }
75 Parameter { name: "direction"; type: "Direction::Type" }
88 }
76 }
89 Signal {
77 Signal {
90− name: "statusChanged"
78+ name: "draggingChanged"
91− Parameter { name: "value"; type: "Status" }
79+ Parameter { name: "value"; type: "bool" }
92 }
80 }
93 Signal {
81 Signal {
94− name: "draggingChanged"
82+ name: "pressedChanged"
95 Parameter { name: "value"; type: "bool" }
83 Parameter { name: "value"; type: "bool" }
96 }
84 }
97 Signal {
85 Signal {
@@ -102,30 +90,6 @@ Module {
102 name: "sceneDistanceChanged"
90 name: "sceneDistanceChanged"
103 Parameter { name: "value"; type: "double" }
91 Parameter { name: "value"; type: "double" }
104 }
92 }
105− Signal {
106− name: "maxDeviationChanged"
107− Parameter { name: "value"; type: "double" }
108− }
109− Signal {
110− name: "wideningAngleChanged"
111− Parameter { name: "value"; type: "double" }
112− }
113− Signal {
114− name: "distanceThresholdChanged"
115− Parameter { name: "value"; type: "double" }
116− }
117− Signal {
118− name: "minSpeedChanged"
119− Parameter { name: "value"; type: "double" }
120− }
121− Signal {
122− name: "maxSilenceTimeChanged"
123− Parameter { name: "value"; type: "int" }
124− }
125− Signal {
126− name: "compositionTimeChanged"
127− Parameter { name: "value"; type: "int" }
128− }
129 Signal {
93 Signal {
130 name: "touchXChanged"
94 name: "touchXChanged"
131 Parameter { name: "value"; type: "double" }
95 Parameter { name: "value"; type: "double" }
@@ -142,7 +106,35 @@ Module {
142 name: "touchSceneYChanged"
106 name: "touchSceneYChanged"
143 Parameter { name: "value"; type: "double" }
107 Parameter { name: "value"; type: "double" }
144 }
108 }
145− Signal { name: "tapped" }
109+ Signal {
110+ name: "immediateRecognitionChanged"
111+ Parameter { name: "value"; type: "bool" }
112+ }
113+ Method { name: "removeTimeConstraints" }
114+ }
115+ Component {
116+ name: "FloatingFlickable"
117+ defaultProperty: "data"
118+ prototype: "QQuickItem"
119+ exports: ["Ubuntu.Gestures/FloatingFlickable 0.1"]
120+ exportMetaObjectRevisions: [0]
121+ Property { name: "contentWidth"; type: "double" }
122+ Property { name: "contentHeight"; type: "double" }
123+ Property { name: "contentX"; type: "double" }
124+ Property { name: "contentY"; type: "double" }
125+ Property { name: "direction"; type: "Direction::Type" }
126+ }
127+ Component {
128+ name: "GestureTouchPoint"
129+ prototype: "QObject"
130+ exports: ["Ubuntu.Gestures/GestureTouchPoint 0.1"]
131+ isCreatable: false
132+ exportMetaObjectRevisions: [0]
133+ Property { name: "pointId"; type: "int"; isReadonly: true }
134+ Property { name: "pressed"; type: "bool"; isReadonly: true }
135+ Property { name: "x"; type: "double"; isReadonly: true }
136+ Property { name: "y"; type: "double"; isReadonly: true }
137+ Property { name: "dragging"; type: "bool"; isReadonly: true }
146 }
138 }
147 Component {
139 Component {
148 name: "PressedOutsideNotifier"
140 name: "PressedOutsideNotifier"
@@ -163,6 +155,55 @@ Module {
163 name: "targetItemChanged"
155 name: "targetItemChanged"
164 Parameter { name: "item"; type: "QQuickItem"; isPointer: true }
156 Parameter { name: "item"; type: "QQuickItem"; isPointer: true }
165 }
157 }
158+ }
159+ Component {
160+ name: "TouchGestureArea"
161+ defaultProperty: "data"
162+ prototype: "QQuickItem"
163+ exports: ["Ubuntu.Gestures/TouchGestureArea 0.1"]
164+ exportMetaObjectRevisions: [0]
165+ Enum {
166+ name: "Status"
167+ values: {
168+ "WaitingForTouch": 0,
169+ "Undecided": 1,
170+ "Recognized": 2,
171+ "Rejected": 3
172+ }
173+ }
174+ Property { name: "touchPoints"; type: "GestureTouchPoint"; isList: true; isReadonly: true }
175+ Property { name: "dragging"; type: "bool"; isReadonly: true }
176+ Property { name: "minimumTouchPoints"; type: "int" }
177+ Property { name: "maximumTouchPoints"; type: "int" }
178+ Signal {
179+ name: "statusChanged"
180+ Parameter { name: "status"; type: "Status" }
181+ }
182+ Signal { name: "touchPointsUpdated" }
183+ Signal {
184+ name: "draggingChanged"
185+ Parameter { name: "dragging"; type: "bool" }
186+ }
187+ Signal {
188+ name: "minimumTouchPointsChanged"
189+ Parameter { name: "value"; type: "bool" }
190+ }
191+ Signal {
192+ name: "maximumTouchPointsChanged"
193+ Parameter { name: "value"; type: "bool" }
194+ }
195+ Signal {
166− Signal { name: "pressed" }
196+ name: "pressed"
197+ Parameter { name: "points"; type: "QList<QObject*>" }
198+ }
199+ Signal {
200+ name: "released"
201+ Parameter { name: "points"; type: "QList<QObject*>" }
202+ }
203+ Signal {
204+ name: "updated"
205+ Parameter { name: "points"; type: "QList<QObject*>" }
206+ }
207+ Signal { name: "clicked" }
167 }
208 }
168}
209}
plugins/Ubuntu/Gestures/TouchGestureArea.cppnew file+875
@@ -0,0 +1,875 @@
1+/*
2+ * Copyright (C) 2016 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 "TouchGestureArea.h"
18+
19+// local
20+#include "TouchOwnershipEvent.h"
21+#include "TouchRegistry.h"
22+#include "UnownedTouchEvent.h"
23+
24+#include <QGuiApplication>
25+#include <QStyleHints>
26+#include <private/qquickwindow_p.h>
27+
28+#define TOUCHGESTUREAREA_DEBUG 0
29+
30+// TODO - understand more about why we lose touch event releases.
31+// Workaround for now is to monitor all the touch points from first touch till
32+// all have been released; no matter if we've rejected the gesture.
33+
34+namespace {
35+
36+struct InternalStatus {
37+ enum Status {
38+ WaitingForTouch,
39+ WaitingForMoreTouches,
40+ WaitingForOwnership, //Recognizing,
41+ Recognized,
42+ WaitingForRejection,
43+ Rejected
44+ };
45+};
46+
47+TouchGestureArea::Status internalStatusToGestureStatus(int internalStatus) {
48+ switch (internalStatus) {
49+ case InternalStatus::WaitingForTouch: return TouchGestureArea::WaitingForTouch;
50+ case InternalStatus::WaitingForMoreTouches: return TouchGestureArea::Undecided;
51+ case InternalStatus::WaitingForOwnership: return TouchGestureArea::Undecided;
52+ case InternalStatus::Recognized: return TouchGestureArea::Recognized;
53+ case InternalStatus::WaitingForRejection: return TouchGestureArea::Recognized;
54+ case InternalStatus::Rejected: return TouchGestureArea::Rejected;
55+ }
56+ return TouchGestureArea::WaitingForTouch;
57+}
58+
59+}
60+
61+#if TOUCHGESTUREAREA_DEBUG
62+#define tgaDebug(params) qDebug().nospace() << "[TGA(" << qPrintable(objectName()) << ")] " << params
63+#include "DebugHelpers.h"
64+
65+namespace {
66+
67+const char *statusToString(int status)
68+{
69+ if (status == InternalStatus::WaitingForTouch) {
70+ return "WaitingForTouch";
71+ } else if (status == InternalStatus::WaitingForMoreTouches) {
72+ return "WaitingForMoreTouches";
73+ } else if (status == InternalStatus::WaitingForOwnership) {
74+ return "WaitingForOwnership";
75+ } else if (status == InternalStatus::Rejected) {
76+ return "Rejected";
77+ } else if (status == InternalStatus::WaitingForRejection) {
78+ return "WaitingForRejection";
79+ } else {
80+ return "Recognized";
81+ }
82+ return "Unknown";
83+}
84+
85+QString touchState(Qt::TouchPointState state) {
86+ switch (state) {
87+ case Qt::TouchPointPressed: return "pressed";
88+ case Qt::TouchPointMoved: return "moved";
89+ case Qt::TouchPointStationary: return "stationary";
90+ case Qt::TouchPointReleased: return "released";
91+ break;
92+ }
93+ return "unknown";
94+}
95+
96+QString touchesString(const QList<QObject*> touches) {
97+ QString str;
98+ Q_FOREACH(QObject* object, touches) {
99+ GestureTouchPoint* touchPoint = qobject_cast<GestureTouchPoint*>(object);
100+ if (touchPoint) {
101+ str += QStringLiteral("[%1 @ (%2, %3)], ").arg(touchPoint->id())
102+ .arg(touchPoint->x())
103+ .arg(touchPoint->y());
104+ }
105+ }
106+ return str;
107+}
108+
109+QString touchEventString(QTouchEvent* event) {
110+ if (!event) return QString();
111+ QString str;
112+ Q_FOREACH(const auto& touchPoint, event->touchPoints()) {
113+ str += QStringLiteral("[%1:%2 @ (%3, %4)], ").arg(touchPoint.id())
114+ .arg(touchState(touchPoint.state()))
115+ .arg(touchPoint.pos().x())
116+ .arg(touchPoint.pos().y());
117+ }
118+ return str;
119+}
120+
121+
122+} // namespace {
123+#else // TOUCHGESTUREAREA_DEBUG
124+#define tgaDebug(params) ((void)0)
125+#endif // TOUCHGESTUREAREA_DEBUG
126+
127+TouchGestureArea::TouchGestureArea(QQuickItem* parent)
128+ : QQuickItem(parent)
129+ , m_status(WaitingForTouch)
130+ , m_recognitionTimer(nullptr)
131+ , m_dragging(false)
132+ , m_minimumTouchPoints(1)
133+ , m_maximumTouchPoints(INT_MAX)
134+ , m_recognitionPeriod(50)
135+ , m_releaseRejectPeriod(100)
136+{
137+ setRecognitionTimer(new UbuntuGestures::Timer(this));
138+ m_recognitionTimer->setInterval(m_recognitionPeriod);
139+ m_recognitionTimer->setSingleShot(true);
140+}
141+
142+TouchGestureArea::~TouchGestureArea()
143+{
144+ clearTouchLists();
145+ qDeleteAll(m_liveTouchPoints);
146+ m_liveTouchPoints.clear();
147+ qDeleteAll(m_cachedTouchPoints);
148+ m_cachedTouchPoints.clear();
149+}
150+
151+bool TouchGestureArea::event(QEvent *event)
152+{
153+ // Process unowned touch events (handles update/release for incomplete gestures)
154+ if (event->type() == TouchOwnershipEvent::touchOwnershipEventType()) {
155+ touchOwnershipEvent(static_cast<TouchOwnershipEvent *>(event));
156+ return true;
157+ } else if (event->type() == UnownedTouchEvent::unownedTouchEventType()) {
158+ unownedTouchEvent(static_cast<UnownedTouchEvent *>(event)->touchEvent());
159+ return true;
160+ }
161+
162+ return QQuickItem::event(event);
163+}
164+
165+void TouchGestureArea::touchOwnershipEvent(TouchOwnershipEvent *event)
166+{
167+ int touchId = event->touchId();
168+ tgaDebug("touchOwnershipEvent - id:" << touchId << ", gained:" << event->gained());
169+
170+ if (event->gained()) {
171+ grabTouchPoints(QVector<int>() << touchId);
172+ m_candidateTouches.remove(touchId);
173+ TouchRegistry::instance()->addTouchWatcher(touchId, this);
174+ m_watchedTouches.insert(touchId);
175+
176+ if (m_watchedTouches.count() >= m_minimumTouchPoints) {
177+ setInternalStatus(InternalStatus::Recognized);
178+ }
179+ } else {
180+ rejectGesture();
181+ }
182+}
183+
184+void TouchGestureArea::touchEvent(QTouchEvent *event)
185+{
186+ if (!isEnabled() || !isVisible()) {
187+ tgaDebug(QString("NOT ENABLED touchEvent(%1) %2").arg(statusToString(m_status)).arg(touchEventString(event)));
188+ QQuickItem::touchEvent(event);
189+ return;
190+ }
191+
192+ tgaDebug(QString("touchEvent(%1) %2").arg(statusToString(m_status)).arg(touchEventString(event)));
193+
194+ switch (m_status) {
195+ case InternalStatus::WaitingForTouch:
196+ touchEvent_waitingForTouch(event);
197+ break;
198+ case InternalStatus::WaitingForMoreTouches:
199+ touchEvent_waitingForMoreTouches(event);
200+ break;
201+ case InternalStatus::WaitingForOwnership:
202+ touchEvent_waitingForOwnership(event);
203+ break;
204+ case InternalStatus::Recognized:
205+ case InternalStatus::WaitingForRejection:
206+ touchEvent_recognized(event);
207+ break;
208+ case InternalStatus::Rejected:
209+ touchEvent_rejected(event);
210+ break;
211+ default: // Recognized:
212+ break;
213+ }
214+
215+ updateTouchPoints(event);
216+}
217+
218+void TouchGestureArea::touchEvent_waitingForTouch(QTouchEvent *event)
219+{
220+ Q_FOREACH(const QTouchEvent::TouchPoint& touchPoint, event->touchPoints()) {
221+ Qt::TouchPointState touchPointState = touchPoint.state();
222+ int touchId = touchPoint.id();
223+
224+ if (touchPointState == Qt::TouchPointPressed) {
225+ if (!m_candidateTouches.contains(touchId)) {
226+ TouchRegistry::instance()->addCandidateOwnerForTouch(touchId, this);
227+ m_candidateTouches.insert(touchId);
228+ }
229+ }
230+ }
231+ event->ignore();
232+
233+ if (m_candidateTouches.count() > m_maximumTouchPoints) {
234+ rejectGesture();
235+ } else if (m_candidateTouches.count() >= m_minimumTouchPoints) {
236+ setInternalStatus(InternalStatus::WaitingForOwnership);
237+
238+ QSet<int> tmpCandidates(m_candidateTouches);
239+ Q_FOREACH(int candidateTouchId, tmpCandidates) {
240+ TouchRegistry::instance()->requestTouchOwnership(candidateTouchId, this);
241+ }
242+ // We accept the gesture; so don't pass to lower items
243+ event->accept();
244+ } else if (m_candidateTouches.count() > 0) {
245+ setInternalStatus(InternalStatus::WaitingForMoreTouches);
246+ }
247+}
248+
249+void TouchGestureArea::touchEvent_waitingForMoreTouches(QTouchEvent *event)
250+{
251+ Q_FOREACH(const QTouchEvent::TouchPoint& touchPoint, event->touchPoints()) {
252+ Qt::TouchPointState touchPointState = touchPoint.state();
253+ int touchId = touchPoint.id();
254+
255+ if (touchPointState == Qt::TouchPointPressed) {
256+ if (!m_candidateTouches.contains(touchId)) {
257+ TouchRegistry::instance()->addCandidateOwnerForTouch(touchId, this);
258+ m_candidateTouches.insert(touchId);
259+ }
260+ }
261+ }
262+ event->ignore();
263+
264+ if (m_candidateTouches.count() > m_maximumTouchPoints) {
265+ rejectGesture();
266+ } else if (m_candidateTouches.count() >= m_minimumTouchPoints) {
267+ setInternalStatus(InternalStatus::WaitingForOwnership);
268+
269+ QSet<int> tmpCandidates(m_candidateTouches);
270+ Q_FOREACH(int candidateTouchId, tmpCandidates) {
271+ TouchRegistry::instance()->requestTouchOwnership(candidateTouchId, this);
272+ }
273+ // We accept the gesture; so don't pass to lower items
274+ event->accept();
275+ }
276+}
277+
278+void TouchGestureArea::touchEvent_waitingForOwnership(QTouchEvent *event)
279+{
280+ Q_FOREACH(const QTouchEvent::TouchPoint& touchPoint, event->touchPoints()) {
281+ Qt::TouchPointState touchPointState = touchPoint.state();
282+ int touchId = touchPoint.id();
283+
284+ if (touchPointState == Qt::TouchPointPressed) {
285+ if (!m_watchedTouches.contains(touchId)) {
286+ TouchRegistry::instance()->addTouchWatcher(touchId, this);
287+ m_watchedTouches.insert(touchId);
288+ }
289+ }
290+ }
291+}
292+
293+void TouchGestureArea::touchEvent_recognized(QTouchEvent *event)
294+{
295+ Q_FOREACH(const QTouchEvent::TouchPoint& touchPoint, event->touchPoints()) {
296+ Qt::TouchPointState touchPointState = touchPoint.state();
297+ int touchId = touchPoint.id();
298+
299+ if (touchPointState == Qt::TouchPointPressed) {
300+ if (!m_watchedTouches.contains(touchId)) {
301+ TouchRegistry::instance()->addTouchWatcher(touchId, this);
302+ m_watchedTouches.insert(touchId);
303+ }
304+ }
305+ }
306+
307+ if (m_watchedTouches.count() > m_maximumTouchPoints) {
308+ rejectGesture();
309+ } else if (m_watchedTouches.count() >= m_minimumTouchPoints &&
310+ m_status==InternalStatus::WaitingForRejection) {
311+ setInternalStatus(InternalStatus::Recognized);
312+ }
313+}
314+
315+void TouchGestureArea::touchEvent_rejected(QTouchEvent *event)
316+{
317+ Q_FOREACH(const QTouchEvent::TouchPoint& touchPoint, event->touchPoints()) {
318+ Qt::TouchPointState touchPointState = touchPoint.state();
319+ int touchId = touchPoint.id();
320+
321+ if (touchPointState == Qt::TouchPointPressed) {
322+ if (!m_watchedTouches.contains(touchId)) {
323+ TouchRegistry::instance()->addTouchWatcher(touchId, this);
324+ m_watchedTouches.insert(touchId);
325+ }
326+ }
327+ }
328+}
329+
330+void TouchGestureArea::unownedTouchEvent(QTouchEvent *unownedTouchEvent)
331+{
332+ tgaDebug(QString("unownedTouchEvent(%1) %2").arg(statusToString(m_status)).arg(touchEventString(unownedTouchEvent)));
333+
334+ // Only monitor unowned touch events for presses/releases
335+ if ((unownedTouchEvent->touchPointStates() & (Qt::TouchPointPressed|Qt::TouchPointReleased)) == 0) {
336+ return;
337+ }
338+
339+ switch (m_status) {
340+ case InternalStatus::WaitingForTouch:
341+ break;
342+ case InternalStatus::WaitingForMoreTouches:
343+ unownedTouchEvent_waitingForMoreTouches(unownedTouchEvent);
344+ // do nothing
345+ break;
346+ case InternalStatus::WaitingForOwnership:
347+ unownedTouchEvent_waitingForOwnership(unownedTouchEvent);
348+ break;
349+ case InternalStatus::Recognized:
350+ case InternalStatus::WaitingForRejection:
351+ unownedTouchEvent_recognised(unownedTouchEvent);
352+ break;
353+ case InternalStatus::Rejected:
354+ unownedTouchEvent_rejected(unownedTouchEvent);
355+ break;
356+ default:
357+ break;
358+ }
359+
360+ updateTouchPoints(unownedTouchEvent);
361+}
362+
363+void TouchGestureArea::unownedTouchEvent_waitingForMoreTouches(QTouchEvent *event)
364+{
365+ Q_FOREACH(const QTouchEvent::TouchPoint& touchPoint, event->touchPoints()) {
366+ Qt::TouchPointState touchPointState = touchPoint.state();
367+ int touchId = touchPoint.id();
368+
369+ if (touchPointState == Qt::TouchPointReleased) {
370+ if (m_candidateTouches.contains(touchId)) {
371+ TouchRegistry::instance()->removeCandidateOwnerForTouch(touchId, this);
372+ m_candidateTouches.remove(touchId);
373+ }
374+ }
375+ }
376+
377+ if (m_candidateTouches.count() == 0) {
378+ setInternalStatus(InternalStatus::WaitingForTouch);
379+ }
380+}
381+
382+void TouchGestureArea::unownedTouchEvent_waitingForOwnership(QTouchEvent *event)
383+{
384+ Q_FOREACH(const QTouchEvent::TouchPoint& touchPoint, event->touchPoints()) {
385+ Qt::TouchPointState touchPointState = touchPoint.state();
386+ int touchId = touchPoint.id();
387+
388+ if (touchPointState == Qt::TouchPointReleased) {
389+ if (m_candidateTouches.contains(touchId)) {
390+ TouchRegistry::instance()->removeCandidateOwnerForTouch(touchId, this);
391+ m_candidateTouches.remove(touchId);
392+ }
393+ if (m_watchedTouches.contains(touchId)) {
394+ m_watchedTouches.remove(touchId);
395+ }
396+ }
397+ }
398+
399+ if (m_candidateTouches.count() + m_watchedTouches.count() == 0) {
400+ setInternalStatus(InternalStatus::WaitingForTouch);
401+ }
402+}
403+
404+void TouchGestureArea::unownedTouchEvent_recognised(QTouchEvent *event)
405+{
406+ Q_FOREACH(const QTouchEvent::TouchPoint& touchPoint, event->touchPoints()) {
407+ Qt::TouchPointState touchPointState = touchPoint.state();
408+ int touchId = touchPoint.id();
409+
410+ if (touchPointState == Qt::TouchPointReleased) {
411+ if (m_watchedTouches.contains(touchId)) {
412+ m_watchedTouches.remove(touchId);
413+ }
414+ }
415+ }
416+
417+ if (m_watchedTouches.count() < m_minimumTouchPoints && m_status==InternalStatus::Recognized) {
418+ setInternalStatus(InternalStatus::WaitingForRejection);
419+ }
420+}
421+
422+void TouchGestureArea::unownedTouchEvent_rejected(QTouchEvent *event)
423+{
424+ Q_FOREACH(const QTouchEvent::TouchPoint& touchPoint, event->touchPoints()) {
425+ Qt::TouchPointState touchPointState = touchPoint.state();
426+ int touchId = touchPoint.id();
427+
428+ if (touchPointState == Qt::TouchPointPressed) {
429+ if (!m_watchedTouches.contains(touchId)) {
430+ TouchRegistry::instance()->addTouchWatcher(touchId, this);
431+ m_watchedTouches.insert(touchId);
432+ }
433+ }
434+ if (touchPointState == Qt::TouchPointReleased) {
435+ if (m_watchedTouches.contains(touchId)) {
436+ m_watchedTouches.remove(touchId);
437+ }
438+ }
439+ }
440+
441+ if (m_watchedTouches.count() == 0) {
442+ setInternalStatus(InternalStatus::WaitingForTouch);
443+ }
444+}
445+
446+void TouchGestureArea::updateTouchPoints(QTouchEvent *touchEvent)
447+{
448+ bool added = false;
449+ bool ended = false;
450+ bool moved = false;
451+
452+ const int dragThreshold = qApp->styleHints()->startDragDistance();
453+ const int dragVelocity = qApp->styleHints()->startDragVelocity();
454+
455+ clearTouchLists();
456+ bool updateable = m_status != InternalStatus::WaitingForRejection;
457+
458+ Q_FOREACH(const QTouchEvent::TouchPoint& touchPoint, touchEvent->touchPoints()) {
459+ Qt::TouchPointState touchPointState = touchPoint.state();
460+ int touchId = touchPoint.id();
461+
462+ if (touchPointState & Qt::TouchPointReleased) {
463+ GestureTouchPoint* gtp = m_liveTouchPoints.value(touchId);
464+ if (!gtp) continue;
465+
466+ gtp->setPos(touchPoint.pos());
467+ gtp->setPressed(false);
468+ m_releasedTouchPoints.append(gtp);
469+ m_liveTouchPoints.remove(touchId);
470+
471+ if (updateable) {
472+ if (m_cachedTouchPoints.contains(touchId)) {
473+ GestureTouchPoint* cachedPoint = m_cachedTouchPoints.take(touchId);
474+ cachedPoint->deleteLater();
475+ }
476+ }
477+ ended = true;
478+ } else {
479+ GestureTouchPoint* gtp = m_liveTouchPoints.value(touchPoint.id(), nullptr);
480+ if (!gtp) {
481+ gtp = addTouchPoint(&touchPoint);
482+ m_pressedTouchPoints.append(gtp);
483+
484+ if (updateable) {
485+ if (m_cachedTouchPoints.contains(touchId)) {
486+ m_cachedTouchPoints[touchId]->setPos(touchPoint.pos());
487+ } else {
488+ m_cachedTouchPoints[touchId] = new GestureTouchPoint(*gtp);
489+ }
490+ }
491+ added = true;
492+ } else if (touchPointState & Qt::TouchPointMoved) {
493+ gtp->setPos(touchPoint.pos());
494+ m_movedTouchPoints.append(gtp);
495+ moved = true;
496+
497+ const QPointF &currentPos = touchPoint.scenePos();
498+ const QPointF &startPos = touchPoint.startScenePos();
499+
500+ bool overDragThreshold = false;
501+ bool supportsVelocity = (touchEvent->device()->capabilities() & QTouchDevice::Velocity) && dragVelocity;
502+ overDragThreshold |= qAbs(currentPos.x() - startPos.x()) > dragThreshold ||
503+ qAbs(currentPos.y() - startPos.y()) > dragThreshold;
504+ if (supportsVelocity) {
505+ QVector2D velocityVec = touchPoint.velocity();
506+ overDragThreshold |= qAbs(velocityVec.x()) > dragVelocity;
507+ overDragThreshold |= qAbs(velocityVec.y()) > dragVelocity;
508+ }
509+
510+ if (overDragThreshold) {
511+ gtp->setDragging(true);
512+ }
513+
514+ if (updateable) {
515+ if (m_cachedTouchPoints.contains(touchId)) {
516+ m_cachedTouchPoints[touchId]->setPos(touchPoint.pos());
517+ if (overDragThreshold) {
518+ m_cachedTouchPoints[touchId]->setDragging(true);
519+ }
520+ }
521+ }
522+ }
523+ }
524+ }
525+
526+ if (updateable) {
527+ if (!dragging() && m_status == InternalStatus::Recognized) {
528+ bool allWantDrag = !m_liveTouchPoints.isEmpty();
529+ Q_FOREACH(auto point, m_liveTouchPoints) {
530+ allWantDrag &= point->dragging();
531+ }
532+ // only dragging if all points are dragging.
533+ if (allWantDrag) {
534+ setDragging(true);
535+ }
536+ }
537+
538+ if (ended) {
539+ if (m_liveTouchPoints.isEmpty()) {
540+ if (!dragging()) Q_EMIT clicked();
541+ setDragging(false);
542+ }
543+ tgaDebug("Released " << touchesString(m_releasedTouchPoints));
544+ Q_EMIT released(m_releasedTouchPoints);
545+ }
546+ if (added) {
547+ tgaDebug("Pressed " << touchesString(m_pressedTouchPoints));
548+ Q_EMIT pressed(m_pressedTouchPoints);
549+ }
550+ if (moved) {
551+ tgaDebug("Updated " << touchesString(m_movedTouchPoints));
552+ Q_EMIT updated(m_movedTouchPoints);
553+ }
554+ if (added || ended || moved) {
555+ Q_EMIT touchPointsUpdated();
556+ }
557+ }
558+}
559+
560+void TouchGestureArea::clearTouchLists()
561+{
562+ Q_FOREACH (QObject *gtp, m_releasedTouchPoints) {
563+ delete gtp;
564+ }
565+ m_releasedTouchPoints.clear();
566+ m_pressedTouchPoints.clear();
567+ m_movedTouchPoints.clear();
568+}
569+
570+void TouchGestureArea::setInternalStatus(uint newStatus)
571+{
572+ if (newStatus == m_status)
573+ return;
574+
575+ uint oldStatus = m_status;
576+
577+ m_status = newStatus;
578+ Q_EMIT statusChanged(status());
579+
580+ if (oldStatus == InternalStatus::WaitingForMoreTouches || oldStatus == InternalStatus::WaitingForRejection) {
581+ m_recognitionTimer->stop();
582+ }
583+
584+ tgaDebug(statusToString(oldStatus) << " -> " << statusToString(newStatus));
585+
586+ switch (newStatus) {
587+ case InternalStatus::WaitingForTouch:
588+ resyncCachedTouchPoints();
589+ break;
590+ case InternalStatus::WaitingForMoreTouches:
591+ m_recognitionTimer->start(m_recognitionPeriod);
592+ break;
593+ case InternalStatus::Recognized:
594+ resyncCachedTouchPoints();
595+ break;
596+ case InternalStatus::WaitingForRejection:
597+ m_recognitionTimer->start(m_releaseRejectPeriod);
598+ break;
599+ case InternalStatus::Rejected:
600+ resyncCachedTouchPoints();
601+ break;
602+ default:
603+ // no-op
604+ break;
605+ }
606+}
607+
608+void TouchGestureArea::setRecognitionTimer(UbuntuGestures::AbstractTimer *timer)
609+{
610+ int interval = 0;
611+ bool timerWasRunning = false;
612+ bool wasSingleShot = false;
613+
614+ // can be null when called from the constructor
615+ if (m_recognitionTimer) {
616+ interval = m_recognitionTimer->interval();
617+ timerWasRunning = m_recognitionTimer->isRunning();
618+ if (m_recognitionTimer->parent() == this) {
619+ delete m_recognitionTimer;
620+ }
621+ }
622+
623+ m_recognitionTimer = timer;
624+ timer->setInterval(interval);
625+ timer->setSingleShot(wasSingleShot);
626+ connect(timer, SIGNAL(timeout()),
627+ this, SLOT(rejectGesture()));
628+ if (timerWasRunning) {
629+ m_recognitionTimer->start();
630+ }
631+}
632+
633+int TouchGestureArea::status() const
634+{
635+ return internalStatusToGestureStatus(m_status);
636+}
637+
638+bool TouchGestureArea::dragging() const
639+{
640+ return m_dragging;
641+}
642+
643+QQmlListProperty<GestureTouchPoint> TouchGestureArea::touchPoints()
644+{
645+ return QQmlListProperty<GestureTouchPoint>(this,
646+ 0,
647+ nullptr,
648+ TouchGestureArea::touchPoint_count,
649+ TouchGestureArea::touchPoint_at,
650+ 0);
651+}
652+
653+int TouchGestureArea::minimumTouchPoints() const
654+{
655+ return m_minimumTouchPoints;
656+}
657+
658+void TouchGestureArea::setMinimumTouchPoints(int value)
659+{
660+ if (m_minimumTouchPoints != value) {
661+ m_minimumTouchPoints = value;
662+ Q_EMIT minimumTouchPointsChanged(value);
663+ }
664+}
665+
666+int TouchGestureArea::maximumTouchPoints() const
667+{
668+ return m_maximumTouchPoints;
669+}
670+
671+void TouchGestureArea::setMaximumTouchPoints(int value)
672+{
673+ if (m_maximumTouchPoints != value) {
674+ m_maximumTouchPoints = value;
675+ Q_EMIT maximumTouchPointsChanged(value);
676+ }
677+}
678+
679+int TouchGestureArea::recognitionPeriod() const
680+{
681+ return m_recognitionPeriod;
682+}
683+
684+void TouchGestureArea::setRecognitionPeriod(int value)
685+{
686+ if (value != m_recognitionPeriod) {
687+ m_recognitionPeriod = value;
688+ Q_EMIT recognitionPeriodChanged(value);
689+ }
690+}
691+
692+int TouchGestureArea::releaseRejectPeriod() const
693+{
694+ return m_releaseRejectPeriod;
695+}
696+
697+void TouchGestureArea::setReleaseRejectPeriod(int value)
698+{
699+ if (value != m_releaseRejectPeriod) {
700+ m_releaseRejectPeriod = value;
701+ Q_EMIT releaseRejectPeriodChanged(value);
702+ }
703+}
704+
705+void TouchGestureArea::rejectGesture()
706+{
707+ tgaDebug("rejectGesture");
708+ ungrabTouchPoints();
709+
710+ Q_FOREACH(int touchId, m_candidateTouches) {
711+ TouchRegistry::instance()->removeCandidateOwnerForTouch(touchId, this);
712+ }
713+
714+ // Monitor the candidates
715+ Q_FOREACH(int touchId, m_candidateTouches) {
716+ TouchRegistry::instance()->addTouchWatcher(touchId, this);
717+ m_watchedTouches.insert(touchId);
718+ }
719+ m_candidateTouches.clear();
720+
721+ if (m_watchedTouches.count() == 0) {
722+ setInternalStatus(InternalStatus::WaitingForTouch);
723+ } else {
724+ setInternalStatus(InternalStatus::Rejected);
725+ }
726+}
727+
728+void TouchGestureArea::resyncCachedTouchPoints()
729+{
730+ clearTouchLists();
731+
732+ bool added = false;
733+ bool ended = false;
734+ bool moved = false;
735+ bool wantsDrag = false;
736+
737+ // list of deletes
738+ QMutableHashIterator<int, GestureTouchPoint*> removeIter(m_cachedTouchPoints);
739+ while(removeIter.hasNext()) {
740+ removeIter.next();
741+ if (!m_liveTouchPoints.contains(removeIter.key())) {
742+ m_releasedTouchPoints.append(removeIter.value());
743+ removeIter.remove();
744+ ended = true;
745+ }
746+ }
747+
748+ // list of adds/moves
749+ Q_FOREACH(GestureTouchPoint* touchPoint, m_liveTouchPoints) {
750+ if (m_cachedTouchPoints.contains(touchPoint->id())) {
751+ GestureTouchPoint* cachedPoint = m_cachedTouchPoints[touchPoint->id()];
752+
753+ if (*cachedPoint != *touchPoint) {
754+ *cachedPoint = *touchPoint;
755+ m_movedTouchPoints.append(touchPoint);
756+ moved = true;
757+ }
758+ } else {
759+ m_cachedTouchPoints.insert(touchPoint->id(), new GestureTouchPoint(*touchPoint));
760+ m_pressedTouchPoints.append(touchPoint);
761+ added = true;
762+ }
763+ }
764+
765+ if (wantsDrag && !dragging()) {
766+ setDragging(true);
767+ }
768+
769+ if (ended) {
770+ if (m_cachedTouchPoints.isEmpty()) {
771+ if (!dragging()) Q_EMIT clicked();
772+ setDragging(false);
773+ }
774+ tgaDebug("Cached Release " << touchesString(m_releasedTouchPoints));
775+ Q_EMIT released(m_releasedTouchPoints);
776+ }
777+ if (added) {
778+ tgaDebug("Cached Press " << touchesString(m_pressedTouchPoints));
779+ Q_EMIT pressed(m_pressedTouchPoints);
780+ }
781+ if (moved) {
782+ tgaDebug("Cached Update " << touchesString(m_movedTouchPoints));
783+ Q_EMIT updated(m_movedTouchPoints);
784+ }
785+ if (added || ended || moved) Q_EMIT touchPointsUpdated();
786+}
787+
788+int TouchGestureArea::touchPoint_count(QQmlListProperty<GestureTouchPoint> *list)
789+{
790+ TouchGestureArea *q = static_cast<TouchGestureArea*>(list->object);
791+ return q->m_cachedTouchPoints.count();
792+}
793+
794+GestureTouchPoint *TouchGestureArea::touchPoint_at(QQmlListProperty<GestureTouchPoint> *list, int index)
795+{
796+ TouchGestureArea *q = static_cast<TouchGestureArea*>(list->object);
797+ return (q->m_cachedTouchPoints.begin()+index).value();
798+}
799+
800+GestureTouchPoint* TouchGestureArea::addTouchPoint(QTouchEvent::TouchPoint const* tp)
801+{
802+ GestureTouchPoint* gtp = new GestureTouchPoint();
803+ gtp->setId(tp->id());
804+ gtp->setPressed(true);
805+ gtp->setPos(tp->pos());
806+ m_liveTouchPoints.insert(tp->id(), gtp);
807+ return gtp;
808+}
809+
810+void TouchGestureArea::itemChange(ItemChange change, const ItemChangeData &value)
811+{
812+ if (change == QQuickItem::ItemSceneChange) {
813+ if (value.window != nullptr) {
814+ value.window->installEventFilter(TouchRegistry::instance());
815+ }
816+ }
817+}
818+
819+void TouchGestureArea::setDragging(bool dragging)
820+{
821+ if (m_dragging == dragging)
822+ return;
823+
824+ tgaDebug("setDragging " << dragging);
825+
826+ m_dragging = dragging;
827+ Q_EMIT draggingChanged(m_dragging);
828+}
829+
830+void GestureTouchPoint::setId(int id)
831+{
832+ if (m_id == id)
833+ return;
834+ m_id = id;
835+ Q_EMIT idChanged();
836+}
837+
838+void GestureTouchPoint::setPressed(bool pressed)
839+{
840+ if (m_pressed == pressed)
841+ return;
842+ m_pressed = pressed;
843+ Q_EMIT pressedChanged();
844+}
845+
846+void GestureTouchPoint::setX(qreal x)
847+{
848+ if (m_x == x)
849+ return;
850+ m_x = x;
851+ Q_EMIT xChanged();
852+}
853+
854+void GestureTouchPoint::setY(qreal y)
855+{
856+ if (m_y == y)
857+ return;
858+ m_y = y;
859+ Q_EMIT yChanged();
860+}
861+
862+void GestureTouchPoint::setDragging(bool dragging)
863+{
864+ if (m_dragging == dragging)
865+ return;
866+
867+ m_dragging = dragging;
868+ Q_EMIT draggingChanged();
869+}
870+
871+void GestureTouchPoint::setPos(const QPointF &pos)
872+{
873+ setX(pos.x());
874+ setY(pos.y());
875+}
plugins/Ubuntu/Gestures/TouchGestureArea.hnew file+229
@@ -0,0 +1,229 @@
1+/*
2+ * Copyright (C) 2016 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 TOUCHGESTUREAREA_H
18+#define TOUCHGESTUREAREA_H
19+
20+#include "UbuntuGesturesQmlGlobal.h"
21+
22+#include <QQuickItem>
23+
24+// lib UbuntuGestures
25+#include <Timer.h>
26+
27+class TouchOwnershipEvent;
28+class UnownedTouchEvent;
29+
30+class GestureTouchPoint : public QObject
31+{
32+ Q_OBJECT
33+ Q_PROPERTY(int id READ id NOTIFY idChanged)
34+ Q_PROPERTY(bool pressed READ pressed NOTIFY pressedChanged)
35+ Q_PROPERTY(qreal x READ x NOTIFY xChanged)
36+ Q_PROPERTY(qreal y READ y NOTIFY yChanged)
37+ Q_PROPERTY(bool dragging READ dragging NOTIFY draggingChanged)
38+public:
39+ GestureTouchPoint()
40+ : m_id(-1)
41+ , m_pressed(false)
42+ , m_x(0)
43+ , m_y(0)
44+ , m_dragging(false)
45+ {
46+ }
47+
48+ GestureTouchPoint(const GestureTouchPoint& other)
49+ : QObject(nullptr)
50+ {
51+ operator=(other);
52+ }
53+
54+ int id() const { return m_id; }
55+ void setId(int id);
56+
57+ bool pressed() const { return m_pressed; }
58+ void setPressed(bool pressed);
59+
60+ qreal x() const { return m_x; }
61+ void setX(qreal x);
62+
63+ qreal y() const { return m_y; }
64+ void setY(qreal y);
65+
66+ bool dragging() const { return m_dragging; }
67+ void setDragging(bool dragging);
68+
69+ GestureTouchPoint& operator=(const GestureTouchPoint& rhs) {
70+ if (&rhs == this) return *this;
71+ m_id = rhs.m_id;
72+ m_pressed = rhs.m_pressed;
73+ m_x = rhs.m_x;
74+ m_y = rhs.m_y;
75+ m_dragging = rhs.m_dragging;
76+ return *this;
77+ }
78+
79+ bool operator=(const GestureTouchPoint& rhs) const {
80+ if (&rhs == this) return true;
81+ return m_id == rhs.m_id &&
82+ m_pressed == rhs.m_pressed &&
83+ m_x == rhs.m_x &&
84+ m_y == rhs.m_y &&
85+ m_dragging == rhs.m_dragging;
86+ }
87+ bool operator!=(const GestureTouchPoint& rhs) const { return !operator=(rhs); }
88+
89+ void setPos(const QPointF &pos);
90+
91+Q_SIGNALS:
92+ void idChanged();
93+ void pressedChanged();
94+ void xChanged();
95+ void yChanged();
96+ void draggingChanged();
97+
98+private:
99+ int m_id;
100+ bool m_pressed;
101+ qreal m_x;
102+ qreal m_y;
103+ bool m_dragging;
104+};
105+
106+/*
107+ An area that detects multi-finger gestures.
108+
109+ We can use this to detect gestures contstrained by a minimim and/or maximum number of touch points.
110+ This components uses the touch registry to apply for ownership of touch points.
111+ This way we can use the component in conjuntion with the directional drag area to compete for ownwership
112+ or gestures; unlike the MultiPointTouchArea.
113+ */
114+class UBUNTUGESTURESQML_EXPORT TouchGestureArea : public QQuickItem
115+{
116+ Q_OBJECT
117+ Q_ENUMS(Status)
118+
119+ Q_PROPERTY(int status READ status NOTIFY statusChanged)
120+ Q_PROPERTY(bool dragging READ dragging NOTIFY draggingChanged)
121+ Q_PROPERTY(QQmlListProperty<GestureTouchPoint> touchPoints READ touchPoints NOTIFY touchPointsUpdated)
122+
123+ Q_PROPERTY(int minimumTouchPoints READ minimumTouchPoints WRITE setMinimumTouchPoints NOTIFY minimumTouchPointsChanged)
124+ Q_PROPERTY(int maximumTouchPoints READ maximumTouchPoints WRITE setMaximumTouchPoints NOTIFY maximumTouchPointsChanged)
125+
126+ // Time(ms) the component will wait for after receiving an initial touch to recognise a gesutre before rejecting it.
127+ Q_PROPERTY(int recognitionPeriod READ recognitionPeriod WRITE setRecognitionPeriod NOTIFY recognitionPeriodChanged)
128+ // Time(ms) the component will allow a recognised gesture to intermitently release a touch point before rejecting the gesture.
129+ // This is so we will not immediately reject a gesture if there are fleeting touch point releases while dragging.
130+ Q_PROPERTY(int releaseRejectPeriod READ releaseRejectPeriod WRITE setReleaseRejectPeriod NOTIFY releaseRejectPeriodChanged)
131+
132+public:
133+ // Describes the state of the touch gesture area.
134+ enum Status {
135+ WaitingForTouch,
136+ Undecided,
137+ Recognized,
138+ Rejected
139+ };
140+ TouchGestureArea(QQuickItem* parent = NULL);
141+ ~TouchGestureArea();
142+
143+ bool event(QEvent *e) override;
144+
145+ void setRecognitionTimer(UbuntuGestures::AbstractTimer *timer);
146+
147+ int status() const;
148+ bool dragging() const;
149+ QQmlListProperty<GestureTouchPoint> touchPoints();
150+
151+ int minimumTouchPoints() const;
152+ void setMinimumTouchPoints(int value);
153+
154+ int maximumTouchPoints() const;
155+ void setMaximumTouchPoints(int value);
156+
157+ int recognitionPeriod() const;
158+ void setRecognitionPeriod(int value);
159+
160+ int releaseRejectPeriod() const;
161+ void setReleaseRejectPeriod(int value);
162+
163+Q_SIGNALS:
164+ void statusChanged(int status);
165+
166+ void touchPointsUpdated();
167+ void draggingChanged(bool dragging);
168+ void minimumTouchPointsChanged(bool value);
169+ void maximumTouchPointsChanged(bool value);
170+ void recognitionPeriodChanged(bool value);
171+ void releaseRejectPeriodChanged(bool value);
172+
173+ void pressed(const QList<QObject*>& points);
174+ void released(const QList<QObject*>& points);
175+ void updated(const QList<QObject*>& points);
176+ void clicked();
177+
178+protected:
179+ void itemChange(ItemChange change, const ItemChangeData &value);
180+
181+private Q_SLOTS:
182+ void rejectGesture();
183+
184+private:
185+ void touchEvent(QTouchEvent *event) override;
186+ void touchEvent_waitingForTouch(QTouchEvent *event);
187+ void touchEvent_waitingForMoreTouches(QTouchEvent *event);
188+ void touchEvent_waitingForOwnership(QTouchEvent *event);
189+ void touchEvent_recognized(QTouchEvent *event);
190+ void touchEvent_rejected(QTouchEvent *event);
191+
192+ void unownedTouchEvent(QTouchEvent *unownedTouchEvent);
193+ void unownedTouchEvent_waitingForMoreTouches(QTouchEvent *unownedTouchEvent);
194+ void unownedTouchEvent_waitingForOwnership(QTouchEvent *unownedTouchEvent);
195+ void unownedTouchEvent_recognised(QTouchEvent *unownedTouchEvent);
196+ void unownedTouchEvent_rejected(QTouchEvent *unownedTouchEvent);
197+
198+ void touchOwnershipEvent(TouchOwnershipEvent *event);
199+ void updateTouchPoints(QTouchEvent *event);
200+
201+ GestureTouchPoint* addTouchPoint(const QTouchEvent::TouchPoint *tp);
202+ void clearTouchLists();
203+ void setDragging(bool dragging);
204+ void setInternalStatus(uint status);
205+ void resyncCachedTouchPoints();
206+
207+ static int touchPoint_count(QQmlListProperty<GestureTouchPoint> *list);
208+ static GestureTouchPoint* touchPoint_at(QQmlListProperty<GestureTouchPoint> *list, int index);
209+
210+ uint m_status;
211+ QSet<int> m_candidateTouches;
212+ QSet<int> m_watchedTouches;
213+ UbuntuGestures::AbstractTimer *m_recognitionTimer;
214+
215+ bool m_dragging;
216+ QHash<int, GestureTouchPoint*> m_liveTouchPoints;
217+ QHash<int, GestureTouchPoint*> m_cachedTouchPoints;
218+ QList<QObject*> m_releasedTouchPoints;
219+ QList<QObject*> m_pressedTouchPoints;
220+ QList<QObject*> m_movedTouchPoints;
221+ int m_minimumTouchPoints;
222+ int m_maximumTouchPoints;
223+ int m_recognitionPeriod;
224+ int m_releaseRejectPeriod;
225+};
226+
227+QML_DECLARE_TYPE(GestureTouchPoint)
228+
229+#endif // TOUCHGESTUREAREA_H
plugins/Ubuntu/Gestures/plugin.cpp+3
@@ -21,6 +21,7 @@
21#include "FloatingFlickable.h"
21#include "FloatingFlickable.h"
22#include "PressedOutsideNotifier.h"
22#include "PressedOutsideNotifier.h"
23#include "TouchGate.h"
23#include "TouchGate.h"
24+#include "TouchGestureArea.h"
24
25
25#include <qqml.h>
26#include <qqml.h>
26
27
@@ -38,4 +39,6 @@ void UbuntuGesturesQmlPlugin::registerTypes(const char *uri)
38 qmlRegisterType<FloatingFlickable>(uri, 0, 1, "FloatingFlickable");
39 qmlRegisterType<FloatingFlickable>(uri, 0, 1, "FloatingFlickable");
39 qmlRegisterType<PressedOutsideNotifier>(uri, 0, 1, "PressedOutsideNotifier");
40 qmlRegisterType<PressedOutsideNotifier>(uri, 0, 1, "PressedOutsideNotifier");
40 qmlRegisterType<TouchGate>(uri, 0, 1, "TouchGate");
41 qmlRegisterType<TouchGate>(uri, 0, 1, "TouchGate");
42+ qmlRegisterType<TouchGestureArea>(uri, 0, 1, "TouchGestureArea");
43+ qmlRegisterUncreatableType<GestureTouchPoint>(uri, 0, 1, "GestureTouchPoint", "Cannot create GestureTouchPoints");
41}
44}
plugins/Unity/InputInfo/InputInfo.qmltypesnew file+71
@@ -0,0 +1,71 @@
1+import QtQuick.tooling 1.1
2+
3+// This file describes the plugin-supplied types contained in the library.
4+// It is used for QML tooling purposes only.
5+//
6+// This file was auto-generated by:
7+// 'qmlplugindump -notrelocatable Unity.InputInfo 0.1 plugins'
8+
9+Module {
10+ Component {
11+ name: "QDeclarativeInputDeviceModel"
12+ prototype: "QAbstractListModel"
13+ exports: ["Unity.InputInfo/InputDeviceModel 0.1"]
14+ exportMetaObjectRevisions: [0]
15+ Property { name: "deviceFilter"; type: "QInputDevice::InputType" }
16+ Property { name: "count"; type: "int"; isReadonly: true }
17+ Signal {
18+ name: "deviceAdded"
19+ Parameter { name: "devicePath"; type: "string" }
20+ }
21+ Signal {
22+ name: "deviceRemoved"
23+ Parameter { name: "devicePath"; type: "string" }
24+ }
25+ Signal {
26+ name: "deviceFilterChanged"
27+ Parameter { name: "filter"; type: "QInputDevice::InputType" }
28+ }
29+ Method { name: "updateDeviceList" }
30+ Method {
31+ name: "indexOf"
32+ type: "int"
33+ Parameter { name: "devicePath"; type: "string" }
34+ }
35+ Method {
36+ name: "get"
37+ type: "QInputDevice*"
38+ Parameter { name: "index"; type: "int" }
39+ }
40+ }
41+ Component {
42+ name: "QInputDevice"
43+ prototype: "QObject"
44+ exports: ["Unity.InputInfo/InputInfo 0.1"]
45+ exportMetaObjectRevisions: [0]
46+ Enum {
47+ name: "InputType"
48+ values: {
49+ "Unknown": 0,
50+ "Button": 1,
51+ "Mouse": 2,
52+ "TouchPad": 4,
53+ "TouchScreen": 8,
54+ "Keyboard": 16,
55+ "Switch": 32
56+ }
57+ }
58+ Enum {
59+ name: "InputTypeFlags"
60+ values: {
61+ "Unknown": 0,
62+ "Button": 1,
63+ "Mouse": 2,
64+ "TouchPad": 4,
65+ "TouchScreen": 8,
66+ "Keyboard": 16,
67+ "Switch": 32
68+ }
69+ }
70+ }
71+}
plugins/Unity/Platform/Platform.qmltypesnew file+20
@@ -0,0 +1,20 @@
1+import QtQuick.tooling 1.1
2+
3+// This file describes the plugin-supplied types contained in the library.
4+// It is used for QML tooling purposes only.
5+//
6+// This file was auto-generated by:
7+// 'qmlplugindump -notrelocatable Unity.Platform 1.0 plugins'
8+
9+Module {
10+ Component {
11+ name: "Platform"
12+ prototype: "QObject"
13+ exports: ["Unity.Platform/Platform 1.0"]
14+ isCreatable: false
15+ isSingleton: true
16+ exportMetaObjectRevisions: [0]
17+ Property { name: "chassis"; type: "string"; isReadonly: true }
18+ Property { name: "isPC"; type: "bool"; isReadonly: true }
19+ }
20+}
plugins/Utils/CMakeLists.txt+1
@@ -24,6 +24,7 @@ set(QMLPLUGIN_SRC
24 timezoneFormatter.cpp
24 timezoneFormatter.cpp
25 inputeventgenerator.cpp
25 inputeventgenerator.cpp
26 deviceconfigparser.cpp
26 deviceconfigparser.cpp
27+ globalfunctions.cpp
27 plugin.cpp
28 plugin.cpp
28 )
29 )
29
30
plugins/Utils/globalfunctions.cppnew file+56
@@ -0,0 +1,56 @@
1+/*
2+ * Copyright 2015 Canonical Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU Lesser 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 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 "globalfunctions.h"
18+
19+#pragma GCC diagnostic push
20+#pragma GCC diagnostic ignored "-pedantic"
21+#include <private/qquickitem_p.h>
22+#pragma GCC diagnostic pop
23+#include <QQmlEngine>
24+
25+GlobalFunctions::GlobalFunctions(QObject *parent)
26+ : QObject(parent)
27+{
28+}
29+
30+QQuickItem *GlobalFunctions::itemAt(QQuickItem* parent, int x, int y, QJSValue matcher)
31+{
32+ if (!parent) return nullptr;
33+ QList<QQuickItem *> children = QQuickItemPrivate::get(parent)->paintOrderChildItems();
34+
35+ for (int i = children.count() - 1; i >= 0; --i) {
36+ QQuickItem *child = children.at(i);
37+
38+ // Map coordinates to the child element's coordinate space
39+ QPointF point = parent->mapToItem(child, QPointF(x, y));
40+ if (child->isVisible() && point.x() >= 0
41+ && child->width() >= point.x()
42+ && point.y() >= 0
43+ && child->height() >= point.y()) {
44+ if (!matcher.isCallable()) return child;
45+
46+ QQmlEngine* engine = qmlEngine(child);
47+ if (!engine) return child;
48+
49+ QJSValue newObj = engine->newQObject(child);
50+ if (matcher.call(QJSValueList() << newObj).toBool()) {
51+ return child;
52+ }
53+ }
54+ }
55+ return nullptr;
56+}
plugins/Utils/globalfunctions.hnew file+42
@@ -0,0 +1,42 @@
1+/*
2+ * Copyright 2015 Canonical Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU Lesser 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 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 GLOBALFUNCTIONS_H
18+#define GLOBALFUNCTIONS_H
19+
20+#include <QObject>
21+#include <QJSValue>
22+class QQuickItem;
23+
24+/**
25+ * @brief The GlobalFunctions class
26+ *
27+ * This singleton class exposes utility functions to QML
28+ *
29+ */
30+class GlobalFunctions : public QObject
31+{
32+ Q_OBJECT
33+public:
34+ explicit GlobalFunctions(QObject *parent = 0);
35+
36+ static Q_INVOKABLE QQuickItem* itemAt(QQuickItem* parent,
37+ int x,
38+ int y,
39+ QJSValue matcher);
40+};
41+
42+#endif // GLOBALFUNCTIONS_H
plugins/Utils/plugin.cpp+9
@@ -38,6 +38,7 @@
38#include "applicationsfiltermodel.h"
38#include "applicationsfiltermodel.h"
39#include "inputeventgenerator.h"
39#include "inputeventgenerator.h"
40#include "deviceconfigparser.h"
40#include "deviceconfigparser.h"
41+#include "globalfunctions.h"
41
42
42static QObject *createWindowStateStorage(QQmlEngine *engine, QJSEngine *scriptEngine)
43static QObject *createWindowStateStorage(QQmlEngine *engine, QJSEngine *scriptEngine)
43{
44{
@@ -53,6 +54,13 @@ static QObject *createConstants(QQmlEngine *engine, QJSEngine *scriptEngine)
53 return new Constants();
54 return new Constants();
54}
55}
55
56
57+static QObject *createGlobalFunctions(QQmlEngine *engine, QJSEngine *scriptEngine)
58+{
59+ Q_UNUSED(engine)
60+ Q_UNUSED(scriptEngine)
61+ return new GlobalFunctions();
62+}
63+
56void UtilsPlugin::registerTypes(const char *uri)
64void UtilsPlugin::registerTypes(const char *uri)
57{
65{
58 Q_ASSERT(uri == QLatin1String("Utils"));
66 Q_ASSERT(uri == QLatin1String("Utils"));
@@ -72,6 +80,7 @@ void UtilsPlugin::registerTypes(const char *uri)
72 qmlRegisterType<ApplicationsFilterModel>(uri, 0, 1, "ApplicationsFilterModel");
80 qmlRegisterType<ApplicationsFilterModel>(uri, 0, 1, "ApplicationsFilterModel");
73 qmlRegisterType<InputEventGenerator>(uri, 0, 1, "InputEventGenerator");
81 qmlRegisterType<InputEventGenerator>(uri, 0, 1, "InputEventGenerator");
74 qmlRegisterType<DeviceConfigParser>(uri, 0, 1, "DeviceConfigParser");
82 qmlRegisterType<DeviceConfigParser>(uri, 0, 1, "DeviceConfigParser");
83+ qmlRegisterSingletonType<GlobalFunctions>(uri, 0, 1, "Functions", createGlobalFunctions);
75}
84}
76
85
77void UtilsPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
86void UtilsPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
plugins/Utils/windowstatestorage.cpp+36−4
@@ -23,9 +23,14 @@
23#include <QSqlError>
23#include <QSqlError>
24#include <QSqlResult>
24#include <QSqlResult>
25#include <QRect>
25#include <QRect>
26+#include <unity/shell/application/ApplicationInfoInterface.h>
26
27
27QMutex WindowStateStorage::s_mutex;
28QMutex WindowStateStorage::s_mutex;
28
29
30+inline QString sanitiseString(QString string) {
31+ return string.remove("\"").remove("'").remove("\\");
32+}
33+
29WindowStateStorage::WindowStateStorage(QObject *parent):
34WindowStateStorage::WindowStateStorage(QObject *parent):
30 QObject(parent)
35 QObject(parent)
31{
36{
@@ -50,7 +55,7 @@ WindowStateStorage::~WindowStateStorage()
50void WindowStateStorage::saveState(const QString &windowId, WindowStateStorage::WindowState state)
55void WindowStateStorage::saveState(const QString &windowId, WindowStateStorage::WindowState state)
51{
56{
52 const QString queryString = QStringLiteral("INSERT OR REPLACE INTO state (windowId, state) values ('%1', '%2');")
57 const QString queryString = QStringLiteral("INSERT OR REPLACE INTO state (windowId, state) values ('%1', '%2');")
53− .arg(windowId)
58+ .arg(sanitiseString(windowId))
54 .arg((int)state);
59 .arg((int)state);
55
60
56 saveValue(queryString);
61 saveValue(queryString);
@@ -59,7 +64,7 @@ void WindowStateStorage::saveState(const QString &windowId, WindowStateStorage::
59WindowStateStorage::WindowState WindowStateStorage::getState(const QString &windowId, WindowStateStorage::WindowState defaultValue) const
64WindowStateStorage::WindowState WindowStateStorage::getState(const QString &windowId, WindowStateStorage::WindowState defaultValue) const
60{
65{
61 const QString queryString = QStringLiteral("SELECT * FROM state WHERE windowId = '%1';")
66 const QString queryString = QStringLiteral("SELECT * FROM state WHERE windowId = '%1';")
62− .arg(windowId);
67+ .arg(sanitiseString(windowId));
63
68
64 QSqlQuery query = getValue(queryString);
69 QSqlQuery query = getValue(queryString);
65
70
@@ -72,7 +77,7 @@ WindowStateStorage::WindowState WindowStateStorage::getState(const QString &wind
72void WindowStateStorage::saveGeometry(const QString &windowId, const QRect rect)
77void WindowStateStorage::saveGeometry(const QString &windowId, const QRect rect)
73{
78{
74 const QString queryString = QStringLiteral("INSERT OR REPLACE INTO geometry (windowId, x, y, width, height) values ('%1', '%2', '%3', '%4', '%5');")
79 const QString queryString = QStringLiteral("INSERT OR REPLACE INTO geometry (windowId, x, y, width, height) values ('%1', '%2', '%3', '%4', '%5');")
75− .arg(windowId)
80+ .arg(sanitiseString(windowId))
76 .arg(rect.x())
81 .arg(rect.x())
77 .arg(rect.y())
82 .arg(rect.y())
78 .arg(rect.width())
83 .arg(rect.width())
@@ -81,6 +86,28 @@ void WindowStateStorage::saveGeometry(const QString &windowId, const QRect rect)
81 saveValue(queryString);
86 saveValue(queryString);
82}
87}
83
88
89+void WindowStateStorage::saveStage(const QString &appId, int stage)
90+{
91+ const QString queryString = QStringLiteral("INSERT OR REPLACE INTO stage (appId, stage) values ('%1', '%2');")
92+ .arg(sanitiseString(appId))
93+ .arg((int)stage);
94+
95+ saveValue(queryString);
96+}
97+
98+int WindowStateStorage::getStage(const QString &appId) const
99+{
100+ const QString queryString = QStringLiteral("SELECT * FROM stage WHERE appId = '%1';")
101+ .arg(sanitiseString(appId));
102+
103+ QSqlQuery query = getValue(queryString);
104+
105+ if (!query.first()) {
106+ return unity::shell::application::ApplicationInfoInterface::MainStage;
107+ }
108+ return query.value("stage").toInt();
109+}
110+
84void WindowStateStorage::executeAsyncQuery(const QString &queryString)
111void WindowStateStorage::executeAsyncQuery(const QString &queryString)
85{
112{
86 QMutexLocker l(&s_mutex);
113 QMutexLocker l(&s_mutex);
@@ -97,7 +124,7 @@ void WindowStateStorage::executeAsyncQuery(const QString &queryString)
97QRect WindowStateStorage::getGeometry(const QString &windowId, const QRect defaultValue) const
124QRect WindowStateStorage::getGeometry(const QString &windowId, const QRect defaultValue) const
98{
125{
99 QString queryString = QStringLiteral("SELECT * FROM geometry WHERE windowId = '%1';")
126 QString queryString = QStringLiteral("SELECT * FROM geometry WHERE windowId = '%1';")
100− .arg(windowId);
127+ .arg(sanitiseString(windowId));
101
128
102 QSqlQuery query = getValue(queryString);
129 QSqlQuery query = getValue(queryString);
103
130
@@ -124,6 +151,11 @@ void WindowStateStorage::initdb()
124 QSqlQuery query;
151 QSqlQuery query;
125 query.exec(QStringLiteral("CREATE TABLE state(windowId TEXT UNIQUE, state INTEGER);"));
152 query.exec(QStringLiteral("CREATE TABLE state(windowId TEXT UNIQUE, state INTEGER);"));
126 }
153 }
154+
155+ if (!m_db.tables().contains(QStringLiteral("stage"))) {
156+ QSqlQuery query;
157+ query.exec(QStringLiteral("CREATE TABLE stage(appId TEXT UNIQUE, stage INTEGER);"));
158+ }
127}
159}
128
160
129void WindowStateStorage::saveValue(const QString &queryString)
161void WindowStateStorage::saveValue(const QString &queryString)
plugins/Utils/windowstatestorage.h+3
@@ -38,6 +38,9 @@ public:
38 Q_INVOKABLE void saveGeometry(const QString &windowId, const QRect rect);
38 Q_INVOKABLE void saveGeometry(const QString &windowId, const QRect rect);
39 Q_INVOKABLE QRect getGeometry(const QString &windowId, const QRect defaultValue) const;
39 Q_INVOKABLE QRect getGeometry(const QString &windowId, const QRect defaultValue) const;
40
40
41+ Q_INVOKABLE void saveStage(const QString &appId, int stage);
42+ Q_INVOKABLE int getStage(const QString &appId) const;
43+
41private:
44private:
42 void initdb();
45 void initdb();
43
46
qml/Components/Showable.qml+10
@@ -31,6 +31,7 @@ Item {
31 property bool required
31 property bool required
32 property bool __shouldShow: false
32 property bool __shouldShow: false
33 property bool __skipShowAnimation: false
33 property bool __skipShowAnimation: false
34+ property bool __skipHideAnimation: false
34
35
35 property list<QtObject> hides
36 property list<QtObject> hides
36 property var showAnimation
37 property var showAnimation
@@ -120,12 +121,21 @@ Item {
120 if (!hideAnimation.running) {
121 if (!hideAnimation.running) {
121 hideAnimation.restart()
122 hideAnimation.restart()
122 }
123 }
124+ if (__skipHideAnimation) {
125+ hideAnimation.complete();
126+ }
123 } else {
127 } else {
124 visible = false
128 visible = false
125 required = false
129 required = false
126 }
130 }
127
131
128 shown = false
132 shown = false
133+ __skipHideAnimation = false;
134+ }
135+
136+ function hideNow() {
137+ __skipHideAnimation = true;
138+ hide();
129 }
139 }
130
140
131 Connections {
141 Connections {
qml/Shell.qml+2−6
@@ -120,9 +120,7 @@ Item {
120 if (ApplicationManager.findApplication(appId)) {
120 if (ApplicationManager.findApplication(appId)) {
121 ApplicationManager.requestFocusApplication(appId);
121 ApplicationManager.requestFocusApplication(appId);
122 } else {
122 } else {
123− var execFlags = shell.usageScenario === "phone" ? ApplicationManager.ForceMainStage
124− : ApplicationManager.NoFlag;
125− ApplicationManager.startApplication(appId, execFlags);
123+ ApplicationManager.startApplication(appId);
126 }
124 }
127 }
125 }
128
126
@@ -547,9 +545,7 @@ Item {
547 greeterShown: greeter.shown
545 greeterShown: greeter.shown
548 }
546 }
549
547
550− readonly property bool topmostApplicationIsFullscreen:
548+ readonly property bool topmostApplicationIsFullscreen: mainApp && mainApp.fullscreen
551− ApplicationManager.focusedApplicationId &&
552− ApplicationManager.findApplication(ApplicationManager.focusedApplicationId).fullscreen
553
549
554 fullscreenMode: (topmostApplicationIsFullscreen && !lightDM.greeter.active && launcher.progress == 0)
550 fullscreenMode: (topmostApplicationIsFullscreen && !lightDM.greeter.active && launcher.progress == 0)
555 || greeter.hasLockedApp
551 || greeter.hasLockedApp
qml/Stages/AbstractStage.qml+1−1
@@ -44,7 +44,7 @@ Rectangle {
44
44
45 // To be read from outside
45 // To be read from outside
46 property var mainApp: null
46 property var mainApp: null
47− property int mainAppWindowOrientationAngle
47+ property int mainAppWindowOrientationAngle: 0
48 property bool orientationChangesEnabled
48 property bool orientationChangesEnabled
49 property int supportedOrientations: Qt.PortraitOrientation
49 property int supportedOrientations: Qt.PortraitOrientation
50 | Qt.LandscapeOrientation
50 | Qt.LandscapeOrientation
qml/Stages/ApplicationWindow.qml+3−1
@@ -24,12 +24,14 @@ FocusScope {
24 implicitHeight: sessionContainer.implicitHeight
24 implicitHeight: sessionContainer.implicitHeight
25
25
26 // to be read from outside
26 // to be read from outside
27− readonly property bool fullscreen: application ? application.fullscreen : false
28 property alias interactive: sessionContainer.interactive
27 property alias interactive: sessionContainer.interactive
29 property bool orientationChangesEnabled: d.supportsSurfaceResize ? d.surfaceOldEnoughToBeResized : true
28 property bool orientationChangesEnabled: d.supportsSurfaceResize ? d.surfaceOldEnoughToBeResized : true
30 readonly property string title: sessionContainer.surface && sessionContainer.surface.name !== "" ?
29 readonly property string title: sessionContainer.surface && sessionContainer.surface.name !== "" ?
31 sessionContainer.surface.name : d.name
30 sessionContainer.surface.name : d.name
32
31
32+ // overridable from outside
33+ property bool fullscreen: application ? application.fullscreen : false
34+
33 // to be set from outside
35 // to be set from outside
34 property QtObject application
36 property QtObject application
35 property int surfaceOrientationAngle
37 property int surfaceOrientationAngle
qml/Stages/SideStage.qmlnew file+118
@@ -0,0 +1,118 @@
1+/*
2+ * Copyright (C) 2016 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.4
18+import QtQuick.Layouts 1.1
19+import Ubuntu.Components 1.3
20+import Ubuntu.Gestures 0.1
21+import "../Components"
22+
23+Showable {
24+ id: root
25+ property bool showHint: true
26+ property int panelWidth: units.gu(40)
27+ readonly property alias dragging: hideSideStageDragArea.dragging
28+ readonly property real progress: width / panelWidth
29+
30+ width: 0
31+ shown: false
32+
33+ Item {
34+ id: sideStageDragHandle
35+ anchors {
36+ right: root.left
37+ top: root.top
38+ bottom: root.bottom
39+ }
40+ width: units.gu(2)
41+
42+ opacity: root.shown ? 1 : 0
43+ Behavior on opacity { UbuntuNumberAnimation {} }
44+
45+ Image {
46+ anchors.centerIn: parent
47+ width: hideSideStageDragArea.pressed ? parent.width * 2 : parent.width
48+ height: parent.height
49+ source: "graphics/sidestage_handle@20.png"
50+ Behavior on width { UbuntuNumberAnimation {} }
51+ }
52+ }
53+
54+ Rectangle {
55+ anchors.fill: parent
56+ color: Qt.rgba(0,0,0,0.95)
57+ }
58+
59+ Column {
60+ anchors.verticalCenter: parent.verticalCenter
61+ width: panelWidth - units.gu(6)
62+ x: panelWidth/2 - width/2
63+ spacing: units.gu(3)
64+ opacity: 0.8
65+ visible: showHint
66+
67+ Icon {
68+ width: units.gu(30)
69+ anchors.horizontalCenter: parent.horizontalCenter
70+ source: "graphics/sidestage_drag.svg"
71+ color: enabled ? Qt.rgba(1,1,1,1) : Qt.rgba(1,0,0,1)
72+ keyColor: Qt.rgba(1,1,1,1)
73+ }
74+
75+ Label {
76+ text: i18n.tr("Drag using 3 fingers any application from one window to the other")
77+ width: parent.width
78+ wrapMode: Text.WordWrap
79+ color: enabled ? Qt.rgba(1,1,1,1) : Qt.rgba(1,0,0,1)
80+ }
81+ }
82+
83+ showAnimation: NumberAnimation {
84+ property: "width"
85+ to: panelWidth
86+ duration: UbuntuAnimation.BriskDuration
87+ easing.type: Easing.OutCubic
88+ }
89+
90+ hideAnimation: NumberAnimation {
91+ property: "width"
92+ to: 0
93+ duration: UbuntuAnimation.BriskDuration
94+ easing.type: Easing.OutCubic
95+ }
96+
97+ DragHandle {
98+ id: hideSideStageDragArea
99+ objectName: "hideSideStageDragArea"
100+
101+ direction: Direction.Leftwards
102+ rotation: 180
103+ enabled: root.shown
104+ anchors.right: root.left
105+ width: sideStageDragHandle.width
106+ height: root.height
107+ stretch: true
108+
109+ immediateRecognition: true
110+ maxTotalDragDistance: panelWidth
111+ autoCompleteDragThreshold: panelWidth / 2
112+ }
113+
114+ // SideStage mouse event eater
115+ MouseArea {
116+ anchors.fill: parent
117+ }
118+}
qml/Stages/SpreadDelegate.qml+10−1
@@ -32,6 +32,11 @@ FocusScope {
32 readonly property alias appWindowOrientationAngle: appWindowWithShadow.orientationAngle
32 readonly property alias appWindowOrientationAngle: appWindowWithShadow.orientationAngle
33 readonly property alias appWindowRotation: appWindowWithShadow.rotation
33 readonly property alias appWindowRotation: appWindowWithShadow.rotation
34 readonly property alias orientationChangesEnabled: appWindow.orientationChangesEnabled
34 readonly property alias orientationChangesEnabled: appWindow.orientationChangesEnabled
35+ property int supportedOrientations: application ? application.supportedOrientations :
36+ Qt.PortraitOrientation
37+ | Qt.LandscapeOrientation
38+ | Qt.InvertedPortraitOrientation
39+ | Qt.InvertedLandscapeOrientation
35
40
36 // to be set from outside
41 // to be set from outside
37 property bool interactive: true
42 property bool interactive: true
@@ -45,6 +50,9 @@ FocusScope {
45 property QtObject orientations
50 property QtObject orientations
46 property bool highlightShown: false
51 property bool highlightShown: false
47
52
53+ // overrideable from outside
54+ property alias fullscreen: appWindow.fullscreen
55+
48 function matchShellOrientation() {
56 function matchShellOrientation() {
49 if (!root.application)
57 if (!root.application)
50 return;
58 return;
@@ -154,7 +162,8 @@ FocusScope {
154 if (!root.application || root.application.rotatesWindowContents) {
162 if (!root.application || root.application.rotatesWindowContents) {
155 return 0;
163 return 0;
156 }
164 }
165+
157− var supportedOrientations = root.application.supportedOrientations;
166+ var supportedOrientations = root.supportedOrientations;
158
167
159 if (supportedOrientations === Qt.PrimaryOrientation) {
168 if (supportedOrientations === Qt.PrimaryOrientation) {
160 supportedOrientations = root.orientations.primary;
169 supportedOrientations = root.orientations.primary;
qml/Stages/SurfaceContainer.qml+1
@@ -118,6 +118,7 @@ FocusScope {
118
118
119
119
120 TouchGate {
120 TouchGate {
121+ objectName: "touchGate-"+name
121 targetItem: surfaceItem
122 targetItem: surfaceItem
122 anchors.fill: root
123 anchors.fill: root
123 enabled: surfaceItem.enabled
124 enabled: surfaceItem.enabled
qml/Stages/TabletSideStageTouchGesture.qmlnew file+154
@@ -0,0 +1,154 @@
1+/*
2+ * Copyright (C) 2016 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.4
18+import Ubuntu.Gestures 0.1
19+
20+TouchGestureArea {
21+ id: root
22+ minimumTouchPoints: 3
23+ maximumTouchPoints: 3
24+
25+ property bool enableDrag: true
26+ property Component dragComponent
27+ property var dragComponentProperties: undefined
28+
29+ readonly property bool recognisedPress: status == TouchGestureArea.Recognized &&
30+ touchPoints.length >= minimumTouchPoints &&
31+ touchPoints.length <= maximumTouchPoints
32+ readonly property bool recognisedDrag: priv.wasRecognisedPress && dragging
33+
34+ signal pressed(int x, int y)
35+ signal clicked
36+ signal dragStarted
37+ signal dropped
38+ signal cancelled
39+
40+ onEnabledChanged: {
41+ if (!enabled) {
42+ if (priv.dragObject) root.cancelled();
43+ priv.wasRecognisedDrag = false;
44+ priv.wasRecognisedPress = false;
45+ }
46+ }
47+
48+ onRecognisedPressChanged: {
49+ if (recognisedPress) {
50+ // get the app at the center of the gesture
51+ var centerX = 0;
52+ var centerY = 0;
53+ for (var i = 0; i < touchPoints.length; i++) {
54+ centerX += touchPoints[i].x;
55+ centerY += touchPoints[i].y;
56+ }
57+ centerX = centerX/touchPoints.length;
58+ centerY = centerY/touchPoints.length;
59+
60+ pressed(centerX, centerY);
61+ priv.wasRecognisedPress = true;
62+ }
63+ }
64+
65+ onStatusChanged: {
66+ if (status != TouchGestureArea.Recognized) {
67+ if (status == TouchGestureArea.Rejected) {
68+ root.cancelled();
69+ } else if (status == TouchGestureArea.WaitingForTouch) {
70+ if (priv.wasRecognisedPress) {
71+ if (!priv.wasRecognisedDrag) {
72+ root.clicked();
73+ } else {
74+ root.dropped();
75+ }
76+ }
77+ }
78+ priv.wasRecognisedDrag = false;
79+ priv.wasRecognisedPress = false;
80+ }
81+ }
82+
83+ onRecognisedDragChanged: {
84+ if (enableDrag && recognisedDrag) {
85+ priv.wasRecognisedDrag = true;
86+ root.dragStarted()
87+ }
88+ }
89+
90+ QtObject {
91+ id: priv
92+ property var dragObject: null
93+
94+ property bool wasRecognisedPress: false
95+ property bool wasRecognisedDrag: false
96+ }
97+
98+ onCancelled: {
99+ if (priv.dragObject) {
100+ var obj = priv.dragObject;
101+ priv.dragObject = null;
102+
103+ obj.Drag.cancel();
104+ obj.destroy();
105+ }
106+ }
107+
108+ onDragStarted: {
109+ if (dragComponentProperties) {
110+ priv.dragObject = dragComponent.createObject(root, dragComponentProperties);
111+ } else {
112+ priv.dragObject = dragComponent.createObject(root);
113+ }
114+ priv.dragObject.Drag.start();
115+ }
116+
117+ onDropped: {
118+ if (priv.dragObject) {
119+ var obj = priv.dragObject;
120+ priv.dragObject = null;
121+
122+ obj.Drag.drop();
123+ obj.destroy();
124+ }
125+ }
126+
127+ Binding {
128+ target: priv.dragObject
129+ when: priv.dragObject && priv.wasRecognisedDrag
130+ property: "x"
131+ value: {
132+ if (!priv.dragObject) return 0;
133+ var sum = 0;
134+ for (var i = 0; i < root.touchPoints.length; i++) {
135+ sum += root.touchPoints[i].x;
136+ }
137+ return sum/root.touchPoints.length - priv.dragObject.width/2;
138+ }
139+ }
140+
141+ Binding {
142+ target: priv.dragObject
143+ when: priv.dragObject && priv.wasRecognisedDrag
144+ property: "y"
145+ value: {
146+ if (!priv.dragObject) return 0;
147+ var sum = 0;
148+ for (var i = 0; i < root.touchPoints.length; i++) {
149+ sum += root.touchPoints[i].y;
150+ }
151+ return sum/root.touchPoints.length - priv.dragObject.height/2;
152+ }
153+ }
154+}
qml/Stages/TabletStage.qml+387−171
@@ -46,7 +46,7 @@ AbstractStage {
46 if (delta < 0) { delta += 360; }
46 if (delta < 0) { delta += 360; }
47 delta = delta % 360;
47 delta = delta % 360;
48
48
49− var supportedOrientations = spreadDelegate.application.supportedOrientations;
49+ var supportedOrientations = spreadDelegate.supportedOrientations;
50 if (supportedOrientations === Qt.PrimaryOrientation) {
50 if (supportedOrientations === Qt.PrimaryOrientation) {
51 supportedOrientations = spreadDelegate.orientations.primary;
51 supportedOrientations = spreadDelegate.orientations.primary;
52 }
52 }
@@ -78,9 +78,24 @@ AbstractStage {
78
78
79 orientationChangesEnabled: priv.mainAppOrientationChangesEnabled
79 orientationChangesEnabled: priv.mainAppOrientationChangesEnabled
80
80
81+ supportedOrientations: {
82+ if (mainApp) {
81− supportedOrientations: mainApp ? mainApp.supportedOrientations
83+ var orientations = mainApp.supportedOrientations;
84+ orientations |= Qt.LandscapeOrientation | Qt.InvertedLandscapeOrientation;
85+ if (priv.sideStageAppId && !spreadView.surfaceDragging) {
86+ // If we have a sidestage app, support Portrait orientation
87+ // so that it will switch the sidestage app to mainstage on rotate
82− : (Qt.PortraitOrientation | Qt.LandscapeOrientation
88+ orientations |= Qt.PortraitOrientation|Qt.InvertedPortraitOrientation;
89+ }
90+ return orientations;
91+ } else {
92+ // we just don't care
93+ return Qt.PortraitOrientation |
94+ Qt.LandscapeOrientation |
95+ Qt.InvertedPortraitOrientation |
83− | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation)
96+ Qt.InvertedLandscapeOrientation;
97+ }
98+ }
84
99
85 onWidthChanged: {
100 onWidthChanged: {
86 spreadView.selectedIndex = -1;
101 spreadView.selectedIndex = -1;
@@ -88,13 +103,6 @@ AbstractStage {
88 spreadView.contentX = -spreadView.shift;
103 spreadView.contentX = -spreadView.shift;
89 }
104 }
90
105
91− onShellOrientationChanged: {
92− if (shellOrientation == Qt.PortraitOrientation || shellOrientation == Qt.InvertedPortraitOrientation) {
93− ApplicationManager.focusApplication(priv.mainStageAppId);
94− priv.sideStageAppId = "";
95− }
96− }
97−
98 onInverseProgressChanged: {
106 onInverseProgressChanged: {
99 // This can't be a simple binding because that would be triggered after this handler
107 // This can't be a simple binding because that would be triggered after this handler
100 // while we need it active before doing the anition left/right
108 // while we need it active before doing the anition left/right
@@ -170,20 +178,7 @@ AbstractStage {
170
178
171 property int highlightIndex: 0
179 property int highlightIndex: 0
172
180
173− onFocusedAppIdChanged: {
181+ onFocusedAppIdChanged: updateStageApps()
174− if (priv.focusedAppId.length > 0) {
175− var focusedApp = ApplicationManager.findApplication(focusedAppId);
176− if (focusedApp.stage == ApplicationInfoInterface.SideStage) {
177− priv.sideStageAppId = focusedAppId;
178− } else {
179− priv.mainStageAppId = focusedAppId;
180− root.mainApp = focusedApp;
181− }
182− }
183−
184− appId0 = ApplicationManager.count >= 1 ? ApplicationManager.get(0).appId : "";
185− appId1 = ApplicationManager.count > 1 ? ApplicationManager.get(1).appId : "";
186− }
187
182
188 onFocusedAppDelegateChanged: {
183 onFocusedAppDelegateChanged: {
189 if (focusedAppDelegate) {
184 if (focusedAppDelegate) {
@@ -229,6 +224,46 @@ AbstractStage {
229 onHighlightIndexChanged: {
224 onHighlightIndexChanged: {
230 spreadView.contentX = highlightIndex * spreadView.contentWidth / (spreadRepeater.count + 2)
225 spreadView.contentX = highlightIndex * spreadView.contentWidth / (spreadRepeater.count + 2)
231 }
226 }
227+
228+ function getTopApp(stage) {
229+ for (var i = 0; i < ApplicationManager.count; i++) {
230+ var app = ApplicationManager.get(i)
231+ if (app.stage === stage) {
232+ return app;
233+ }
234+ }
235+ return null;
236+ }
237+
238+ function setAppStage(appId, stage, save) {
239+ var app = ApplicationManager.findApplication(appId);
240+ if (app) {
241+ app.stage = stage;
242+ if (save) {
243+ WindowStateStorage.saveStage(appId, stage);
244+ }
245+ }
246+ }
247+
248+ function updateStageApps() {
249+ var app = priv.getTopApp(ApplicationInfoInterface.MainStage);
250+ priv.mainStageAppId = app ? app.appId : ""
251+ root.mainApp = app;
252+
253+ if (sideStage.shown) {
254+ app = priv.getTopApp(ApplicationInfoInterface.SideStage);
255+ priv.sideStageAppId = app ? app.appId : ""
256+ } else {
257+ priv.sideStageAppId = "";
258+ }
259+
260+ appId0 = ApplicationManager.count >= 1 ? ApplicationManager.get(0).appId : "";
261+ appId1 = ApplicationManager.count > 1 ? ApplicationManager.get(1).appId : "";
262+ }
263+
264+ readonly property bool sideStageEnabled: root.shellOrientation == Qt.LandscapeOrientation ||
265+ root.shellOrientation == Qt.InvertedLandscapeOrientation
266+ Component.onCompleted: updateStageApps();
232 }
267 }
233
268
234 Connections {
269 Connections {
@@ -256,7 +291,8 @@ AbstractStage {
256 ApplicationManager.focusApplication("unity8-dash")
291 ApplicationManager.focusApplication("unity8-dash")
257 }
292 }
258 if (priv.sideStageAppId == appId) {
293 if (priv.sideStageAppId == appId) {
294+ var app = priv.getTopApp(ApplicationInfoInterface.SideStage);
259− priv.sideStageAppId = "";
295+ priv.sideStageAppId = app === null ? "" : app.appId;
260 }
296 }
261
297
262 if (ApplicationManager.count == 0) {
298 if (ApplicationManager.count == 0) {
@@ -267,6 +303,7 @@ AbstractStage {
267 // lets make sure the spread doesn't mess up by the changing app list.
303 // lets make sure the spread doesn't mess up by the changing app list.
268 spreadView.phase = 0;
304 spreadView.phase = 0;
269 spreadView.contentX = -spreadView.shift;
305 spreadView.contentX = -spreadView.shift;
306+
270 ApplicationManager.focusApplication(ApplicationManager.get(0).appId);
307 ApplicationManager.focusApplication(ApplicationManager.get(0).appId);
271 }
308 }
272 }
309 }
@@ -322,6 +359,7 @@ AbstractStage {
322 property int selectedIndex: -1
359 property int selectedIndex: -1
323 property int draggedDelegateCount: 0
360 property int draggedDelegateCount: 0
324 property int closingIndex: -1
361 property int closingIndex: -1
362+ property var selectedApplication: selectedIndex !== -1 ? ApplicationManager.get(selectedIndex) : null
325
363
326 // FIXME: Workaround Flickable's not keepping its contentX still when resized
364 // FIXME: Workaround Flickable's not keepping its contentX still when resized
327 onContentXChanged: { forceItToRemainStillIfBeingResized(); }
365 onContentXChanged: { forceItToRemainStillIfBeingResized(); }
@@ -343,15 +381,8 @@ AbstractStage {
343 }
381 }
344 }
382 }
345
383
346− property bool sideStageDragging: sideStageDragHandle.dragging
347− property real sideStageDragProgress: sideStageDragHandle.progress
384+ property real sideStageDragProgress: sideStage.progress
348−
385+ property bool surfaceDragging: triGestureArea.recognisedDrag
349− onSideStageDragProgressChanged: {
350− if (sideStageDragProgress == 1) {
351− ApplicationManager.focusApplication(priv.mainStageAppId);
352− priv.sideStageAppId = "";
353− }
354− }
355
386
356 // In case the ApplicationManager already holds an app when starting up we're missing animations
387 // In case the ApplicationManager already holds an app when starting up we're missing animations
357 // Make sure we end up in the same state
388 // Make sure we end up in the same state
@@ -402,7 +433,7 @@ AbstractStage {
402 }
433 }
403 ]
434 ]
404 state: {
435 state: {
405− if (priv.mainStageAppId && !priv.sideStageAppId) {
436+ if ((priv.mainStageAppId && !priv.sideStageAppId) || !priv.sideStageEnabled) {
406 return "main";
437 return "main";
407 }
438 }
408 if (!priv.mainStageAppId && priv.sideStageAppId) {
439 if (!priv.mainStageAppId && priv.sideStageAppId) {
@@ -419,12 +450,26 @@ AbstractStage {
419 // Flickabe.contentX wiggles during resizes. Don't react to it.
450 // Flickabe.contentX wiggles during resizes. Don't react to it.
420 return;
451 return;
421 }
452 }
453+
454+ switch (phase) {
455+ case 0:
456+ // the "spreadEnabled" part is because when code does "phase = 0; contentX = -shift" to
457+ // dismiss the spread because spreadEnabled went to false, for some reason, during tests,
458+ // Flickable might jump in and change contentX value back, causing the code below to do
459+ // "phase = 1" which will make the spread stay.
460+ // It sucks that we have no control whatsoever over whether or when Flickable animates its
461+ // contentX.
422− if (spreadView.phase == 0 && spreadView.shiftedContentX > spreadView.width * spreadView.positionMarker2) {
462+ if (root.spreadEnabled && shiftedContentX > width * positionMarker2) {
423− spreadView.phase = 1;
463+ phase = 1;
464+ }
465+ break;
466+ case 1:
467+ if (shiftedContentX < width * positionMarker2) {
468+ phase = 0;
424− } else if (spreadView.phase == 1 && spreadView.shiftedContentX > spreadView.width * spreadView.positionMarker4) {
469+ } else if (shiftedContentX >= width * positionMarker4 && !spreadDragArea.dragging) {
425− spreadView.phase = 2;
470+ phase = 2;
426− } else if (spreadView.phase == 1 && spreadView.shiftedContentX < spreadView.width * spreadView.positionMarker2) {
471+ }
427− spreadView.phase = 0;
472+ break;
428 }
473 }
429 }
474 }
430
475
@@ -446,6 +491,7 @@ AbstractStage {
446 }
491 }
447
492
448 function snapTo(index) {
493 function snapTo(index) {
494+ snapAnimation.stop();
449 spreadView.selectedIndex = index;
495 spreadView.selectedIndex = index;
450 snapAnimation.targetContentX = -shift;
496 snapAnimation.targetContentX = -shift;
451 snapAnimation.start();
497 snapAnimation.start();
@@ -455,55 +501,41 @@ AbstractStage {
455 // We don't want to really reorder them in the model because that allows us to keep track
501 // We don't want to really reorder them in the model because that allows us to keep track
456 // of the last focused order.
502 // of the last focused order.
457 function indexToZIndex(index) {
503 function indexToZIndex(index) {
504+ // only shuffle when we've got a main and overlay
505+ if (state !== "mainAndOverlay") return index;
506+
458 var app = ApplicationManager.get(index);
507 var app = ApplicationManager.get(index);
459 if (!app) {
508 if (!app) {
460 return index;
509 return index;
461 }
510 }
462
511
512+ // don't shuffle indexes greater than "actives or next"
513+ if (index > 2) return index;
514+
463− var active = app.appId == priv.mainStageAppId || app.appId == priv.sideStageAppId;
515+ if (app.appId === priv.mainStageAppId) {
464− if (active && app.stage == ApplicationInfoInterface.MainStage) {
516+ // Active main stage always at 0
465− // if this app is active, and its the MainStage, always put it to index 0
466 return 0;
517 return 0;
467 }
518 }
468− if (active && app.stage == ApplicationInfoInterface.SideStage) {
469− if (!priv.mainStageAppId) {
470− // Only have SS apps running. Put the active one at 0
471− return 0;
472− }
473
519
474− // Precondition now: There's an active MS app and this is SS app:
520+ if (spreadView.nextInStack > 0) {
475− if (spreadView.nextInStack >= 0 && ApplicationManager.get(spreadView.nextInStack).stage == ApplicationInfoInterface.MainStage) {
521+ var nextAppInStack = ApplicationManager.get(spreadView.nextInStack);
476− // If the next app coming from the right is a MS app, we need to elevate this SS ap above it.
522+
477− // Put it to at least level 2, or higher if there's more apps coming in before this one.
523+ if (index === spreadView.nextInStack) {
478− return Math.max(index, 2);
524+ // this is the next app in stack.
479− } else {
525+
480− // if this is no next app to come in from the right, place this one at index 1, just on top the active MS app.
526+ if (app.stage === ApplicationInfoInterface.SideStage) {
527+ // if the next app in stack is a sidestage app, it must order on top of other side stage app
528+ return Math.min(2, ApplicationManager.count-1);
529+ }
481 return 1;
530 return 1;
482 }
531 }
483− }
484− if (index <= 2 && app.stage == ApplicationInfoInterface.MainStage && priv.sideStageAppId) {
485− // Ok, this is an inactive MS app. If there's an active SS app around, we need to place this one
486− // in between the active MS app and the active SS app, so that it comes in from there when dragging from the right.
487− // If there's now active SS app, just leave it where it is.
488− return priv.indexOf(priv.sideStageAppId) < index ? index - 1 : index;
489− }
490− if (index == spreadView.nextInStack && app.stage == ApplicationInfoInterface.SideStage) {
532+ if (nextAppInStack.stage === ApplicationInfoInterface.SideStage) {
491− // This is a SS app and the next one to come in from the right:
533+ // if the next app in stack is a sidestage app, it must order on top of other side stage app
492− if (priv.sideStageAppId && priv.mainStageAppId) {
493− // If there's both, an active MS and an active SS app, put this one right on top of that
494− return 2;
534+ return 1;
495 }
535 }
496− // Or if there's only one other active app, put it on top of that.
536+ return Math.min(2, ApplicationManager.count-1);
497− // The case that there isn't any other active app is already handled above.
498− return 1;
499− }
500− if (index == 2 && spreadView.nextInStack == 1 && priv.sideStageAppId) {
501− // If its index 2 but not the next one to come in, it means
502− // we've pulled another one down to index 2. Move this one up to 2 instead.
503− return 3;
504 }
537 }
505− // don't touch all others... (mostly index > 3 + simple cases where the above doesn't shuffle much)
538+ return Math.min(index+1, ApplicationManager.count-1);
506− return index;
507 }
539 }
508
540
509 SequentialAnimation {
541 SequentialAnimation {
@@ -521,8 +553,12 @@ AbstractStage {
521 script: {
553 script: {
522 if (spreadView.selectedIndex >= 0) {
554 if (spreadView.selectedIndex >= 0) {
523 var newIndex = spreadView.selectedIndex;
555 var newIndex = spreadView.selectedIndex;
556+ var application = ApplicationManager.get(newIndex);
557+ if (application.stage === ApplicationInfoInterface.SideStage) {
558+ sideStage.showNow();
559+ }
524 spreadView.selectedIndex = -1;
560 spreadView.selectedIndex = -1;
525− ApplicationManager.focusApplication(ApplicationManager.get(newIndex).appId);
561+ ApplicationManager.focusApplication(application.appId);
526 spreadView.phase = 0;
562 spreadView.phase = 0;
527 spreadView.contentX = -spreadView.shift;
563 spreadView.contentX = -spreadView.shift;
528 }
564 }
@@ -545,87 +581,82 @@ AbstractStage {
545 spreadView.snapTo(0);
581 spreadView.snapTo(0);
546 }
582 }
547
583
548− Rectangle {
584+ DropArea {
549− id: sideStageBackground
585+ objectName: "MainStageDropArea"
550− color: "black"
586+ anchors {
551− width: spreadView.sideStageWidth * (1 - sideStageDragHandle.progress)
587+ left: parent.left
552− height: priv.landscapeHeight
588+ top: parent.top
589+ bottom: parent.bottom
590+ }
553− x: spreadView.width - width
591+ width: spreadView.width - sideStage.width
554− z: spreadView.indexToZIndex(priv.indexOf(priv.sideStageAppId))
592+ enabled: priv.sideStageEnabled
555− opacity: spreadView.phase == 0 ? 1 : 0
593+
556− Behavior on opacity { UbuntuNumberAnimation {} }
594+ onDropped: {
595+ priv.setAppStage(drag.source.appId, ApplicationInfoInterface.MainStage, true);
596+ ApplicationManager.focusApplication(drag.source.appId);
597+ }
598+ keys: "SideStage"
557 }
599 }
558
600
559− Item {
601+ SideStage {
560− id: sideStageDragHandle
602+ id: sideStage
561− anchors.right: sideStageBackground.left
603+ objectName: "sideStage"
562− anchors.top: sideStageBackground.top
563− width: units.gu(2)
564 height: priv.landscapeHeight
604 height: priv.landscapeHeight
565− z: sideStageBackground.z
605+ x: spreadView.width - width
566− opacity: spreadView.phase <= 0 && spreadView.sideStageVisible ? 1 : 0
606+ z: {
567− property real progress: 0
607+ if (!priv.mainStageAppId) return 0;
568− property bool dragging: false
569
608
570− Behavior on opacity { UbuntuNumberAnimation {} }
609+ if (priv.sideStageAppId && spreadView.nextInStack > 0) {
610+ var nextAppInStack = ApplicationManager.get(spreadView.nextInStack);
571
611
572− Connections {
612+ if (nextAppInStack.stage === ApplicationInfoInterface.MainStage) {
573− target: spreadView
613+ // if the next app in stack is a main stage app, put the sidestage on top of it.
574− onSideStageVisibleChanged: {
614+ return 2;
575− if (spreadView.sideStageVisible) {
576− sideStageDragHandle.progress = 0;
577 }
615 }
616+ return 1;
578 }
617 }
618+
619+ return 1;
579 }
620 }
621+ visible: progress != 0
622+ enabled: priv.sideStageEnabled && sideStageDropArea.dropAllowed
623+ opacity: priv.sideStageEnabled && !spreadView.active ? 1 : 0
624+ Behavior on opacity { UbuntuNumberAnimation {} }
580
625
581− Image {
626+ onShownChanged: {
582− anchors.centerIn: parent
627+ if (!shown && ApplicationManager.focusedApplicationId == priv.sideStageAppId) {
583− width: sideStageDragHandleMouseArea.pressed ? parent.width * 2 : parent.width
628+ ApplicationManager.requestFocusApplication(priv.mainStageAppId);
584− height: parent.height
629+ }
585− source: "graphics/sidestage_handle@20.png"
630+ priv.updateStageApps();
586− Behavior on width { UbuntuNumberAnimation {} }
631+ if (shown && priv.sideStageAppId) {
632+ ApplicationManager.requestFocusApplication(priv.sideStageAppId);
633+ }
587 }
634 }
588
635
589− MouseArea {
636+ DropArea {
590− id: sideStageDragHandleMouseArea
637+ id: sideStageDropArea
638+ objectName: "SideStageDropArea"
591 anchors.fill: parent
639 anchors.fill: parent
592− enabled: spreadView.shiftedContentX == 0
640+
593− property int startX
641+ property bool dropAllowed: true
594− property var gesturePoints: new Array()
642+
595− property real totalDiff
643+ onEntered: {
596−
644+ dropAllowed = drag.keys != "Disabled";
597− onPressed: {
598− gesturePoints = [];
599− startX = mouseX;
600− totalDiff = 0.0;
601− sideStageDragHandle.progress = 0;
602− sideStageDragHandle.dragging = true;
603− }
604− onMouseXChanged: {
605− totalDiff += mouseX - startX;
606− if (priv.mainStageAppId) {
607− sideStageDragHandle.progress = Math.max(0, totalDiff / spreadView.sideStageWidth);
608− }
609− gesturePoints.push(mouseX);
610 }
645 }
611− onReleased: {
646+ onExited: {
612− if (priv.mainStageAppId) {
647+ dropAllowed = true;
613− var oneWayFlick = priv.evaluateOneWayFlick(gesturePoints);
648+ }
614− sideStageDragSnapAnimation.to = sideStageDragHandle.progress > 0.5 || oneWayFlick ? 1 : 0;
649+ onDropped: {
615− sideStageDragSnapAnimation.start();
650+ if (drop.keys == "MainStage") {
616− } else {
651+ priv.setAppStage(drop.source.appId, ApplicationInfoInterface.SideStage, true);
617− sideStageDragHandle.dragging = false;
652+ ApplicationManager.requestFocusApplication(drop.source.appId);
618 }
653 }
619 }
654 }
620− }
655+ drag {
621− UbuntuNumberAnimation {
656+ onSourceChanged: {
622− id: sideStageDragSnapAnimation
657+ if (!sideStageDropArea.drag.source) {
623− target: sideStageDragHandle
658+ dropAllowed = true;
624− property: "progress"
659+ }
625−
626− onRunningChanged: {
627− if (!running) {
628− sideStageDragHandle.dragging = false;
629 }
660 }
630 }
661 }
631 }
662 }
@@ -640,29 +671,17 @@ AbstractStage {
640 id: spreadTile
671 id: spreadTile
641 objectName: model.appId ? "tabletSpreadDelegate_" + model.appId
672 objectName: model.appId ? "tabletSpreadDelegate_" + model.appId
642 : "tabletSpreadDelegate_null";
673 : "tabletSpreadDelegate_null";
643− width: {
644− if (wantsMainStage) {
645− return spreadView.width;
674+ width: spreadView.width
646− } else {
647− return spreadView.sideStageWidth;
648− }
649− }
650− height: {
651− if (wantsMainStage) {
652− return spreadView.height;
675+ height: spreadView.height
653− } else {
654− return priv.landscapeHeight;
655− }
656− }
657− active: model.appId == priv.mainStageAppId || model.appId == priv.sideStageAppId
676+ active: appId == priv.mainStageAppId || appId == priv.sideStageAppId
658− zIndex: spreadView.indexToZIndex(index)
677+ zIndex: selected && stage == ApplicationInfoInterface.MainStage ? 0 : spreadView.indexToZIndex(index)
659 selected: spreadView.selectedIndex == index
678 selected: spreadView.selectedIndex == index
660 otherSelected: spreadView.selectedIndex >= 0 && !selected
679 otherSelected: spreadView.selectedIndex >= 0 && !selected
661− isInSideStage: priv.sideStageAppId == model.appId
680+ isInSideStage: priv.sideStageAppId === appId
662 interactive: !spreadView.interactive && spreadView.phase === 0 && root.interactive
681 interactive: !spreadView.interactive && spreadView.phase === 0 && root.interactive
663 swipeToCloseEnabled: spreadView.interactive && !snapAnimation.running
682 swipeToCloseEnabled: spreadView.interactive && !snapAnimation.running
664 maximizedAppTopMargin: root.maximizedAppTopMargin
683 maximizedAppTopMargin: root.maximizedAppTopMargin
665− dragOffset: !isDash && model.appId == priv.mainStageAppId && root.inverseProgress > 0 && spreadView.phase === 0 ? root.inverseProgress : 0
684+ dragOffset: !isDash && appId == priv.mainStageAppId && root.inverseProgress > 0 && spreadView.phase === 0 ? root.inverseProgress : 0
666 application: ApplicationManager.get(index)
685 application: ApplicationManager.get(index)
667 closeable: !isDash
686 closeable: !isDash
668 highlightShown: root.altTabPressed && priv.highlightIndex == zIndex
687 highlightShown: root.altTabPressed && priv.highlightIndex == zIndex
@@ -671,6 +690,35 @@ AbstractStage {
671
690
672 readonly property bool isDash: model.appId == "unity8-dash"
691 readonly property bool isDash: model.appId == "unity8-dash"
673
692
693+ stage: model.stage
694+ fullscreen: {
695+ if (mainApp && stage === ApplicationInfoInterface.SideStage) {
696+ return mainApp.fullscreen;
697+ }
698+ return application ? application.fullscreen : false;
699+ }
700+
701+ supportedOrientations: {
702+ if (application) {
703+ var orientations = application.supportedOrientations;
704+ if (stage == ApplicationInfoInterface.MainStage) {
705+ // When an app is in the mainstage, it always supports Landscape|InvertedLandscape
706+ // so that we can drag it from the main stage to the side stage
707+ orientations |= Qt.LandscapeOrientation | Qt.InvertedLandscapeOrientation;
708+ }
709+ return orientations;
710+ } else {
711+ // we just don't care
712+ return Qt.PortraitOrientation |
713+ Qt.LandscapeOrientation |
714+ Qt.InvertedPortraitOrientation |
715+ Qt.InvertedLandscapeOrientation;
716+ }
717+ }
718+
719+ readonly property string appId: model.appId
720+ readonly property bool isDash: appId == "unity8-dash"
721+
674 Binding {
722 Binding {
675 target: spreadTile.application
723 target: spreadTile.application
676 property: "exemptFromLifecycle"
724 property: "exemptFromLifecycle"
@@ -701,6 +749,28 @@ AbstractStage {
701 enabled: spreadView.closingIndex >= 0
749 enabled: spreadView.closingIndex >= 0
702 UbuntuNumberAnimation {}
750 UbuntuNumberAnimation {}
703 }
751 }
752+ Connections {
753+ target: priv
754+ onSideStageEnabledChanged: refreshStage()
755+ }
756+
757+ Component.onCompleted: {
758+ refreshStage()
759+ stageChanged.connect(priv.updateStageApps);
760+ }
761+
762+ function refreshStage() {
763+ var stage = ApplicationInfoInterface.MainStage;
764+ if (priv.sideStageEnabled) {
765+ if (application && application.supportedOrientations & (Qt.PortraitOrientation|Qt.InvertedPortraitOrientation)) {
766+ stage = WindowStateStorage.getStage(appId);
767+ }
768+ }
769+
770+ if (model.stage !== stage) {
771+ priv.setAppStage(appId, stage, false);
772+ }
773+ }
704
774
705 // This is required because none of the bindings are triggered in some cases:
775 // This is required because none of the bindings are triggered in some cases:
706 // When an app is closed, it might happen that ApplicationManager.get(nextInStack)
776 // When an app is closed, it might happen that ApplicationManager.get(nextInStack)
@@ -725,7 +795,7 @@ AbstractStage {
725
795
726 // TODO: Hiding tile when progress is such that it will be off screen.
796 // TODO: Hiding tile when progress is such that it will be off screen.
727 property bool occluded: {
797 property bool occluded: {
728− if (spreadView.active) return false;
798+ if (spreadView.active && !offScreen) return false;
729 else if (spreadTile.active) return false;
799 else if (spreadTile.active) return false;
730 else if (xTranslateAnimating) return false;
800 else if (xTranslateAnimating) return false;
731 else if (z <= 1 && priv.focusedAppDelegateIsDislocated) return false;
801 else if (z <= 1 && priv.focusedAppDelegateIsDislocated) return false;
@@ -749,17 +819,87 @@ AbstractStage {
749 return progress;
819 return progress;
750 }
820 }
751
821
752− shellOrientationAngle: wantsMainStage ? root.shellOrientationAngle : 0
822+ shellOrientationAngle: root.shellOrientationAngle
823+ shellOrientation: root.shellOrientation
824+ orientations: root.orientations
825+
826+ states: [
827+ State {
828+ name: "MainStage"
829+ when: spreadTile.stage == ApplicationInfoInterface.MainStage
830+ },
831+ State {
832+ name: "SideStage"
833+ when: spreadTile.stage == ApplicationInfoInterface.SideStage
834+
835+ PropertyChanges {
836+ target: spreadTile
837+ width: spreadView.sideStageWidth
838+ height: priv.landscapeHeight
839+
840+ supportedOrientations: Qt.PortraitOrientation
841+ shellOrientationAngle: 0
753− shellOrientation: wantsMainStage ? root.shellOrientation : Qt.PortraitOrientation
842+ shellOrientation: Qt.PortraitOrientation
843+ orientations: sideStageOrientations
844+ }
845+ }
846+ ]
847+
754− orientations: Orientations {
848+ Orientations {
849+ id: sideStageOrientations
755− primary: spreadTile.wantsMainStage ? root.orientations.primary : Qt.PortraitOrientation
850+ primary: Qt.PortraitOrientation
756− native_: spreadTile.wantsMainStage ? root.orientations.native_ : Qt.PortraitOrientation
851+ native_: Qt.PortraitOrientation
757 portrait: root.orientations.portrait
852 portrait: root.orientations.portrait
758 invertedPortrait: root.orientations.invertedPortrait
853 invertedPortrait: root.orientations.invertedPortrait
759 landscape: root.orientations.landscape
854 landscape: root.orientations.landscape
760 invertedLandscape: root.orientations.invertedLandscape
855 invertedLandscape: root.orientations.invertedLandscape
761 }
856 }
762
857
858+ transitions: [
859+ Transition {
860+ to: "SideStage"
861+ SequentialAnimation {
862+ PropertyAction {
863+ target: spreadTile
864+ properties: "width,height,supportedOrientations,shellOrientationAngle,shellOrientation,orientations"
865+ }
866+ ScriptAction {
867+ script: {
868+ // rotate immediately.
869+ spreadTile.matchShellOrientation();
870+ if (ApplicationManager.focusedApplicationId === spreadTile.appId &&
871+ priv.sideStageEnabled && !sideStage.shown) {
872+ // Sidestage was focused, so show the side stage.
873+ sideStage.show();
874+ // if we've switched to a main app which doesnt support portrait, hide the side stage.
875+ } else if (mainApp && (mainApp.supportedOrientations & (Qt.PortraitOrientation|Qt.InvertedPortraitOrientation)) == 0) {
876+ sideStage.hideNow();
877+ }
878+ }
879+ }
880+ }
881+ },
882+ Transition {
883+ from: "SideStage"
884+ SequentialAnimation {
885+ ScriptAction {
886+ script: {
887+ if (priv.sideStageAppId === spreadTile.appId &&
888+ mainApp && (mainApp.supportedOrientations & (Qt.PortraitOrientation|Qt.InvertedPortraitOrientation)) == 0) {
889+ // The mainstage app did not natively support portrait orientation, so focus the sidestage.
890+ ApplicationManager.requestFocusApplication(spreadTile.appId);
891+ }
892+ }
893+ }
894+ PropertyAction {
895+ target: spreadTile
896+ properties: "width,height,supportedOrientations,shellOrientationAngle,shellOrientation,orientations"
897+ }
898+ ScriptAction { script: { spreadTile.matchShellOrientation(); } }
899+ }
900+ }
901+ ]
902+
763 onClicked: {
903 onClicked: {
764 if (spreadView.phase == 2) {
904 if (spreadView.phase == 2) {
765 spreadView.snapTo(index);
905 spreadView.snapTo(index);
@@ -779,6 +919,16 @@ AbstractStage {
779 ApplicationManager.stopApplication(ApplicationManager.get(index).appId);
919 ApplicationManager.stopApplication(ApplicationManager.get(index).appId);
780 }
920 }
781
921
922+ onFocusChanged: {
923+ if (focus && ApplicationManager.focusedApplicationId !== appId) {
924+ ApplicationManager.focusApplication(appId);
925+ }
926+
927+ if (focus && priv.sideStageEnabled && stage === ApplicationInfoInterface.SideStage) {
928+ sideStage.show();
929+ }
930+ }
931+
782 Binding {
932 Binding {
783 target: root
933 target: root
784 when: model.appId == priv.mainStageAppId
934 when: model.appId == priv.mainStageAppId
@@ -803,6 +953,71 @@ AbstractStage {
803 }
953 }
804 }
954 }
805
955
956+ TabletSideStageTouchGesture {
957+ id: triGestureArea
958+ anchors.fill: parent
959+ enabled: priv.sideStageEnabled && !spreadView.active
960+ property var dragObject: null
961+ property string appId: ""
962+ dragComponent: dragComponent
963+ dragComponentProperties: { "appId": appId }
964+
965+ onPressed: {
966+ function matchDelegate(obj) { return String(obj.objectName).indexOf("tabletSpreadDelegate") >= 0; }
967+
968+ var delegateAtCenter = Functions.itemAt(spreadRow, x, y, matchDelegate);
969+ if (!delegateAtCenter) return;
970+
971+ appId = delegateAtCenter.appId;
972+ }
973+
974+ onClicked: {
975+ if (sideStage.shown) {
976+ sideStage.hide();
977+ } else {
978+ sideStage.show();
979+ }
980+ }
981+
982+ onDragStarted: {
983+ // If we're dragging to the sidestage.
984+ if (!sideStage.shown) {
985+ sideStage.show();
986+ }
987+ }
988+
989+ Component {
990+ id: dragComponent
991+ SessionContainer {
992+ property string appId: ""
993+ property var application: ApplicationManager.findApplication(appId)
994+
995+ session: application ? application.session : null
996+ interactive: false
997+ resizeSurface: false
998+ focus: false
999+
1000+ width: units.gu(40)
1001+ height: units.gu(40)
1002+
1003+ Drag.hotSpot.x: width/2
1004+ Drag.hotSpot.y: height/2
1005+ // only accept opposite stage.
1006+ Drag.keys: {
1007+ if (!application) return "Disabled";
1008+
1009+ if (application.stage === ApplicationInfo.MainStage) {
1010+ if (application.supportedOrientations & (Qt.PortraitOrientation|Qt.InvertedPortraitOrientation)) {
1011+ return "MainStage";
1012+ }
1013+ return "Disabled";
1014+ }
1015+ return "SideStage";
1016+ }
1017+ }
1018+ }
1019+ }
1020+
806 //eat touch events during the right edge gesture
1021 //eat touch events during the right edge gesture
807 MouseArea {
1022 MouseArea {
808 anchors.fill: parent
1023 anchors.fill: parent
@@ -812,7 +1027,8 @@ AbstractStage {
812 DirectionalDragArea {
1027 DirectionalDragArea {
813 id: spreadDragArea
1028 id: spreadDragArea
814 objectName: "spreadDragArea"
1029 objectName: "spreadDragArea"
1030+ x: parent.width - root.dragAreaWidth
815− anchors { top: parent.top; right: parent.right; bottom: parent.bottom }
1031+ anchors { top: parent.top; bottom: parent.bottom }
816 width: root.dragAreaWidth
1032 width: root.dragAreaWidth
817 direction: Direction.Leftwards
1033 direction: Direction.Leftwards
818 enabled: (spreadView.phase != 2 && root.spreadEnabled) || dragging
1034 enabled: (spreadView.phase != 2 && root.spreadEnabled) || dragging
qml/Stages/TransformedTabletSpreadDelegate.qml+84−56
@@ -31,6 +31,7 @@ SpreadDelegate {
31 // Set this to true when this tile a currently active on either the MS or the SS.
31 // Set this to true when this tile a currently active on either the MS or the SS.
32 property bool active: false
32 property bool active: false
33
33
34+ property int stage
34 property int zIndex
35 property int zIndex
35 property real progress: 0
36 property real progress: 0
36 property real animatedProgress: 0
37 property real animatedProgress: 0
@@ -49,10 +50,11 @@ SpreadDelegate {
49
50
50 property int dragOffset: 0
51 property int dragOffset: 0
51 readonly property alias xTranslateAnimating: xTranslateAnimation.running
52 readonly property alias xTranslateAnimating: xTranslateAnimation.running
53+ readonly property bool offScreen: priv.xTranslate >= 0
52
54
53 dropShadow: spreadView.active ||
55 dropShadow: spreadView.active ||
54 (active
56 (active
55− && (model.stage == ApplicationInfoInterface.MainStage || !priv.shellIsLandscape)
57+ && (stage == ApplicationInfoInterface.MainStage || !priv.shellIsLandscape)
56 && priv.xTranslate != 0)
58 && priv.xTranslate != 0)
57
59
58 onSelectedChanged: {
60 onSelectedChanged: {
@@ -141,9 +143,9 @@ SpreadDelegate {
141 enabled: !spreadView.active &&
143 enabled: !spreadView.active &&
142 !snapAnimation.running &&
144 !snapAnimation.running &&
143 model.appId !== "unity8-dash" &&
145 model.appId !== "unity8-dash" &&
144− !spreadView.sideStageDragging &&
145 spreadView.animateX &&
146 spreadView.animateX &&
146− !spreadView.beingResized
147+ !spreadView.beingResized &&
148+ priv.state !== "sideStage"
147 UbuntuNumberAnimation {
149 UbuntuNumberAnimation {
148 id: xTranslateAnimation
150 id: xTranslateAnimation
149 duration: UbuntuAnimation.FastDuration
151 duration: UbuntuAnimation.FastDuration
@@ -153,21 +155,20 @@ SpreadDelegate {
153 property real xTranslate: {
155 property real xTranslate: {
154 var newTranslate = 0;
156 var newTranslate = 0;
155
157
156− if (otherSelected) {
158+ // selected app or opposite stage active app.
157− return priv.selectedXTranslate;
159+ if (isSelected || (otherSelected && root.active && spreadView.selectedApplication && spreadView.selectedApplication.stage !== stage)) {
158− }
159−
160− if (isSelected) {
161− if (model.stage == ApplicationInfoInterface.MainStage) {
160+ if (stage == ApplicationInfoInterface.MainStage) {
162 return linearAnimation(selectedProgress, negativeProgress, selectedXTranslate, -spreadView.width, root.progress);
161 return linearAnimation(selectedProgress, negativeProgress, selectedXTranslate, -spreadView.width, root.progress);
163 } else {
162 } else {
164 return linearAnimation(selectedProgress, negativeProgress, selectedXTranslate, -spreadView.sideStageWidth, root.progress);
163 return linearAnimation(selectedProgress, negativeProgress, selectedXTranslate, -spreadView.sideStageWidth, root.progress);
165 }
164 }
165+ } else if (otherSelected) {
166+ return selectedXTranslate;
166 }
167 }
167
168
168 // The tile should move a bit to the left if a new one comes on top of it, but not for the Side Stage and not
169 // The tile should move a bit to the left if a new one comes on top of it, but not for the Side Stage and not
169 // when we're only dragging the side stage in on top of a main stage app
170 // when we're only dragging the side stage in on top of a main stage app
170− var shouldMoveAway = spreadView.nextInStack >= 0 && priv.movedActive && model.stage === ApplicationInfoInterface.MainStage &&
171+ var shouldMoveAway = spreadView.nextInStack >= 0 && priv.movedActive && stage === ApplicationInfoInterface.MainStage &&
171 ApplicationManager.get(spreadView.nextInStack).stage === ApplicationInfoInterface.MainStage;
172 ApplicationManager.get(spreadView.nextInStack).stage === ApplicationInfoInterface.MainStage;
172
173
173 if (active) {
174 if (active) {
@@ -177,36 +178,45 @@ SpreadDelegate {
177 newTranslate += linearAnimation(0, spreadView.positionMarker2, 0, -units.gu(4), root.animatedProgress);
178 newTranslate += linearAnimation(0, spreadView.positionMarker2, 0, -units.gu(4), root.animatedProgress);
178 }
179 }
179 newTranslate += root.dragOffset;
180 newTranslate += root.dragOffset;
180− }
181− if (!spreadView.active && model.appId == "unity8-dash" && !root.active) {
181+ } else if (!spreadView.active && model.appId == "unity8-dash") {
182 newTranslate -= root.width;
182 newTranslate -= root.width;
183 }
183 }
184
184
185+ if (!spreadView.active) {
186+ return newTranslate;
187+ }
188+
189+ var shadowOffset = units.gu(2);
190+
185− if (nextInStack && spreadView.phase == 0) {
191+ if (spreadView.phase == 0) {
192+ if (nextInStack) {
186− if (model.stage == ApplicationInfoInterface.MainStage) {
193+ if (stage == ApplicationInfoInterface.MainStage) {
187− if (spreadView.sideStageVisible && root.progress > 0) {
194+ if (spreadView.sideStageVisible && root.progress > 0) {
188− // Move it so it appears from behind the side stage immediately
195+ // Move it so it appears from behind the side stage immediately
189− newTranslate += -spreadView.sideStageWidth;
196+ newTranslate += -spreadView.sideStageWidth;
197+ }
190 }
198 }
191− }
192
199
193− if (model.stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
200+ if (stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
194− // This is when we only drag the side stage in, without rotation or snapping
201+ // This is when we only drag the side stage in, without rotation or snapping
195− newTranslate = linearAnimation(0, spreadView.positionMarker2, 0, -spreadView.sideStageWidth, root.progress);
202+ newTranslate = linearAnimation(0, spreadView.positionMarker2, 0, -spreadView.sideStageWidth, root.animatedProgress);
196− } else {
203+ } else {
197− newTranslate += linearAnimation(0, spreadView.positionMarker2, 0, -spreadView.sideStageWidth * spreadView.snapPosition, root.animatedProgress);
204+ newTranslate += linearAnimation(0, spreadView.positionMarker2, 0, -spreadView.sideStageWidth * spreadView.snapPosition, root.animatedProgress);
205+ }
206+ } else if (!root.active) {
207+ newTranslate += shadowOffset;
198 }
208 }
199 }
209 }
200
210
201 if (spreadView.phase == 1) {
211 if (spreadView.phase == 1) {
202 if (nextInStack) {
212 if (nextInStack) {
203− if (model.stage == ApplicationInfoInterface.MainStage) {
213+ if (stage == ApplicationInfoInterface.MainStage) {
204 var startValue = -spreadView.sideStageWidth * spreadView.snapPosition + (spreadView.sideStageVisible ? -spreadView.sideStageWidth : 0);
214 var startValue = -spreadView.sideStageWidth * spreadView.snapPosition + (spreadView.sideStageVisible ? -spreadView.sideStageWidth : 0);
205 newTranslate += linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, startValue, priv.phase2StartTranslate, root.animatedProgress);
215 newTranslate += linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, startValue, priv.phase2StartTranslate, root.animatedProgress);
206 } else {
216 } else {
207 var endValue = -spreadView.width + spreadView.width * root.zIndex / 6;
217 var endValue = -spreadView.width + spreadView.width * root.zIndex / 6;
208 if (!spreadView.sideStageVisible) {
218 if (!spreadView.sideStageVisible) {
209− newTranslate += linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, -spreadView.sideStageWidth, priv.phase2StartTranslate, root.progress);
219+ newTranslate += linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, -spreadView.sideStageWidth, priv.phase2StartTranslate, root.animatedProgress);
210 } else {
220 } else {
211 newTranslate += linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, -spreadView.sideStageWidth * spreadView.snapPosition, priv.phase2StartTranslate, root.animatedProgress);
221 newTranslate += linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, -spreadView.sideStageWidth * spreadView.snapPosition, priv.phase2StartTranslate, root.animatedProgress);
212 }
222 }
@@ -215,16 +225,20 @@ SpreadDelegate {
215 var startProgress = spreadView.positionMarker2 - (zIndex * spreadView.positionMarker2 / 2);
225 var startProgress = spreadView.positionMarker2 - (zIndex * spreadView.positionMarker2 / 2);
216 var endProgress = spreadView.positionMarker4 - (zIndex * spreadView.tileDistance / spreadView.width);
226 var endProgress = spreadView.positionMarker4 - (zIndex * spreadView.tileDistance / spreadView.width);
217 var startTranslate = -root.width + (shouldMoveAway ? -units.gu(4) : 0);
227 var startTranslate = -root.width + (shouldMoveAway ? -units.gu(4) : 0);
218− newTranslate = linearAnimation(startProgress, endProgress, startTranslate, priv.phase2StartTranslate, root.progress);
228+ newTranslate = linearAnimation(startProgress, endProgress, startTranslate, priv.phase2StartTranslate, root.animatedProgress);
219 } else {
229 } else {
220 var startProgress = spreadView.positionMarker2 - (zIndex * spreadView.positionMarker2 / 2);
230 var startProgress = spreadView.positionMarker2 - (zIndex * spreadView.positionMarker2 / 2);
221 var endProgress = spreadView.positionMarker4 - (zIndex * spreadView.tileDistance / spreadView.width);
231 var endProgress = spreadView.positionMarker4 - (zIndex * spreadView.tileDistance / spreadView.width);
222− newTranslate = linearAnimation(startProgress, endProgress, 0, priv.phase2StartTranslate, root.progress);
232+ newTranslate = linearAnimation(startProgress, endProgress, shadowOffset * Math.max(0, zIndex-2), priv.phase2StartTranslate, root.animatedProgress);
223 }
233 }
224 }
234 }
225
235
226 if (spreadView.phase == 2) {
236 if (spreadView.phase == 2) {
237+ if (easingCurve.value == 0 && !nextInStack) {
238+ newTranslate = shadowOffset;
239+ } else {
227− newTranslate = -easingCurve.value * (spreadView.width - root.zIndex * animatedEndDistance);
240+ newTranslate = -easingCurve.value * (spreadView.width - root.zIndex * animatedEndDistance);
241+ }
228 }
242 }
229
243
230 return newTranslate;
244 return newTranslate;
@@ -235,33 +249,30 @@ SpreadDelegate {
235 return 1;
249 return 1;
236 }
250 }
237
251
238− if (otherSelected) {
252+ // selected app or opposite stage active app.
239− return selectedScale;
253+ if (isSelected || (otherSelected && root.active && spreadView.selectedApplication && spreadView.selectedApplication.stage !== stage)) {
240− }
241−
242− if (isSelected) {
243 return linearAnimation(selectedProgress, negativeProgress, selectedScale, 1, root.progress);
254 return linearAnimation(selectedProgress, negativeProgress, selectedScale, 1, root.progress);
255+ } else if (otherSelected) {
256+ return selectedScale;
244 }
257 }
245
258
246 if (spreadView.phase == 0) {
259 if (spreadView.phase == 0) {
247 if (nextInStack) {
260 if (nextInStack) {
248− if (model.stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
261+ if (stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
249 return 1;
262 return 1;
250 } else {
263 } else {
251 var targetScale = root.dragStartScale - ((root.dragStartScale - 1) * spreadView.snapPosition);
264 var targetScale = root.dragStartScale - ((root.dragStartScale - 1) * spreadView.snapPosition);
252 return linearAnimation(0, spreadView.positionMarker2, root.dragStartScale, targetScale, root.animatedProgress);
265 return linearAnimation(0, spreadView.positionMarker2, root.dragStartScale, targetScale, root.animatedProgress);
253 }
266 }
254− } else if (active) {
255− return 1;
256 } else {
267 } else {
257− return linearAnimation(0, spreadView.positionMarker2, root.startScale, root.endScale, root.progress);
268+ return 1;
258 }
269 }
259 }
270 }
260
271
261 if (spreadView.phase == 1) {
272 if (spreadView.phase == 1) {
262 if (nextInStack) {
273 if (nextInStack) {
263 var startScale = 1;
274 var startScale = 1;
264− if (model.stage !== ApplicationInfoInterface.SideStage || spreadView.sideStageVisible) {
275+ if (stage !== ApplicationInfoInterface.SideStage || spreadView.sideStageVisible) {
265 startScale = root.dragStartScale - ((root.dragStartScale - 1) * spreadView.snapPosition);
276 startScale = root.dragStartScale - ((root.dragStartScale - 1) * spreadView.snapPosition);
266 }
277 }
267 return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, startScale, priv.phase2StartScale, root.animatedProgress);
278 return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, startScale, priv.phase2StartScale, root.animatedProgress);
@@ -283,21 +294,22 @@ SpreadDelegate {
283 return 0;
294 return 0;
284 }
295 }
285
296
286− if (otherSelected) {
297+ // selected app or opposite stage active app.
287− return selectedAngle;
298+ if (isSelected || (otherSelected && root.active && spreadView.selectedApplication && spreadView.selectedApplication.stage !== stage)) {
288− }
289− if (isSelected) {
290 return linearAnimation(selectedProgress, negativeProgress, selectedAngle, 0, root.progress);
299 return linearAnimation(selectedProgress, negativeProgress, selectedAngle, 0, root.progress);
300+ } else if (otherSelected) {
301+ return selectedAngle;
291 }
302 }
292
303
293 // The tile should rotate a bit when another one comes on top, but not when only dragging the side stage in
304 // The tile should rotate a bit when another one comes on top, but not when only dragging the side stage in
294− var shouldMoveAway = spreadView.nextInStack >= 0 && movedActive &&
305+ var shouldMoveAway = spreadView.nextInStack == -1 ||
306+ spreadView.nextInStack >= 0 && priv.movedActive &&
295 (ApplicationManager.get(spreadView.nextInStack).stage === ApplicationInfoInterface.MainStage ||
307 (ApplicationManager.get(spreadView.nextInStack).stage === ApplicationInfoInterface.MainStage ||
296− model.stage == ApplicationInfoInterface.SideStage);
308+ stage == ApplicationInfoInterface.SideStage);
297
309
298 if (spreadView.phase == 0) {
310 if (spreadView.phase == 0) {
299 if (nextInStack) {
311 if (nextInStack) {
300− if (model.stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
312+ if (stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
301 return 0;
313 return 0;
302 } else {
314 } else {
303 return linearAnimation(0, spreadView.positionMarker2, root.startAngle, root.startAngle * (1-spreadView.snapPosition), root.animatedProgress);
315 return linearAnimation(0, spreadView.positionMarker2, root.startAngle, root.startAngle * (1-spreadView.snapPosition), root.animatedProgress);
@@ -309,7 +321,7 @@ SpreadDelegate {
309 }
321 }
310 if (spreadView.phase == 1) {
322 if (spreadView.phase == 1) {
311 if (nextInStack) {
323 if (nextInStack) {
312− if (model.stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
324+ if (stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
313 return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, 0, priv.phase2StartAngle, root.animatedProgress);
325 return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, 0, priv.phase2StartAngle, root.animatedProgress);
314 } else {
326 } else {
315 return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, root.startAngle * (1-spreadView.snapPosition), priv.phase2StartAngle, root.animatedProgress);
327 return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, root.startAngle * (1-spreadView.snapPosition), priv.phase2StartAngle, root.animatedProgress);
@@ -328,31 +340,47 @@ SpreadDelegate {
328 }
340 }
329
341
330 property real opacityTransform: {
342 property real opacityTransform: {
343+ // animate opacity for items not snapping into view.
331− if (otherSelected && spreadView.phase == 2) {
344+ if (spreadView.phase !== 2) return 1;
345+ if (root.isSelected) return 1;
346+
347+ if (otherSelected) {
348+ if (root.active && spreadView.selectedApplication && spreadView.selectedApplication.stage !== stage) {
349+ return 1;
350+ }
332 return linearAnimation(selectedProgress, negativeProgress, selectedOpacity, 0, root.progress);
351 return linearAnimation(selectedProgress, negativeProgress, selectedOpacity, 0, root.progress);
333 }
352 }
334−
335 return 1;
353 return 1;
336 }
354 }
337
355
338 property real topMarginProgress: {
356 property real topMarginProgress: {
339− if (priv.isSelected) {
357+ // selected app or opposite stage active app.
358+ if (isSelected || (otherSelected && root.active && spreadView.selectedApplication && spreadView.selectedApplication.stage !== stage)) {
340 return linearAnimation(selectedProgress, negativeProgress, selectedTopMarginProgress, 0, root.progress);
359 return linearAnimation(selectedProgress, negativeProgress, selectedTopMarginProgress, 0, root.progress);
341 }
360 }
361+
342− switch (spreadView.phase) {
362+ if (spreadView.phase == 0) {
343− case 0:
344 return 0;
363 return 0;
345− case 1:
364+ } else if (spreadView.phase == 1) {
365+ if (nextInStack) {
346− return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4,
366+ return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4,
347− 0, priv.phase2StartTopMarginProgress, root.progress);
367+ 0, 1, root.progress);
368+ }
369+ var startProgress = spreadView.positionMarker2 - (zIndex * spreadView.positionMarker2 / 2);
370+ var endProgress = spreadView.positionMarker4 - (zIndex * spreadView.tileDistance / spreadView.width);
371+ return linearAnimation(startProgress, endProgress, 0, 1, root.progress);
348 }
372 }
349 return 1;
373 return 1;
350 }
374 }
351
375
352 states: [
376 states: [
353 State {
377 State {
354− name: "sideStageDragging"; when: spreadView.sideStageDragging && root.isInSideStage
378+ name: "sideStage";
379+ when: root.isInSideStage && spreadView.shiftedContentX == 0 && spreadView.phase == 0
380+ PropertyChanges {
381+ target: priv;
355− PropertyChanges { target: priv; xTranslate: -spreadView.sideStageWidth + spreadView.sideStageWidth * spreadView.sideStageDragProgress }
382+ xTranslate: -spreadView.sideStageWidth + spreadView.sideStageWidth * (1-spreadView.sideStageDragProgress)
383+ }
356 }
384 }
357 ]
385 ]
358 }
386 }
qml/Stages/graphics/sidestage_drag.svgnew file+207
@@ -0,0 +1,207 @@
1+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2+<!-- Created with Inkscape (http://www.inkscape.org/) -->
3+
4+<svg
5+ xmlns:dc="http://purl.org/dc/elements/1.1/"
6+ xmlns:cc="http://creativecommons.org/ns#"
7+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
8+ xmlns:svg="http://www.w3.org/2000/svg"
9+ xmlns="http://www.w3.org/2000/svg"
10+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
11+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
12+ width="540"
13+ height="324"
14+ id="svg4874"
15+ version="1.1"
16+ inkscape:version="0.91+devel r"
17+ viewBox="0 0 540 324"
18+ sodipodi:docname="gesture_3f_touch.svg">
19+ <defs
20+ id="defs4876" />
21+ <sodipodi:namedview
22+ id="base"
23+ pagecolor="#292929"
24+ bordercolor="#666666"
25+ borderopacity="1.0"
26+ inkscape:pageopacity="1"
27+ inkscape:pageshadow="2"
28+ inkscape:zoom="1.1785993"
29+ inkscape:cx="-19.084613"
30+ inkscape:cy="11.707485"
31+ inkscape:document-units="px"
32+ inkscape:current-layer="g4780"
33+ showgrid="false"
34+ showborder="true"
35+ fit-margin-top="0"
36+ fit-margin-left="0"
37+ fit-margin-right="0"
38+ fit-margin-bottom="0"
39+ inkscape:snap-bbox="true"
40+ inkscape:bbox-paths="true"
41+ inkscape:bbox-nodes="true"
42+ inkscape:snap-bbox-edge-midpoints="true"
43+ inkscape:snap-bbox-midpoints="true"
44+ inkscape:object-paths="true"
45+ inkscape:snap-intersection-paths="true"
46+ inkscape:object-nodes="true"
47+ inkscape:snap-smooth-nodes="true"
48+ inkscape:snap-midpoints="true"
49+ inkscape:snap-object-midpoints="true"
50+ inkscape:snap-center="true"
51+ showguides="false"
52+ inkscape:guide-bbox="true"
53+ inkscape:snap-global="true"
54+ inkscape:snap-others="false"
55+ inkscape:snap-page="true">
56+ <inkscape:grid
57+ type="xygrid"
58+ id="grid5451"
59+ empspacing="8"
60+ originx="-2.7715014e-06"
61+ originy="-2.5124622e-06" />
62+ <sodipodi:guide
63+ orientation="1,0"
64+ position="7.9999972,-8.0000025"
65+ id="guide4063"
66+ inkscape:locked="false" />
67+ <sodipodi:guide
68+ orientation="1,0"
69+ position="3.9999972,-8.0000025"
70+ id="guide4065"
71+ inkscape:locked="false" />
72+ <sodipodi:guide
73+ orientation="0,1"
74+ position="-8.0000027,87.999998"
75+ id="guide4067"
76+ inkscape:locked="false" />
77+ <sodipodi:guide
78+ orientation="0,1"
79+ position="-8.0000027,91.999998"
80+ id="guide4069"
81+ inkscape:locked="false" />
82+ <sodipodi:guide
83+ orientation="0,1"
84+ position="104,3.9999975"
85+ id="guide4071"
86+ inkscape:locked="false" />
87+ <sodipodi:guide
88+ orientation="0,1"
89+ position="-5.0000027,7.9999975"
90+ id="guide4073"
91+ inkscape:locked="false" />
92+ <sodipodi:guide
93+ orientation="1,0"
94+ position="91.999996,-8.0000025"
95+ id="guide4075"
96+ inkscape:locked="false" />
97+ <sodipodi:guide
98+ orientation="1,0"
99+ position="87.999997,-8.0000025"
100+ id="guide4077"
101+ inkscape:locked="false" />
102+ <sodipodi:guide
103+ orientation="0,1"
104+ position="-8.0000027,83.999998"
105+ id="guide4074"
106+ inkscape:locked="false" />
107+ <sodipodi:guide
108+ orientation="1,0"
109+ position="11.999997,-8.0000025"
110+ id="guide4076"
111+ inkscape:locked="false" />
112+ <sodipodi:guide
113+ orientation="0,1"
114+ position="-5.0000027,11.999997"
115+ id="guide4078"
116+ inkscape:locked="false" />
117+ <sodipodi:guide
118+ orientation="1,0"
119+ position="83.999997,-9.0000025"
120+ id="guide4080"
121+ inkscape:locked="false" />
122+ <sodipodi:guide
123+ position="47.999997,-8.0000025"
124+ orientation="1,0"
125+ id="guide4170"
126+ inkscape:locked="false" />
127+ <sodipodi:guide
128+ position="-8.0000027,47.999997"
129+ orientation="0,1"
130+ id="guide4172"
131+ inkscape:locked="false" />
132+ </sodipodi:namedview>
133+ <metadata
134+ id="metadata4879">
135+ <rdf:RDF>
136+ <cc:Work
137+ rdf:about="">
138+ <dc:format>image/svg+xml</dc:format>
139+ <dc:type
140+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
141+ <dc:title></dc:title>
142+ </cc:Work>
143+ </rdf:RDF>
144+ </metadata>
145+ <g
146+ inkscape:label="Layer 1"
147+ inkscape:groupmode="layer"
148+ id="layer1"
149+ transform="translate(67.857143,149.49498)">
150+ <g
151+ transform="matrix(0,-1,-1,0,373.50506,516.50504)"
152+ id="g4845"
153+ style="display:inline">
154+ <g
155+ inkscape:export-ydpi="90"
156+ inkscape:export-xdpi="90"
157+ inkscape:export-filename="next01.png"
158+ transform="matrix(-0.9996045,0,0,1,575.94296,-611.00001)"
159+ id="g4778"
160+ inkscape:label="Layer 1">
161+ <g
162+ transform="matrix(-1,0,0,1,575.99999,611)"
163+ id="g4780"
164+ style="display:inline">
165+ <path
166+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:53.6204567px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:end;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:end;display:inline;fill:none;fill-opacity:1;stroke:#f7f7f7;stroke-width:4.00079107;stroke-miterlimit:4;stroke-dasharray:none"
167+ d="m 526.90578,343.72628 c 14.3803,3.85163 8.37809,26.10701 -5.96811,22.26448 l -10e-6,10e-6 -50.91437,-13.63709 67.92084,18.21108 c 15.93089,4.26693 9.28148,28.92202 -6.61162,24.66516 l -10e-6,1e-5 -67.93497,-18.15834 58.38757,15.63874 c 15.85637,4.24702 9.23761,28.78845 -6.58115,24.5515 v -2e-5 l 1e-5,-2e-5 -71.03537,-19.00646 -33.09607,-8.85524 -0.18781,-0.0503 v 0 l -6.91408,-1.85189 18.07353,23.47068 c 8.45364,10.48525 11.9521,17.66446 -3.54852,27.89697 l -43.91145,-37.31132 c -28.38683,-22.97811 -35.17793,-41.86133 -27.95127,-70.42985 10.25002,-40.5206 40.5632,-49.89857 76.24862,-40.34047 4.5224,1.21129 36.24839,9.63672 40.29164,10.71968 14.0412,3.76084 12.54552,13.74563 6.76936,26.96549 z"
168+ id="path4845-6"
169+ inkscape:connector-curvature="0"
170+ sodipodi:nodetypes="ccccccccccccccccccccssscc" />
171+ <path
172+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.25;fill:#f7f7f7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.51758671;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
173+ d="m 571.27587,383.27233 a 35.064884,35.05101 0 0 1 -16.02978,29.39649 35.064884,35.05101 0 0 1 -34.38665,28.3418 35.064884,35.05101 0 0 1 -34.85362,-31.56055 l 29.19904,7.8125 c 15.81876,4.23695 22.4371,-20.30571 6.58073,-24.55274 l -28.91769,-7.74609 a 35.064884,35.05101 0 0 1 0.01,-0.0137 l 38.45466,10.2793 c 15.8931,4.25686 22.54288,-20.39714 6.61199,-24.66407 l -48.41758,-12.98242 a 35.064884,35.05101 0 0 1 0,-0.006 l 31.41086,8.41407 c 14.3462,3.84253 20.34945,-18.414 5.96915,-22.26563 l -31.70199,-8.49023 a 35.064884,35.05101 0 0 1 29.21468,-15.70704 35.064884,35.05101 0 0 1 35.06465,35.05274 35.064884,35.05101 0 0 1 -0.0879,2.45312 35.064884,35.05101 0 0 1 11.8797,26.23828 z"
174+ id="ellipse4239-1"
175+ inkscape:connector-curvature="0" />
176+ <g
177+ style="display:inline"
178+ id="g5379"
179+ transform="matrix(-0.99999722,0,0,-0.99999722,645.25394,919.46958)">
180+ <path
181+ style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:#f7f7f7;stroke-width:4.00079107;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
182+ id="path4116"
183+ sodipodi:type="arc"
184+ sodipodi:cx="808.72699"
185+ sodipodi:cy="244.79436"
186+ sodipodi:rx="250.36082"
187+ sodipodi:ry="263.63275"
188+ sodipodi:start="3.8048178"
189+ sodipodi:end="5.6286868"
190+ sodipodi:open="true"
191+ d="M 611.43997,82.485822 A 250.36082,263.63275 0 0 1 809.81939,-18.835885 250.36082,263.63275 0 0 1 1007.3516,84.304899"
192+ inkscape:transform-center-x="-17.044876"
193+ inkscape:transform-center-y="-0.0015034578"
194+ transform="matrix(0,1,1,0,0,0)" />
195+ <path
196+ inkscape:transform-center-y="4.9738279"
197+ inkscape:transform-center-x="2.0443183"
198+ inkscape:connector-curvature="0"
199+ id="path4198"
200+ d="m 73.559638,1014.0226 14.78221,-18.91287 c 0,0 6.80763,12.50667 9.95238,23.00047 -10.9469,-0.5114 -24.73459,-4.0876 -24.73459,-4.0876 z"
201+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#f7f7f7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.00059342;marker:none;enable-background:accumulate" />
202+ </g>
203+ </g>
204+ </g>
205+ </g>
206+ </g>
207+</svg>
qml/Stages/graphics/sidestage_open.svgnew file+181
@@ -0,0 +1,181 @@
1+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2+<!-- Created with Inkscape (http://www.inkscape.org/) -->
3+
4+<svg
5+ xmlns:dc="http://purl.org/dc/elements/1.1/"
6+ xmlns:cc="http://creativecommons.org/ns#"
7+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
8+ xmlns:svg="http://www.w3.org/2000/svg"
9+ xmlns="http://www.w3.org/2000/svg"
10+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
11+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
12+ width="162"
13+ height="230"
14+ id="svg4874"
15+ version="1.1"
16+ inkscape:version="0.91+devel r"
17+ viewBox="0 0 162 230"
18+ sodipodi:docname="gesture_3f_tap.svg"
19+ style="shape-rendering:crispEdges">
20+ <defs
21+ id="defs4876" />
22+ <sodipodi:namedview
23+ id="base"
24+ pagecolor="#292929"
25+ bordercolor="#666666"
26+ borderopacity="1.0"
27+ inkscape:pageopacity="1"
28+ inkscape:pageshadow="2"
29+ inkscape:zoom="1.8415614"
30+ inkscape:cx="162.13161"
31+ inkscape:cy="116.01485"
32+ inkscape:document-units="px"
33+ inkscape:current-layer="g4780"
34+ showgrid="false"
35+ showborder="true"
36+ fit-margin-top="0"
37+ fit-margin-left="0"
38+ fit-margin-right="0"
39+ fit-margin-bottom="0"
40+ inkscape:snap-bbox="true"
41+ inkscape:bbox-paths="true"
42+ inkscape:bbox-nodes="true"
43+ inkscape:snap-bbox-edge-midpoints="true"
44+ inkscape:snap-bbox-midpoints="true"
45+ inkscape:object-paths="true"
46+ inkscape:snap-intersection-paths="true"
47+ inkscape:object-nodes="true"
48+ inkscape:snap-smooth-nodes="true"
49+ inkscape:snap-midpoints="true"
50+ inkscape:snap-object-midpoints="true"
51+ inkscape:snap-center="true"
52+ showguides="false"
53+ inkscape:guide-bbox="true"
54+ inkscape:snap-global="false"
55+ inkscape:snap-others="false"
56+ inkscape:snap-page="true">
57+ <inkscape:grid
58+ type="xygrid"
59+ id="grid5451"
60+ empspacing="8"
61+ originx="-2.7715014e-06"
62+ originy="-2.5124622e-06" />
63+ <sodipodi:guide
64+ orientation="1,0"
65+ position="7.9999972,-8.0000026"
66+ id="guide4063"
67+ inkscape:locked="false" />
68+ <sodipodi:guide
69+ orientation="1,0"
70+ position="3.9999972,-8.0000026"
71+ id="guide4065"
72+ inkscape:locked="false" />
73+ <sodipodi:guide
74+ orientation="0,1"
75+ position="-8.0000027,87.999998"
76+ id="guide4067"
77+ inkscape:locked="false" />
78+ <sodipodi:guide
79+ orientation="0,1"
80+ position="-8.0000027,91.999998"
81+ id="guide4069"
82+ inkscape:locked="false" />
83+ <sodipodi:guide
84+ orientation="0,1"
85+ position="104,3.9999975"
86+ id="guide4071"
87+ inkscape:locked="false" />
88+ <sodipodi:guide
89+ orientation="0,1"
90+ position="-5.0000027,7.9999975"
91+ id="guide4073"
92+ inkscape:locked="false" />
93+ <sodipodi:guide
94+ orientation="1,0"
95+ position="91.999997,-8.0000026"
96+ id="guide4075"
97+ inkscape:locked="false" />
98+ <sodipodi:guide
99+ orientation="1,0"
100+ position="87.999997,-8.0000026"
101+ id="guide4077"
102+ inkscape:locked="false" />
103+ <sodipodi:guide
104+ orientation="0,1"
105+ position="-8.0000027,83.999998"
106+ id="guide4074"
107+ inkscape:locked="false" />
108+ <sodipodi:guide
109+ orientation="1,0"
110+ position="11.999997,-8.0000026"
111+ id="guide4076"
112+ inkscape:locked="false" />
113+ <sodipodi:guide
114+ orientation="0,1"
115+ position="-5.0000027,11.999997"
116+ id="guide4078"
117+ inkscape:locked="false" />
118+ <sodipodi:guide
119+ orientation="1,0"
120+ position="83.999997,-9.0000026"
121+ id="guide4080"
122+ inkscape:locked="false" />
123+ <sodipodi:guide
124+ position="47.999997,-8.0000026"
125+ orientation="1,0"
126+ id="guide4170"
127+ inkscape:locked="false" />
128+ <sodipodi:guide
129+ position="-8.0000027,47.999997"
130+ orientation="0,1"
131+ id="guide4172"
132+ inkscape:locked="false" />
133+ </sodipodi:namedview>
134+ <metadata
135+ id="metadata4879">
136+ <rdf:RDF>
137+ <cc:Work
138+ rdf:about="">
139+ <dc:format>image/svg+xml</dc:format>
140+ <dc:type
141+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
142+ <dc:title></dc:title>
143+ </cc:Work>
144+ </rdf:RDF>
145+ </metadata>
146+ <g
147+ inkscape:label="Layer 1"
148+ inkscape:groupmode="layer"
149+ id="layer1"
150+ transform="translate(67.857143,55.494966)">
151+ <g
152+ transform="matrix(0,-1,-1,0,373.50506,516.50504)"
153+ id="g4845"
154+ style="display:inline">
155+ <g
156+ inkscape:export-ydpi="90"
157+ inkscape:export-xdpi="90"
158+ inkscape:export-filename="next01.png"
159+ transform="matrix(-0.9996045,0,0,1,575.94296,-611.00001)"
160+ id="g4778"
161+ inkscape:label="Layer 1">
162+ <g
163+ transform="matrix(-1,0,0,1,575.99999,611)"
164+ id="g4780"
165+ style="display:inline">
166+ <path
167+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:53.6204567px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:end;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:end;display:inline;fill:none;fill-opacity:1;stroke:#f7f7f7;stroke-width:4.00079107;stroke-miterlimit:4;stroke-dasharray:none"
168+ d="m 526.90578,343.72628 c 14.3803,3.85163 8.37809,26.10701 -5.96811,22.26448 l -10e-6,10e-6 -50.91437,-13.63709 67.92084,18.21108 c 15.93089,4.26693 9.28148,28.92202 -6.61162,24.66516 l -10e-6,1e-5 -67.93497,-18.15834 58.38757,15.63874 c 15.85637,4.24702 9.23761,28.78845 -6.58115,24.5515 v -2e-5 l 1e-5,-2e-5 -71.03537,-19.00646 -33.09607,-8.85524 -0.18781,-0.0503 v 0 l -6.91408,-1.85189 18.07353,23.47068 c 8.45364,10.48525 11.9521,17.66446 -3.54852,27.89697 l -43.91145,-37.31132 c -28.38683,-22.97811 -35.17793,-41.86133 -27.95127,-70.42985 10.25002,-40.5206 40.5632,-49.89857 76.24862,-40.34047 4.5224,1.21129 36.24839,9.63672 40.29164,10.71968 14.0412,3.76084 12.54552,13.74563 6.76936,26.96549 z"
169+ id="path4845-6"
170+ inkscape:connector-curvature="0"
171+ sodipodi:nodetypes="ccccccccccccccccccccssscc" />
172+ <path
173+ style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.5;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#f7f7f7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.99999976;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
174+ d="m 571.45563,383.2743 c -0.0175,12.04505 -6.22338,23.19765 -16.35804,29.64062 -3.3052,16.24493 -17.57029,28.04101 -34.23815,28.04493 -18.13447,4.3e-4 -33.08059,-13.83538 -34.83995,-31.50586 l 4.16766,1.11523 c 2.22233,14.95164 15.08799,26.39099 30.67229,26.39063 15.06147,-0.004 27.92054,-10.79228 30.53552,-25.61915 l 0.15631,-0.89648 0.78156,-0.46875 c 9.37156,-5.64255 15.10396,-15.76543 15.12121,-26.70117 v -0.004 c -0.0108,-9.30873 -4.17362,-18.12357 -11.35605,-24.04883 l -0.8265,-0.68359 0.10942,-1.06641 c 0.10013,-0.96319 0.1562,-1.9301 0.16608,-2.89843 -0.004,-17.20453 -13.91519,-31.10752 -31.12755,-31.10743 -10.37812,-5e-5 -19.55027,5.05891 -25.20333,12.84375 l -4.0856,-1.09375 c 6.29481,-9.48451 17.06827,-15.75006 29.28893,-15.75 19.37764,-1e-4 35.12924,15.74527 35.12913,35.11524 v 0.01 0.01 c -0.008,0.77822 -0.0716,1.55426 -0.13091,2.33008 7.57765,6.66645 12.02745,16.21382 12.03797,26.33984 v 0.002 z m -74.3634,2.80273 c -0.004,0.005 -0.008,0.0106 -0.0117,0.0156 l -4.15595,-1.11328 c 0.003,-0.004 0.006,-0.009 0.01,-0.0137 z m -3.51507,-27.4082 -4.14421,-1.10938 c -1.8e-4,-0.002 1.7e-4,-0.004 0,-0.006 l 4.14226,1.10937 c 2.7e-4,0.002 0.002,0.004 0.002,0.006 z"
175+ id="ellipse4488"
176+ inkscape:connector-curvature="0" />
177+ </g>
178+ </g>
179+ </g>
180+ </g>
181+</svg>
src/MouseTouchAdaptor.cpp+53−9
@@ -71,6 +71,8 @@ using QTest::QTouchEventSequence;
71namespace {
71namespace {
72MouseTouchAdaptor *g_instance = nullptr;
72MouseTouchAdaptor *g_instance = nullptr;
73
73
74+const Qt::KeyboardModifiers TRI_PRESS_MODIFIER = Qt::ShiftModifier|Qt::ControlModifier|Qt::AltModifier;
75+
74Qt::MouseButton translateMouseButton(xcb_button_t detail)
76Qt::MouseButton translateMouseButton(xcb_button_t detail)
75{
77{
76 switch (detail) {
78 switch (detail) {
@@ -81,10 +83,25 @@ Qt::MouseButton translateMouseButton(xcb_button_t detail)
81 default: return Qt::NoButton;
83 default: return Qt::NoButton;
82 }
84 }
83}
85}
86+
87+Qt::KeyboardModifiers translateMofidier(uint32_t mod)
88+{
89+ Qt::KeyboardModifiers qtMod = Qt::NoModifier;
90+
91+ if (mod & 0x01) qtMod |= Qt::ShiftModifier;
92+ if (mod & 0x04) qtMod |= Qt::ControlModifier;
93+ if (mod & 0x08) qtMod |= Qt::AltModifier;
94+ if (mod & 0x80) qtMod |= Qt::MetaModifier;
95+
96+ return qtMod;
97+}
84} // end of anonymous namespace
98} // end of anonymous namespace
85
99
86MouseTouchAdaptor::MouseTouchAdaptor()
100MouseTouchAdaptor::MouseTouchAdaptor()
101+ : QObject(nullptr)
87− : QObject(nullptr), m_leftButtonIsPressed(false), m_enabled(true)
102+ , m_leftButtonIsPressed(false)
103+ , m_triPressModifier(false)
104+ , m_enabled(true)
88{
105{
89 QCoreApplication::instance()->installNativeEventFilter(this);
106 QCoreApplication::instance()->installNativeEventFilter(this);
90
107
@@ -192,17 +209,20 @@ bool MouseTouchAdaptor::xi2HandleEvent(xcb_ge_event_t *event)
192 return handleButtonPress(
209 return handleButtonPress(
193 static_cast<WId>(xiDeviceEvent->event),
210 static_cast<WId>(xiDeviceEvent->event),
194 xiDeviceEvent->detail,
211 xiDeviceEvent->detail,
212+ xiDeviceEvent->mods.base_mods,
195 fixed1616ToReal(xiDeviceEvent->event_x),
213 fixed1616ToReal(xiDeviceEvent->event_x),
196 fixed1616ToReal(xiDeviceEvent->event_y));
214 fixed1616ToReal(xiDeviceEvent->event_y));
197 case XI_ButtonRelease:
215 case XI_ButtonRelease:
198 return handleButtonRelease(
216 return handleButtonRelease(
199 static_cast<WId>(xiDeviceEvent->event),
217 static_cast<WId>(xiDeviceEvent->event),
200 xiDeviceEvent->detail,
218 xiDeviceEvent->detail,
219+ xiDeviceEvent->mods.base_mods,
201 fixed1616ToReal(xiDeviceEvent->event_x),
220 fixed1616ToReal(xiDeviceEvent->event_x),
202 fixed1616ToReal(xiDeviceEvent->event_y));
221 fixed1616ToReal(xiDeviceEvent->event_y));
203 case XI_Motion:
222 case XI_Motion:
204 return handleMotionNotify(
223 return handleMotionNotify(
205 static_cast<WId>(xiDeviceEvent->event),
224 static_cast<WId>(xiDeviceEvent->event),
225+ xiDeviceEvent->mods.base_mods,
206 fixed1616ToReal(xiDeviceEvent->event_x),
226 fixed1616ToReal(xiDeviceEvent->event_x),
207 fixed1616ToReal(xiDeviceEvent->event_y));
227 fixed1616ToReal(xiDeviceEvent->event_y));
208 return true;
228 return true;
@@ -232,17 +252,18 @@ bool MouseTouchAdaptor::nativeEventFilter(const QByteArray & eventType,
232 switch (xcbEvent->response_type & ~0x80) {
252 switch (xcbEvent->response_type & ~0x80) {
233 case XCB_BUTTON_PRESS: {
253 case XCB_BUTTON_PRESS: {
234 auto pressEvent = reinterpret_cast<xcb_button_press_event_t *>(xcbEvent);
254 auto pressEvent = reinterpret_cast<xcb_button_press_event_t *>(xcbEvent);
235− return handleButtonPress(static_cast<WId>(pressEvent->event), pressEvent->detail,
255+ return handleButtonPress(static_cast<WId>(pressEvent->event), pressEvent->detail, 0,
236 pressEvent->event_x, pressEvent->event_y);
256 pressEvent->event_x, pressEvent->event_y);
237 }
257 }
238 case XCB_BUTTON_RELEASE: {
258 case XCB_BUTTON_RELEASE: {
239 auto releaseEvent = reinterpret_cast<xcb_button_release_event_t *>(xcbEvent);
259 auto releaseEvent = reinterpret_cast<xcb_button_release_event_t *>(xcbEvent);
240− return handleButtonRelease(static_cast<WId>(releaseEvent->event), releaseEvent->detail,
260+ return handleButtonRelease(static_cast<WId>(releaseEvent->event), releaseEvent->detail, 0,
241 releaseEvent->event_x, releaseEvent->event_y);
261 releaseEvent->event_x, releaseEvent->event_y);
242 }
262 }
243 case XCB_MOTION_NOTIFY: {
263 case XCB_MOTION_NOTIFY: {
244 auto motionEvent = reinterpret_cast<xcb_motion_notify_event_t *>(xcbEvent);
264 auto motionEvent = reinterpret_cast<xcb_motion_notify_event_t *>(xcbEvent);
245− return handleMotionNotify(static_cast<WId>(motionEvent->event), motionEvent->event_x, motionEvent->event_y);
265+ return handleMotionNotify(static_cast<WId>(motionEvent->event), 0,
266+ motionEvent->event_x, motionEvent->event_y);
246 }
267 }
247 case XCB_GE_GENERIC:
268 case XCB_GE_GENERIC:
248 if (m_xi2Enabled) {
269 if (m_xi2Enabled) {
@@ -255,9 +276,10 @@ bool MouseTouchAdaptor::nativeEventFilter(const QByteArray & eventType,
255 };
276 };
256}
277}
257
278
258−bool MouseTouchAdaptor::handleButtonPress(WId windowId, uint32_t detail, int x, int y)
279+bool MouseTouchAdaptor::handleButtonPress(WId windowId, uint32_t detail, uint32_t modifiers, int x, int y)
259{
280{
260 Qt::MouseButton button = translateMouseButton(detail);
281 Qt::MouseButton button = translateMouseButton(detail);
282+ Qt::KeyboardModifiers qtMod = translateMofidier(modifiers);
261
283
262 // Just eat the event if it wasn't a left mouse press
284 // Just eat the event if it wasn't a left mouse press
263 if (button != Qt::LeftButton)
285 if (button != Qt::LeftButton)
@@ -270,19 +292,24 @@ bool MouseTouchAdaptor::handleButtonPress(WId windowId, uint32_t detail, int x,
270 QTouchEventSequence touchEvent = QTest::touchEvent(targetWindow, m_touchDevice,
292 QTouchEventSequence touchEvent = QTest::touchEvent(targetWindow, m_touchDevice,
271 false /* autoCommit */);
293 false /* autoCommit */);
272 touchEvent.press(0 /* touchId */, windowPos);
294 touchEvent.press(0 /* touchId */, windowPos);
295+ if (qtMod == TRI_PRESS_MODIFIER) {
296+ touchEvent.press(1, windowPos);
297+ touchEvent.press(2, windowPos);
298+ m_triPressModifier = true;
299+ }
273 touchEvent.commit(false /* processEvents */);
300 touchEvent.commit(false /* processEvents */);
274
301
275 m_leftButtonIsPressed = true;
302 m_leftButtonIsPressed = true;
276 return true;
303 return true;
277}
304}
278
305
279−bool MouseTouchAdaptor::handleButtonRelease(WId windowId, uint32_t detail, int x, int y)
306+bool MouseTouchAdaptor::handleButtonRelease(WId windowId, uint32_t detail, uint32_t, int x, int y)
280{
307{
281 Qt::MouseButton button = translateMouseButton(detail);
308 Qt::MouseButton button = translateMouseButton(detail);
282
309
283− // Just eat the event if it wasn't a left mouse release
310+ // Don't eat the event if it wasn't a left mouse press
284 if (button != Qt::LeftButton)
311 if (button != Qt::LeftButton)
285− return true;
312+ return false;
286
313
287 QWindow *targetWindow = findQWindowWithXWindowID(windowId);
314 QWindow *targetWindow = findQWindowWithXWindowID(windowId);
288
315
@@ -291,17 +318,23 @@ bool MouseTouchAdaptor::handleButtonRelease(WId windowId, uint32_t detail, int x
291 QTouchEventSequence touchEvent = QTest::touchEvent(targetWindow, m_touchDevice,
318 QTouchEventSequence touchEvent = QTest::touchEvent(targetWindow, m_touchDevice,
292 false /* autoCommit */);
319 false /* autoCommit */);
293 touchEvent.release(0 /* touchId */, windowPos);
320 touchEvent.release(0 /* touchId */, windowPos);
321+ if (m_triPressModifier) {
322+ touchEvent.release(1, windowPos);
323+ touchEvent.release(2, windowPos);
324+ }
294 touchEvent.commit(false /* processEvents */);
325 touchEvent.commit(false /* processEvents */);
295
326
296 m_leftButtonIsPressed = false;
327 m_leftButtonIsPressed = false;
328+ m_triPressModifier = false;
297 return true;
329 return true;
298}
330}
299
331
300−bool MouseTouchAdaptor::handleMotionNotify(WId windowId, int x, int y)
332+bool MouseTouchAdaptor::handleMotionNotify(WId windowId, uint32_t modifiers, int x, int y)
301{
333{
302 if (!m_leftButtonIsPressed) {
334 if (!m_leftButtonIsPressed) {
303 return true;
335 return true;
304 }
336 }
337+ Qt::KeyboardModifiers qtMod = translateMofidier(modifiers);
305
338
306 QWindow *targetWindow = findQWindowWithXWindowID(windowId);
339 QWindow *targetWindow = findQWindowWithXWindowID(windowId);
307
340
@@ -310,6 +343,17 @@ bool MouseTouchAdaptor::handleMotionNotify(WId windowId, int x, int y)
310 QTouchEventSequence touchEvent = QTest::touchEvent(targetWindow, m_touchDevice,
343 QTouchEventSequence touchEvent = QTest::touchEvent(targetWindow, m_touchDevice,
311 false /* autoCommit */);
344 false /* autoCommit */);
312 touchEvent.move(0 /* touchId */, windowPos);
345 touchEvent.move(0 /* touchId */, windowPos);
346+ if (m_triPressModifier) {
347+ if (qtMod == TRI_PRESS_MODIFIER) {
348+ touchEvent.move(1, windowPos);
349+ touchEvent.move(2, windowPos);
350+ } else {
351+ // released modifiers
352+ touchEvent.release(1, windowPos);
353+ touchEvent.release(2, windowPos);
354+ m_triPressModifier = false;
355+ }
356+ }
313 touchEvent.commit(false /* processEvents */);
357 touchEvent.commit(false /* processEvents */);
314
358
315 return true;
359 return true;
src/MouseTouchAdaptor.h+5−3
@@ -51,13 +51,15 @@ private:
51 void fetchXInput2Info();
51 void fetchXInput2Info();
52 bool xi2HandleEvent(xcb_ge_event_t *event);
52 bool xi2HandleEvent(xcb_ge_event_t *event);
53
53
54− bool handleButtonPress(WId windowId, uint32_t detail, int x, int y);
54+ bool handleButtonPress(WId windowId, uint32_t detail, uint32_t modifiers, int x, int y);
55− bool handleButtonRelease(WId windowId, uint32_t detail, int x, int y);
55+ bool handleButtonRelease(WId windowId, uint32_t detail, uint32_t modifiers, int x, int y);
56− bool handleMotionNotify(WId windowId, int x, int y);
56+ bool handleMotionNotify(WId windowId, uint32_t modifiers, int x, int y);
57 QWindow *findQWindowWithXWindowID(WId windowId);
57 QWindow *findQWindowWithXWindowID(WId windowId);
58
58
59 QTouchDevice *m_touchDevice;
59 QTouchDevice *m_touchDevice;
60 bool m_leftButtonIsPressed;
60 bool m_leftButtonIsPressed;
61+ bool m_triPressModifier;
62+
61
63
62 bool m_enabled;
64 bool m_enabled;
63
65
tests/libs/UbuntuGestures/tst_TouchRegistry.cpp+59
@@ -42,6 +42,10 @@ public:
42 QSet<int> ownedTouches;
42 QSet<int> ownedTouches;
43 QSet<int> lostTouches;
43 QSet<int> lostTouches;
44 QList<TouchMemento> unownedTouchEvents;
44 QList<TouchMemento> unownedTouchEvents;
45+
46+Q_SIGNALS:
47+ void gainedOwnership();
48+ void lostOwnership();
45};
49};
46
50
47class tst_TouchRegistry : public QObject
51class tst_TouchRegistry : public QObject
@@ -68,6 +72,7 @@ private Q_SLOTS:
68 void removeOldUndecidedCandidates();
72 void removeOldUndecidedCandidates();
69 void interimOwnerWontGetUnownedTouchEvents();
73 void interimOwnerWontGetUnownedTouchEvents();
70 void candidateVanishes();
74 void candidateVanishes();
75+ void candicateOwnershipReentrace();
71
76
72private:
77private:
73 TouchRegistry *touchRegistry;
78 TouchRegistry *touchRegistry;
@@ -864,6 +869,58 @@ void tst_TouchRegistry::candidateVanishes()
864 QVERIFY(mainCandidate.ownedTouches.contains(0));
869 QVERIFY(mainCandidate.ownedTouches.contains(0));
865}
870}
866
871
872+/*
873+ Regression test for canidate reentrance
874+
875+ Bug caused by candidates getting removed during ownership resolution
876+ */
877+void tst_TouchRegistry::candicateOwnershipReentrace()
878+{
879+ DummyCandidate mainCandidate;
880+ DummyCandidate candicate2;
881+ DummyCandidate candicate3;
882+
883+ {
884+ QList<QTouchEvent::TouchPoint> touchPoints;
885+ touchPoints.append(QTouchEvent::TouchPoint(0));
886+ touchPoints[0].setState(Qt::TouchPointPressed);
887+ QTouchEvent touchEvent(QEvent::TouchBegin,
888+ 0 /* device */,
889+ Qt::NoModifier,
890+ Qt::TouchPointPressed,
891+ touchPoints);
892+ touchRegistry->update(&touchEvent);
893+ }
894+
895+ // Re-entrance!
896+ connect(&candicate2, &DummyCandidate::lostOwnership, this, [&]() {
897+ touchRegistry->removeCandidateOwnerForTouch(0, &candicate2);
898+ });
899+
900+ touchRegistry->addCandidateOwnerForTouch(0, &mainCandidate);
901+ touchRegistry->addCandidateOwnerForTouch(0, &candicate2);
902+ touchRegistry->addCandidateOwnerForTouch(0, &candicate3);
903+
904+ {
905+ QList<QTouchEvent::TouchPoint> touchPoints;
906+ touchPoints.append(QTouchEvent::TouchPoint(0));
907+ touchPoints[0].setState(Qt::TouchPointMoved);
908+ QTouchEvent touchEvent(QEvent::TouchUpdate,
909+ 0 /* device */,
910+ Qt::NoModifier,
911+ Qt::TouchPointMoved,
912+ touchPoints);
913+ touchRegistry->update(&touchEvent);
914+ }
915+
916+ touchRegistry->requestTouchOwnership(0, &mainCandidate);
917+
918+ QCOMPARE(mainCandidate.ownedTouches.count(), 1);
919+ QCOMPARE(candicate2.lostTouches.count(), 1);
920+ QCOMPARE(candicate3.lostTouches.count(), 1);
921+}
922+
923+
867////////////// TouchMemento //////////
924////////////// TouchMemento //////////
868
925
869TouchMemento::TouchMemento(const QTouchEvent *touchEvent)
926TouchMemento::TouchMemento(const QTouchEvent *touchEvent)
@@ -897,8 +954,10 @@ bool DummyCandidate::event(QEvent *e)
897
954
898 if (touchOwnershipEvent->gained()) {
955 if (touchOwnershipEvent->gained()) {
899 ownedTouches.insert(touchOwnershipEvent->touchId());
956 ownedTouches.insert(touchOwnershipEvent->touchId());
957+ Q_EMIT gainedOwnership();
900 } else {
958 } else {
901 lostTouches.insert(touchOwnershipEvent->touchId());
959 lostTouches.insert(touchOwnershipEvent->touchId());
960+ Q_EMIT lostOwnership();
902 }
961 }
903 return true;
962 return true;
904 } else if (e->type() == UnownedTouchEvent::unownedTouchEventType()) {
963 } else if (e->type() == UnownedTouchEvent::unownedTouchEventType()) {
tests/mocks/Unity/Application/ApplicationManager.cpp+10−25
@@ -151,10 +151,6 @@ void ApplicationManager::add(ApplicationInfo *application) {
151
151
152 beginInsertRows(QModelIndex(), m_runningApplications.size(), m_runningApplications.size());
152 beginInsertRows(QModelIndex(), m_runningApplications.size(), m_runningApplications.size());
153 m_runningApplications.append(application);
153 m_runningApplications.append(application);
154− endInsertRows();
155− Q_EMIT applicationAdded(application->appId());
156− Q_EMIT countChanged();
157− if (count() == 1) Q_EMIT emptyChanged(isEmpty()); // was empty but not anymore
158
154
159 connect(application, &ApplicationInfo::sessionChanged, this, [application, this]() {
155 connect(application, &ApplicationInfo::sessionChanged, this, [application, this]() {
160 QModelIndex appIndex = findIndex(application);
156 QModelIndex appIndex = findIndex(application);
@@ -171,6 +167,16 @@ void ApplicationManager::add(ApplicationInfo *application) {
171 if (!appIndex.isValid()) return;
167 if (!appIndex.isValid()) return;
172 Q_EMIT dataChanged(appIndex, appIndex, QVector<int>() << ApplicationManager::RoleState);
168 Q_EMIT dataChanged(appIndex, appIndex, QVector<int>() << ApplicationManager::RoleState);
173 });
169 });
170+ connect(application, &ApplicationInfo::stageChanged, this, [application, this]() {
171+ QModelIndex appIndex = findIndex(application);
172+ if (!appIndex.isValid()) return;
173+ Q_EMIT dataChanged(appIndex, appIndex, QVector<int>() << ApplicationManager::RoleStage);
174+ });
175+
176+ endInsertRows();
177+ Q_EMIT applicationAdded(application->appId());
178+ Q_EMIT countChanged();
179+ if (count() == 1) Q_EMIT emptyChanged(isEmpty()); // was empty but not anymore
174}
180}
175
181
176void ApplicationManager::remove(ApplicationInfo *application) {
182void ApplicationManager::remove(ApplicationInfo *application) {
@@ -202,23 +208,11 @@ void ApplicationManager::move(int from, int to) {
202
208
203ApplicationInfo* ApplicationManager::startApplication(const QString &appId,
209ApplicationInfo* ApplicationManager::startApplication(const QString &appId,
204 const QStringList &arguments)
210 const QStringList &arguments)
205−{
206− return startApplication(appId, NoFlag, arguments);
207−}
208−
209−ApplicationInfo* ApplicationManager::startApplication(const QString &appId,
210− ExecFlags flags,
211− const QStringList &arguments)
212{
211{
213 Q_UNUSED(arguments)
212 Q_UNUSED(arguments)
214 ApplicationInfo *application = add(appId);
213 ApplicationInfo *application = add(appId);
215 if (!application)
214 if (!application)
216 return 0;
215 return 0;
217−
218− if (flags.testFlag(ApplicationManager::ForceMainStage)
219− && application->stage() == ApplicationInfo::SideStage) {
220− application->setStage(ApplicationInfo::MainStage);
221− }
222 application->setState(ApplicationInfo::Starting);
216 application->setState(ApplicationInfo::Starting);
223
217
224 return application;
218 return application;
@@ -317,7 +311,6 @@ void ApplicationManager::buildListOfAvailableApplications()
317 application->setAppId("unity8-dash");
311 application->setAppId("unity8-dash");
318 application->setName("Unity 8 Mock Dash");
312 application->setName("Unity 8 Mock Dash");
319 application->setScreenshotId("unity8-dash");
313 application->setScreenshotId("unity8-dash");
320− application->setStage(ApplicationInfo::MainStage);
321 application->setSupportedOrientations(Qt::PrimaryOrientation);
314 application->setSupportedOrientations(Qt::PrimaryOrientation);
322 m_availableApplications.append(application);
315 m_availableApplications.append(application);
323
316
@@ -326,7 +319,6 @@ void ApplicationManager::buildListOfAvailableApplications()
326 application->setName("Dialer");
319 application->setName("Dialer");
327 application->setScreenshotId("dialer");
320 application->setScreenshotId("dialer");
328 application->setIconId("dialer-app");
321 application->setIconId("dialer-app");
329− application->setStage(ApplicationInfo::SideStage);
330 application->setSupportedOrientations(Qt::PortraitOrientation
322 application->setSupportedOrientations(Qt::PortraitOrientation
331 | Qt::InvertedPortraitOrientation);
323 | Qt::InvertedPortraitOrientation);
332 m_availableApplications.append(application);
324 m_availableApplications.append(application);
@@ -350,7 +342,6 @@ void ApplicationManager::buildListOfAvailableApplications()
350 application->setScreenshotId("gallery");
342 application->setScreenshotId("gallery");
351 application->setIconId("gallery");
343 application->setIconId("gallery");
352 application->setFullscreen(true);
344 application->setFullscreen(true);
353− application->setStage(ApplicationInfo::MainStage);
354 m_availableApplications.append(application);
345 m_availableApplications.append(application);
355
346
356 application = new ApplicationInfo(this);
347 application = new ApplicationInfo(this);
@@ -358,7 +349,6 @@ void ApplicationManager::buildListOfAvailableApplications()
358 application->setName("Facebook");
349 application->setName("Facebook");
359 application->setScreenshotId("facebook");
350 application->setScreenshotId("facebook");
360 application->setIconId("facebook");
351 application->setIconId("facebook");
361− application->setStage(ApplicationInfo::SideStage);
362 m_availableApplications.append(application);
352 m_availableApplications.append(application);
363
353
364 application = new ApplicationInfo(this);
354 application = new ApplicationInfo(this);
@@ -374,7 +364,6 @@ void ApplicationManager::buildListOfAvailableApplications()
374 application->setName("Twitter");
364 application->setName("Twitter");
375 application->setScreenshotId("twitter");
365 application->setScreenshotId("twitter");
376 application->setIconId("twitter");
366 application->setIconId("twitter");
377− application->setStage(ApplicationInfo::SideStage);
378 m_availableApplications.append(application);
367 m_availableApplications.append(application);
379
368
380 application = new ApplicationInfo(this);
369 application = new ApplicationInfo(this);
@@ -390,7 +379,6 @@ void ApplicationManager::buildListOfAvailableApplications()
390 application->setIconId("gmail");
379 application->setIconId("gmail");
391 application->setScreenshotId("gmail-webapp.svg");
380 application->setScreenshotId("gmail-webapp.svg");
392 application->setFullscreen(false);
381 application->setFullscreen(false);
393− application->setStage(ApplicationInfo::MainStage);
394 application->setSupportedOrientations(Qt::PortraitOrientation
382 application->setSupportedOrientations(Qt::PortraitOrientation
395 | Qt::LandscapeOrientation
383 | Qt::LandscapeOrientation
396 | Qt::InvertedPortraitOrientation
384 | Qt::InvertedPortraitOrientation
@@ -403,7 +391,6 @@ void ApplicationManager::buildListOfAvailableApplications()
403 application->setIconId("soundcloud");
391 application->setIconId("soundcloud");
404 application->setScreenshotId("music");
392 application->setScreenshotId("music");
405 application->setFullscreen(false);
393 application->setFullscreen(false);
406− application->setStage(ApplicationInfo::MainStage);
407 application->setSupportedOrientations(Qt::PortraitOrientation
394 application->setSupportedOrientations(Qt::PortraitOrientation
408 | Qt::LandscapeOrientation
395 | Qt::LandscapeOrientation
409 | Qt::InvertedPortraitOrientation
396 | Qt::InvertedPortraitOrientation
@@ -423,14 +410,12 @@ void ApplicationManager::buildListOfAvailableApplications()
423 application->setAppId("notes-app");
410 application->setAppId("notes-app");
424 application->setName("Notepad");
411 application->setName("Notepad");
425 application->setIconId("notepad");
412 application->setIconId("notepad");
426− application->setStage(ApplicationInfo::SideStage);
427 m_availableApplications.append(application);
413 m_availableApplications.append(application);
428
414
429 application = new ApplicationInfo(this);
415 application = new ApplicationInfo(this);
430 application->setAppId("calendar-app");
416 application->setAppId("calendar-app");
431 application->setName("Calendar");
417 application->setName("Calendar");
432 application->setIconId("calendar");
418 application->setIconId("calendar");
433− application->setStage(ApplicationInfo::SideStage);
434 m_availableApplications.append(application);
419 m_availableApplications.append(application);
435
420
436 application = new ApplicationInfo(this);
421 application = new ApplicationInfo(this);
tests/mocks/Unity/Application/ApplicationManager.h−8
@@ -46,11 +46,6 @@ class ApplicationManager : public ApplicationManagerInterface {
46 RoleSession = RoleExemptFromLifecycle+1,
46 RoleSession = RoleExemptFromLifecycle+1,
47 RoleFullscreen,
47 RoleFullscreen,
48 };
48 };
49− enum Flag {
50− NoFlag = 0x0,
51− ForceMainStage = 0x1,
52− };
53− Q_DECLARE_FLAGS(ExecFlags, Flag)
54
49
55 // QAbstractItemModel methods.
50 // QAbstractItemModel methods.
56 int rowCount(const QModelIndex& parent = QModelIndex()) const override;
51 int rowCount(const QModelIndex& parent = QModelIndex()) const override;
@@ -65,7 +60,6 @@ class ApplicationManager : public ApplicationManagerInterface {
65 Q_INVOKABLE bool focusApplication(const QString &appId) override;
60 Q_INVOKABLE bool focusApplication(const QString &appId) override;
66 Q_INVOKABLE void unfocusCurrentApplication() override;
61 Q_INVOKABLE void unfocusCurrentApplication() override;
67 Q_INVOKABLE ApplicationInfo *startApplication(const QString &appId, const QStringList &arguments = QStringList()) override;
62 Q_INVOKABLE ApplicationInfo *startApplication(const QString &appId, const QStringList &arguments = QStringList()) override;
68− Q_INVOKABLE ApplicationInfo *startApplication(const QString &appId, ExecFlags flags, const QStringList &arguments = QStringList());
69 Q_INVOKABLE bool stopApplication(const QString &appId) override;
63 Q_INVOKABLE bool stopApplication(const QString &appId) override;
70
64
71 QString focusedApplicationId() const override;
65 QString focusedApplicationId() const override;
@@ -98,8 +92,6 @@ class ApplicationManager : public ApplicationManagerInterface {
98 static ApplicationManager *the_application_manager;
92 static ApplicationManager *the_application_manager;
99};
93};
100
94
101−Q_DECLARE_OPERATORS_FOR_FLAGS(ApplicationManager::ExecFlags)
102−
103Q_DECLARE_METATYPE(ApplicationManager*)
95Q_DECLARE_METATYPE(ApplicationManager*)
104
96
105#endif // APPLICATION_MANAGER_H
97#endif // APPLICATION_MANAGER_H
tests/mocks/Unity/Application/MirSurfaceItem.cpp+17
@@ -156,6 +156,10 @@ void MirSurfaceItem::createQmlContentItem()
156
156
157void MirSurfaceItem::touchEvent(QTouchEvent * event)
157void MirSurfaceItem::touchEvent(QTouchEvent * event)
158{
158{
159+ if (event->type() == QEvent::TouchBegin) {
160+ m_touchTrail.clear();
161+ }
162+
159 if (event->touchPointStates() & Qt::TouchPointPressed) {
163 if (event->touchPointStates() & Qt::TouchPointPressed) {
160 ++m_touchPressCount;
164 ++m_touchPressCount;
161 Q_EMIT touchPressCountChanged(m_touchPressCount);
165 Q_EMIT touchPressCountChanged(m_touchPressCount);
@@ -163,6 +167,19 @@ void MirSurfaceItem::touchEvent(QTouchEvent * event)
163 ++m_touchReleaseCount;
167 ++m_touchReleaseCount;
164 Q_EMIT touchReleaseCountChanged(m_touchReleaseCount);
168 Q_EMIT touchReleaseCountChanged(m_touchReleaseCount);
165 }
169 }
170+
171+ Q_FOREACH(QTouchEvent::TouchPoint touchPoint, event->touchPoints()) {
172+ QString id(touchPoint.id());
173+ QVariantList list = m_touchTrail[id].toList();
174+ list.append(QVariant::fromValue(touchPoint.pos()));
175+ if (list.count() > 100) list.pop_front();
176+ m_touchTrail[id] = list;
177+ }
178+
179+ if (m_qmlItem) {
180+ QQmlProperty touchTrail(m_qmlItem, "touchTrail");
181+ touchTrail.write(m_touchTrail);
182+ }
166}
183}
167
184
168void MirSurfaceItem::mousePressEvent(QMouseEvent * event)
185void MirSurfaceItem::mousePressEvent(QMouseEvent * event)
tests/mocks/Unity/Application/MirSurfaceItem.h+1
@@ -123,6 +123,7 @@ private:
123 int m_touchReleaseCount;
123 int m_touchReleaseCount;
124 int m_mousePressCount;
124 int m_mousePressCount;
125 int m_mouseReleaseCount;
125 int m_mouseReleaseCount;
126+ QVariantMap m_touchTrail;
126
127
127 FillMode m_fillMode{Stretch};
128 FillMode m_fillMode{Stretch};
128
129
tests/mocks/Unity/Application/resources/MirSurfaceItem.qml+59
@@ -29,6 +29,9 @@ Rectangle {
29 property alias screenshotSource: screenshotImage.source
29 property alias screenshotSource: screenshotImage.source
30 property int orientationAngle
30 property int orientationAngle
31
31
32+ property var touchTrail
33+ onTouchTrailChanged: canvas.requestPaint()
34+
32 Image {
35 Image {
33 id: screenshotImage
36 id: screenshotImage
34 anchors.fill: parent
37 anchors.fill: parent
@@ -67,4 +70,60 @@ Rectangle {
67 width: (rotation == 0 || rotation == 180 ? parent.width : parent.height)
70 width: (rotation == 0 || rotation == 180 ? parent.width : parent.height)
68 height:(rotation == 0 || rotation == 180 ? parent.height : parent.width)
71 height:(rotation == 0 || rotation == 180 ? parent.height : parent.width)
69 }
72 }
73+
74+ Canvas {
75+ id: canvas
76+ anchors.fill: parent
77+ onPaint: {
78+ // get context to draw with
79+ var ctx = getContext("2d")
80+ ctx.reset(0, 0, canvas.width, canvas.height);
81+ ctx.lineWidth = 20;
82+
83+ if (touchTrail === undefined) return;
84+
85+ var i = 0;
86+ for (var prop in touchTrail) {
87+ var trail = touchTrail[prop];
88+
89+ if (trail.length > 0) {
90+ ctx.fillStyle = Qt.rgba(0, 0, 1, 0.3);
91+ ctx.strokeStyle = Qt.rgba(0, 0, 1, 0.3);
92+ ctx.beginPath();
93+ ctx.moveTo(trail[0].x, trail[0].y);
94+ ctx.ellipse(trail[0].x-1, trail[0].y-1, 2, 2);
95+ ctx.stroke();
96+
97+ ctx.beginPath();
98+ if (i == 0) {
99+ ctx.strokeStyle = Qt.rgba(1, 1, 0, 0.3);
100+ } else if (i == 1) {
101+ ctx.strokeStyle = Qt.rgba(0, 1, 1, 0.3);
102+
103+ } else if (i == 2) {
104+ ctx.strokeStyle = Qt.rgba(1, 0, 1, 0.3);
105+
106+ } else if (i == 3) {
107+ ctx.strokeStyle = Qt.rgba(0, 0.5, 0.5, 0.3);
108+
109+ } else {
110+ ctx.strokeStyle = Qt.rgba(0.7, 0.5, 0, 0.3);
111+ }
112+
113+ for (var i = 1; i < trail.length; i++) {
114+ ctx.lineTo(trail[i].x, trail[i].y)
115+ }
116+ ctx.stroke();
117+
118+ ctx.beginPath();
119+ ctx.fillStyle = Qt.rgba(1, 0, 0, 0.3);
120+ ctx.strokeStyle = Qt.rgba(1, 0, 0, 0.3);
121+ ctx.ellipse(trail[i-1].x-1, trail[i-1].y-1, 2, 2);
122+ ctx.stroke();
123+ }
124+ i++;
125+ }
126+
127+ }
128+ }
70}
129}
tests/mocks/Utils/CMakeLists.txt+1
@@ -19,6 +19,7 @@ set(QMLPLUGIN_SRC
19 ${CMAKE_SOURCE_DIR}/plugins/Utils/applicationsfiltermodel.cpp
19 ${CMAKE_SOURCE_DIR}/plugins/Utils/applicationsfiltermodel.cpp
20 ${CMAKE_SOURCE_DIR}/plugins/Utils/inputeventgenerator.cpp
20 ${CMAKE_SOURCE_DIR}/plugins/Utils/inputeventgenerator.cpp
21 ${CMAKE_SOURCE_DIR}/plugins/Utils/deviceconfigparser.cpp
21 ${CMAKE_SOURCE_DIR}/plugins/Utils/deviceconfigparser.cpp
22+ ${CMAKE_SOURCE_DIR}/plugins/Utils/globalfunctions.cpp
22 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationManagerInterface.h
23 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationManagerInterface.h
23 constants.cpp
24 constants.cpp
24 plugin.cpp
25 plugin.cpp
tests/mocks/Utils/plugin.cpp+9
@@ -38,6 +38,7 @@
38#include <applicationsfiltermodel.h>
38#include <applicationsfiltermodel.h>
39#include <inputeventgenerator.h>
39#include <inputeventgenerator.h>
40#include <deviceconfigparser.h>
40#include <deviceconfigparser.h>
41+#include <globalfunctions.h>
41
42
42static QObject *createWindowStateStorage(QQmlEngine *engine, QJSEngine *scriptEngine)
43static QObject *createWindowStateStorage(QQmlEngine *engine, QJSEngine *scriptEngine)
43{
44{
@@ -53,6 +54,13 @@ static QObject *createConstants(QQmlEngine *engine, QJSEngine *scriptEngine)
53 return new Constants();
54 return new Constants();
54}
55}
55
56
57+static QObject *createGlobalFunctions(QQmlEngine *engine, QJSEngine *scriptEngine)
58+{
59+ Q_UNUSED(engine)
60+ Q_UNUSED(scriptEngine)
61+ return new GlobalFunctions();
62+}
63+
56void FakeUtilsPlugin::registerTypes(const char *uri)
64void FakeUtilsPlugin::registerTypes(const char *uri)
57{
65{
58 Q_ASSERT(uri == QLatin1String("Utils"));
66 Q_ASSERT(uri == QLatin1String("Utils"));
@@ -69,6 +77,7 @@ void FakeUtilsPlugin::registerTypes(const char *uri)
69 qmlRegisterType<ApplicationsFilterModel>(uri, 0, 1, "ApplicationsFilterModel");
77 qmlRegisterType<ApplicationsFilterModel>(uri, 0, 1, "ApplicationsFilterModel");
70 qmlRegisterType<InputEventGenerator>(uri, 0, 1, "InputEventGenerator");
78 qmlRegisterType<InputEventGenerator>(uri, 0, 1, "InputEventGenerator");
71 qmlRegisterType<DeviceConfigParser>(uri, 0, 1, "DeviceConfigParser");
79 qmlRegisterType<DeviceConfigParser>(uri, 0, 1, "DeviceConfigParser");
80+ qmlRegisterSingletonType<GlobalFunctions>(uri, 0, 1, "Functions", createGlobalFunctions);
72}
81}
73
82
74void FakeUtilsPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
83void FakeUtilsPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
tests/mocks/Utils/windowstatestorage.cpp+13
@@ -16,6 +16,8 @@
16
16
17#include "windowstatestorage.h"
17#include "windowstatestorage.h"
18
18
19+#include <unity/shell/application/ApplicationInfoInterface.h>
20+
19WindowStateStorage::WindowStateStorage(QObject *parent):
21WindowStateStorage::WindowStateStorage(QObject *parent):
20 QObject(parent)
22 QObject(parent)
21{
23{
@@ -45,10 +47,21 @@ QRect WindowStateStorage::getGeometry(const QString &windowId, const QRect &defa
45 return m_geometry.value(windowId).toRect();
47 return m_geometry.value(windowId).toRect();
46}
48}
47
49
50+void WindowStateStorage::saveStage(const QString &appId, int stage)
51+{
52+ m_stage[appId] = stage;
53+}
54+
55+int WindowStateStorage::getStage(const QString &appId) const
56+{
57+ return m_stage.value(appId, unity::shell::application::ApplicationInfoInterface::MainStage);
58+}
59+
48void WindowStateStorage::clear()
60void WindowStateStorage::clear()
49{
61{
50 m_state.clear();
62 m_state.clear();
51 m_geometry.clear();
63 m_geometry.clear();
64+ m_stage.clear();
52}
65}
53
66
54void WindowStateStorage::saveState(const QString &windowId, WindowState state)
67void WindowStateStorage::saveState(const QString &windowId, WindowState state)
tests/mocks/Utils/windowstatestorage.h+4
@@ -36,6 +36,9 @@ public:
36 Q_INVOKABLE void saveGeometry(const QString &windowId, const QRect &rect);
36 Q_INVOKABLE void saveGeometry(const QString &windowId, const QRect &rect);
37 Q_INVOKABLE QRect getGeometry(const QString &windowId, const QRect &defaultValue);
37 Q_INVOKABLE QRect getGeometry(const QString &windowId, const QRect &defaultValue);
38
38
39+ Q_INVOKABLE void saveStage(const QString &appId, int stage);
40+ Q_INVOKABLE int getStage(const QString &appId) const;
41+
39 // Only in the mock, to easily restore a fresh state
42 // Only in the mock, to easily restore a fresh state
40 Q_INVOKABLE void clear();
43 Q_INVOKABLE void clear();
41
44
@@ -47,5 +50,6 @@ private:
47 QVariantMap geometry() const;
50 QVariantMap geometry() const;
48
51
49 QHash<QString, WindowState> m_state;
52 QHash<QString, WindowState> m_state;
53+ QHash<QString, int> m_stage;
50 QVariantMap m_geometry;
54 QVariantMap m_geometry;
51};
55};
tests/plugins/Ubuntu/Gestures/CMakeLists.txt+7−1
@@ -20,6 +20,11 @@ macro(build_gesture_test CLASSNAME)
20 target_link_libraries(${CLASSNAME}TestExec UbuntuGesturesQml UbuntuGestures)
20 target_link_libraries(${CLASSNAME}TestExec UbuntuGesturesQml UbuntuGestures)
21endmacro()
21endmacro()
22
22
23+set(UNITY_IMPORT_PATHS
24+ ${CMAKE_BINARY_DIR}/tests/utils/modules
25+ ${UNITY_PLUGINPATH}
26+)
27+
23macro(add_gesture_unit_test CLASSNAME)
28macro(add_gesture_unit_test CLASSNAME)
24 build_gesture_test(${CLASSNAME})
29 build_gesture_test(${CLASSNAME})
25 add_unity8_unittest(${CLASSNAME} ${CLASSNAME}TestExec
30 add_unity8_unittest(${CLASSNAME} ${CLASSNAME}TestExec
@@ -36,7 +41,7 @@ macro(add_gesture_ui_test CLASSNAME)
36 DEPENDS UbuntuGesturesTest-qmlfiles
41 DEPENDS UbuntuGesturesTest-qmlfiles
37 ${ARGN}
42 ${ARGN}
38 )
43 )
39− add_manual_qml_test(. ${CLASSNAME} IMPORT_PATHS ${UNITY_PLUGINPATH})
44+ add_manual_qml_test(. ${CLASSNAME} IMPORT_PATHS ${UNITY_IMPORT_PATHS})
40endmacro()
45endmacro()
41
46
42add_gesture_ui_test(DirectionalDragArea)
47add_gesture_ui_test(DirectionalDragArea)
@@ -46,3 +51,4 @@ add_gesture_ui_test(TouchDispatcher)
46add_gesture_ui_test(TouchGate)
51add_gesture_ui_test(TouchGate)
47add_gesture_unit_test(Damper)
52add_gesture_unit_test(Damper)
48add_gesture_unit_test(AxisVelocityCalculator)
53add_gesture_unit_test(AxisVelocityCalculator)
54+add_gesture_ui_test(TouchGestureArea)
tests/plugins/Ubuntu/Gestures/GestureTest.cpp+44
@@ -17,6 +17,7 @@
17#include "GestureTest.h"
17#include "GestureTest.h"
18
18
19#include <qpa/qwindowsysteminterface.h>
19#include <qpa/qwindowsysteminterface.h>
20+#include <private/qquickwindow_p.h>
20#include <QQmlEngine>
21#include <QQmlEngine>
21#include <QQuickView>
22#include <QQuickView>
22#include <QtTest>
23#include <QtTest>
@@ -72,6 +73,49 @@ void GestureTest::cleanup()
72 m_view = nullptr;
73 m_view = nullptr;
73}
74}
74
75
76+void GestureTest::sendTouchPress(qint64 timestamp, int id, QPointF pos)
77+{
78+ sendTouch(timestamp, id, pos, Qt::TouchPointPressed, QEvent::TouchBegin);
79+}
80+
81+void GestureTest::sendTouchUpdate(qint64 timestamp, int id, QPointF pos)
82+{
83+ sendTouch(timestamp, id, pos, Qt::TouchPointMoved, QEvent::TouchUpdate);
84+}
85+
86+void GestureTest::sendTouchRelease(qint64 timestamp, int id, QPointF pos)
87+{
88+ sendTouch(timestamp, id, pos, Qt::TouchPointReleased, QEvent::TouchEnd);
89+}
90+
91+void GestureTest::sendTouch(qint64 timestamp, int id, QPointF pos,
92+ Qt::TouchPointState pointState, QEvent::Type eventType)
93+{
94+ m_fakeTimerFactory->updateTime(timestamp);
95+
96+ QTouchEvent::TouchPoint point;
97+
98+ point.setState(pointState);
99+ point.setId(id);
100+ point.setScenePos(pos);
101+ point.setPos(pos);
102+
103+ QList<QTouchEvent::TouchPoint> points;
104+ points << point;
105+
106+ QTouchEvent touchEvent(eventType, m_device, Qt::NoModifier, Qt::TouchPointPressed, points);
107+ QCoreApplication::sendEvent(m_view, &touchEvent);
108+
109+ QQuickWindowPrivate *windowPrivate = QQuickWindowPrivate::get(m_view);
110+ windowPrivate->flushDelayedTouchEvent();
111+}
112+
113+void GestureTest::passTime(qint64 timeSpanMs)
114+{
115+ qint64 finalTime = m_fakeTimerFactory->timeSource()->msecsSinceReference() + timeSpanMs;
116+ m_fakeTimerFactory->updateTime(finalTime);
117+}
118+
75////////////////////////// TouchMemento /////////////////////////////
119////////////////////////// TouchMemento /////////////////////////////
76
120
77TouchMemento::TouchMemento(const QTouchEvent *touchEvent)
121TouchMemento::TouchMemento(const QTouchEvent *touchEvent)
tests/plugins/Ubuntu/Gestures/GestureTest.h+13−1
@@ -72,7 +72,7 @@ class GestureTest : public QObject
72{
72{
73 Q_OBJECT
73 Q_OBJECT
74public:
74public:
75− // \param qmlFilename name of the qml file to be loaded by the QQuickView
75+ // \param qmlFilename name of the qml file to be loaded by the QQuickView
76 GestureTest(const QString &qmlFilename);
76 GestureTest(const QString &qmlFilename);
77
77
78protected Q_SLOTS:
78protected Q_SLOTS:
@@ -80,6 +80,18 @@ protected Q_SLOTS:
80 virtual void init(); // called right before each and every test function is executed
80 virtual void init(); // called right before each and every test function is executed
81 virtual void cleanup(); // called right after each and every test function is executed
81 virtual void cleanup(); // called right after each and every test function is executed
82
82
83+protected:
84+ // QTest::touchEvent takes QPoint instead of QPointF and I don't want to
85+ // lose precision due to rounding.
86+ // Besides, those helper functions lead to more compact code.
87+ void sendTouchPress(qint64 timestamp, int id, QPointF pos);
88+ void sendTouchUpdate(qint64 timestamp, int id, QPointF pos);
89+ void sendTouchRelease(qint64 timestamp, int id, QPointF pos);
90+ void sendTouch(qint64 timestamp, int id, QPointF pos,
91+ Qt::TouchPointState pointState, QEvent::Type eventType);
92+
93+ void passTime(qint64 timeSpanMs);
94+
83protected:
95protected:
84 QTouchDevice *m_device;
96 QTouchDevice *m_device;
85 QQuickView *m_view;
97 QQuickView *m_view;
tests/plugins/Ubuntu/Gestures/tst_DirectionalDragArea.cpp−56
@@ -20,7 +20,6 @@
20#include <QtQml/QQmlEngine>
20#include <QtQml/QQmlEngine>
21#include <QPointer>
21#include <QPointer>
22#include <private/qquickmousearea_p.h>
22#include <private/qquickmousearea_p.h>
23−#include <private/qquickwindow_p.h>
24
23
25
24
26#include <DirectionalDragArea.h>
25#include <DirectionalDragArea.h>
@@ -109,18 +108,6 @@ private Q_SLOTS:
109 void makoRightEdgeDrag_verticalDownwards();
108 void makoRightEdgeDrag_verticalDownwards();
110 void makoLeftEdgeDrag_slowStart();
109 void makoLeftEdgeDrag_slowStart();
111 void makoLeftEdgeDrag_movesSlightlyBackwardsOnStart();
110 void makoLeftEdgeDrag_movesSlightlyBackwardsOnStart();
112−
113−private:
114− // QTest::touchEvent takes QPoint instead of QPointF and I don't want to
115− // lose precision due to rounding.
116− // Besides, those helper functions lead to more compact code.
117− void sendTouchPress(qint64 timestamp, int id, QPointF pos);
118− void sendTouchUpdate(qint64 timestamp, int id, QPointF pos);
119− void sendTouchRelease(qint64 timestamp, int id, QPointF pos);
120− void sendTouch(qint64 timestamp, int id, QPointF pos,
121− Qt::TouchPointState pointState, QEvent::Type eventType);
122−
123− void passTime(qint64 timeSpanMs);
124};
111};
125
112
126tst_DirectionalDragArea::tst_DirectionalDragArea()
113tst_DirectionalDragArea::tst_DirectionalDragArea()
@@ -141,49 +128,6 @@ void tst_DirectionalDragArea::init()
141 QTRY_COMPARE(m_view->height(), (int)m_view->rootObject()->height());
128 QTRY_COMPARE(m_view->height(), (int)m_view->rootObject()->height());
142}
129}
143
130
144−void tst_DirectionalDragArea::sendTouchPress(qint64 timestamp, int id, QPointF pos)
145−{
146− sendTouch(timestamp, id, pos, Qt::TouchPointPressed, QEvent::TouchBegin);
147−}
148−
149−void tst_DirectionalDragArea::sendTouchUpdate(qint64 timestamp, int id, QPointF pos)
150−{
151− sendTouch(timestamp, id, pos, Qt::TouchPointMoved, QEvent::TouchUpdate);
152−}
153−
154−void tst_DirectionalDragArea::sendTouchRelease(qint64 timestamp, int id, QPointF pos)
155−{
156− sendTouch(timestamp, id, pos, Qt::TouchPointReleased, QEvent::TouchEnd);
157−}
158−
159−void tst_DirectionalDragArea::sendTouch(qint64 timestamp, int id, QPointF pos,
160− Qt::TouchPointState pointState, QEvent::Type eventType)
161−{
162− m_fakeTimerFactory->updateTime(timestamp);
163−
164− QTouchEvent::TouchPoint point;
165−
166− point.setState(pointState);
167− point.setId(id);
168− point.setScenePos(pos);
169− point.setPos(pos);
170−
171− QList<QTouchEvent::TouchPoint> points;
172− points << point;
173−
174− QTouchEvent touchEvent(eventType, m_device, Qt::NoModifier, Qt::TouchPointPressed, points);
175− QCoreApplication::sendEvent(m_view, &touchEvent);
176−
177− QQuickWindowPrivate *windowPrivate = QQuickWindowPrivate::get(m_view);
178− windowPrivate->flushDelayedTouchEvent();
179−}
180−
181−void tst_DirectionalDragArea::passTime(qint64 timeSpanMs)
182−{
183− qint64 finalTime = m_fakeTimerFactory->timeSource()->msecsSinceReference() + timeSpanMs;
184− m_fakeTimerFactory->updateTime(finalTime);
185−}
186−
187namespace {
131namespace {
188QPointF calculateInitialTouchPos(DirectionalDragArea *edgeDragArea)
132QPointF calculateInitialTouchPos(DirectionalDragArea *edgeDragArea)
189{
133{
tests/plugins/Ubuntu/Gestures/tst_TouchGestureArea.cppnew file+270
@@ -0,0 +1,270 @@
1+/*
2+ * Copyright (C) 2016 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 "GestureTest.h"
18+
19+#include <TouchGestureArea.h>
20+
21+#include <QQuickItem>
22+#include <QQuickView>
23+#include <QtTest>
24+
25+namespace {
26+QPointF calculateInitialTouchPos(TouchGestureArea *edgeDragArea)
27+{
28+ QPointF localCenter(edgeDragArea->width() / 2., edgeDragArea->height() / 2.);
29+ return edgeDragArea->mapToScene(localCenter);
30+}
31+}
32+
33+class tst_TouchGestureArea: public GestureTest
34+{
35+ Q_OBJECT
36+public:
37+ tst_TouchGestureArea();
38+private Q_SLOTS:
39+ void init() override; // called right before each and every test function is executed
40+
41+ void minimumTouchPoints();
42+ void maximumTouchPoints();
43+ void minimumAndMaximumTouchPoints();
44+ void rejectGestureAfterRecognitionPeriod();
45+ void releaseAndPressRecognisedGestureDoesNotRejectForPeriod();
46+ void topAreaReceivesOwnershipFirstWithEqualPoints();
47+ void topAreaReceivesOwnershipFirstWithMorePoints();
48+
49+private:
50+ void initGestureComponent(TouchGestureArea *area);
51+
52+ QQuickItem *m_blueRect;
53+ TouchGestureArea *m_gestureBottom;
54+ TouchGestureArea *m_gestureMiddle;
55+ TouchGestureArea *m_gestureTop;
56+};
57+
58+tst_TouchGestureArea::tst_TouchGestureArea()
59+ : GestureTest(QStringLiteral("tst_TouchGestureArea.qml"))
60+{
61+}
62+
63+inline void tst_TouchGestureArea::initGestureComponent(TouchGestureArea* area)
64+{
65+ area->setRecognitionTimer(m_fakeTimerFactory->createTimer(area));
66+ area->setMinimumTouchPoints(1);
67+ area->setMaximumTouchPoints(INT_MAX);
68+ area->setRecognitionPeriod(50);
69+ area->setReleaseRejectPeriod(100);
70+ // start tests with area disabled (enable as desired)
71+ area->setEnabled(false);
72+}
73+
74+void tst_TouchGestureArea::init()
75+{
76+ GestureTest::init();
77+
78+ m_blueRect = m_view->rootObject()->findChild<QQuickItem*>("blueRect");
79+ Q_ASSERT(m_blueRect != nullptr);
80+
81+ m_gestureBottom =
82+ m_view->rootObject()->findChild<TouchGestureArea*>("touchGestureAreaBottom");
83+ Q_ASSERT(m_gestureBottom != nullptr);
84+
85+ m_gestureMiddle =
86+ m_view->rootObject()->findChild<TouchGestureArea*>("touchGestureAreaMiddle");
87+ Q_ASSERT(m_gestureMiddle != nullptr);
88+
89+ m_gestureTop =
90+ m_view->rootObject()->findChild<TouchGestureArea*>("touchGestureAreaTop");
91+ Q_ASSERT(m_gestureTop != nullptr);
92+
93+ initGestureComponent(m_gestureBottom);
94+ initGestureComponent(m_gestureMiddle);
95+ initGestureComponent(m_gestureTop);
96+}
97+
98+void tst_TouchGestureArea::minimumTouchPoints()
99+{
100+ m_gestureBottom->setEnabled(true);
101+ m_gestureBottom->setMinimumTouchPoints(4);
102+
103+ QPointF touchPoint = calculateInitialTouchPos(m_gestureBottom);
104+
105+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::WaitingForTouch);
106+ QTest::touchEvent(m_view, m_device).press(0, touchPoint.toPoint());
107+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Undecided);
108+ QTest::touchEvent(m_view, m_device).stationary(0)
109+ .press(1, touchPoint.toPoint());
110+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Undecided);
111+ QTest::touchEvent(m_view, m_device).stationary(0)
112+ .stationary(1)
113+ .press(2, touchPoint.toPoint());
114+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Undecided);
115+ QTest::touchEvent(m_view, m_device).stationary(0)
116+ .stationary(1)
117+ .stationary(2)
118+ .press(3, touchPoint.toPoint());
119+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Recognized);
120+ // test minimum overflow
121+ QTest::touchEvent(m_view, m_device).stationary(0)
122+ .stationary(1)
123+ .stationary(2)
124+ .stationary(3)
125+ .press(4, touchPoint.toPoint());
126+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Recognized);
127+}
128+
129+void tst_TouchGestureArea::maximumTouchPoints()
130+{
131+ m_gestureBottom->setEnabled(true);
132+ m_gestureBottom->setMaximumTouchPoints(2);
133+
134+ QPointF touchPoint = calculateInitialTouchPos(m_gestureBottom);
135+
136+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::WaitingForTouch);
137+ QTest::touchEvent(m_view, m_device).press(0, touchPoint.toPoint());
138+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Recognized);
139+ QTest::touchEvent(m_view, m_device).stationary(0)
140+ .press(1, touchPoint.toPoint());
141+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Recognized);
142+
143+ // test maximum overflow
144+ QTest::touchEvent(m_view, m_device).stationary(0)
145+ .stationary(1)
146+ .press(2, touchPoint.toPoint());
147+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Rejected);
148+ // test still rejected
149+ QTest::touchEvent(m_view, m_device).stationary(0)
150+ .stationary(1)
151+ .press(3, touchPoint.toPoint());
152+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Rejected);
153+}
154+
155+void tst_TouchGestureArea::minimumAndMaximumTouchPoints()
156+{
157+ m_gestureBottom->setEnabled(true);
158+ m_gestureBottom->setMinimumTouchPoints(2);
159+ m_gestureBottom->setMaximumTouchPoints(2);
160+
161+ QPointF touchPoint = calculateInitialTouchPos(m_gestureBottom);
162+
163+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::WaitingForTouch);
164+ QTest::touchEvent(m_view, m_device).press(0, touchPoint.toPoint());
165+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Undecided);
166+ QTest::touchEvent(m_view, m_device).stationary(0)
167+ .press(1, touchPoint.toPoint());
168+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Recognized);
169+
170+ // test maximum overflow
171+ QTest::touchEvent(m_view, m_device).stationary(0)
172+ .stationary(1)
173+ .press(2, touchPoint.toPoint());
174+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Rejected);
175+}
176+
177+void tst_TouchGestureArea::rejectGestureAfterRecognitionPeriod()
178+{
179+ m_gestureBottom->setEnabled(true);
180+ m_gestureBottom->setMinimumTouchPoints(2);
181+ m_gestureBottom->setMaximumTouchPoints(2);
182+
183+ QPointF touchPoint = calculateInitialTouchPos(m_gestureBottom);
184+
185+ QTest::touchEvent(m_view, m_device).press(0, touchPoint.toPoint());
186+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Undecided); // Recognition period is 50.
187+ passTime(40);
188+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Undecided);
189+ passTime(10);
190+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Rejected);
191+}
192+
193+void tst_TouchGestureArea::releaseAndPressRecognisedGestureDoesNotRejectForPeriod()
194+{
195+ m_gestureBottom->setEnabled(true);
196+ m_gestureBottom->setMinimumTouchPoints(2);
197+ m_gestureBottom->setMaximumTouchPoints(2);
198+
199+ bool wasRejected = false;
200+ connect(m_gestureBottom, &TouchGestureArea::statusChanged,
201+ this, [&wasRejected](int status) {
202+ if (status == TouchGestureArea::Rejected) wasRejected = true;
203+ });
204+
205+ QPointF touchPoint = calculateInitialTouchPos(m_gestureBottom);
206+
207+ QTest::touchEvent(m_view, m_device).press(0, touchPoint.toPoint())
208+ .press(1, touchPoint.toPoint());
209+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Recognized);
210+ passTime(5);
211+ QTest::touchEvent(m_view, m_device).stationary(0)
212+ .release(1, touchPoint.toPoint()); // Release period is 100.
213+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Recognized);
214+ passTime(70);
215+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Recognized);
216+ QCOMPARE(wasRejected, false);
217+ passTime(29);
218+ QCOMPARE(wasRejected, false);
219+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Recognized);
220+ passTime(1);
221+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Rejected);
222+}
223+
224+void tst_TouchGestureArea::topAreaReceivesOwnershipFirstWithEqualPoints()
225+{
226+ m_gestureBottom->setEnabled(true);
227+ m_gestureMiddle->setEnabled(true);
228+ m_gestureTop->setEnabled(true);
229+
230+ m_gestureBottom->setMinimumTouchPoints(1);
231+ m_gestureMiddle->setMinimumTouchPoints(1);
232+ m_gestureTop->setMinimumTouchPoints(1);
233+
234+ QPointF touchPoint = calculateInitialTouchPos(m_gestureBottom);
235+
236+ QTest::touchEvent(m_view, m_device).press(0, touchPoint.toPoint());
237+
238+ QCOMPARE((int)m_gestureTop->status(), (int)TouchGestureArea::Recognized);
239+ // Lower items will not recieve the touch events
240+ QCOMPARE((int)m_gestureMiddle->status(), (int)TouchGestureArea::WaitingForTouch);
241+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::WaitingForTouch);
242+}
243+
244+void tst_TouchGestureArea::topAreaReceivesOwnershipFirstWithMorePoints()
245+{
246+ m_gestureBottom->setEnabled(true);
247+ m_gestureMiddle->setEnabled(true);
248+ m_gestureTop->setEnabled(true);
249+
250+ m_gestureBottom->setMinimumTouchPoints(1);
251+ m_gestureMiddle->setMinimumTouchPoints(1);
252+ m_gestureTop->setMinimumTouchPoints(2);
253+
254+ QPointF touchPoint = calculateInitialTouchPos(m_gestureBottom);
255+
256+ QTest::touchEvent(m_view, m_device).press(0, touchPoint.toPoint());
257+ QCOMPARE((int)m_gestureTop->status(), (int)TouchGestureArea::Undecided);
258+ QCOMPARE((int)m_gestureMiddle->status(), (int)TouchGestureArea::Undecided);
259+ // The middle item will accept the events; so the bottom item will not get a chance.
260+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::WaitingForTouch);
261+
262+ QTest::touchEvent(m_view, m_device).stationary(0)
263+ .press(1, touchPoint.toPoint());
264+ QCOMPARE((int)m_gestureTop->status(), (int)TouchGestureArea::Recognized);
265+ QCOMPARE((int)m_gestureMiddle->status(), (int)TouchGestureArea::Rejected);
266+}
267+
268+QTEST_MAIN(tst_TouchGestureArea)
269+
270+#include "tst_TouchGestureArea.moc"
tests/plugins/Ubuntu/Gestures/tst_TouchGestureArea.qmlnew file+90
@@ -0,0 +1,90 @@
1+/*
2+ * Copyright (C) 2016 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.4
18+import Ubuntu.Components 1.3
19+import Ubuntu.Gestures 0.1
20+import Unity.Test 0.1
21+
22+Rectangle {
23+ width: units.gu(60)
24+ height: units.gu(60)
25+ color: "white"
26+
27+ Binding {
28+ target: MouseTouchAdaptor
29+ property: "enabled"
30+ value: true
31+ }
32+
33+ Rectangle {
34+ id: blueRect
35+ objectName: "blueRect"
36+ color: "blue"
37+ width: units.gu(50)
38+ height: units.gu(50)
39+
40+ Timer {
41+ id: resetColorTimer
42+ interval: 300
43+ repeat: false
44+ onTriggered: {
45+ blueRect.color = "blue";
46+ }
47+ }
48+
49+ function blink(color) {
50+ blueRect.color = color;
51+ resetColorTimer.start();
52+ }
53+
54+ TouchGestureArea {
55+ anchors.fill: parent
56+ objectName: "touchGestureAreaBottom"
57+ minimumTouchPoints: 1
58+
59+ onStatusChanged: {
60+ if (status == TouchGestureArea.Recognized) {
61+ blueRect.blink("red");
62+ }
63+ }
64+ }
65+
66+ TouchGestureArea {
67+ anchors.fill: parent
68+ objectName: "touchGestureAreaMiddle"
69+ minimumTouchPoints: 2
70+
71+ onStatusChanged: {
72+ if (status == TouchGestureArea.Recognized) {
73+ blueRect.blink("yellow");
74+ }
75+ }
76+ }
77+
78+ TouchGestureArea {
79+ anchors.fill: parent
80+ objectName: "touchGestureAreaTop"
81+ minimumTouchPoints: 3
82+
83+ onStatusChanged: {
84+ if (status == TouchGestureArea.Recognized) {
85+ blueRect.blink("green");
86+ }
87+ }
88+ }
89+ }
90+}
tests/plugins/Unity/Launcher/launchermodeltest.cpp+1
@@ -48,6 +48,7 @@ public:
48 QString comment() const override { return "this is a mock"; }
48 QString comment() const override { return "this is a mock"; }
49 QUrl icon() const override { return QUrl(); }
49 QUrl icon() const override { return QUrl(); }
50 ApplicationInfoInterface::Stage stage() const override { return ApplicationInfoInterface::MainStage; }
50 ApplicationInfoInterface::Stage stage() const override { return ApplicationInfoInterface::MainStage; }
51+ void setStage(ApplicationInfoInterface::Stage) override {}
51 ApplicationInfoInterface::State state() const override { return ApplicationInfoInterface::Running; }
52 ApplicationInfoInterface::State state() const override { return ApplicationInfoInterface::Running; }
52 bool focused() const override { return m_focused; }
53 bool focused() const override { return m_focused; }
53 QString splashTitle() const override { return QString(); }
54 QString splashTitle() const override { return QString(); }
tests/qmltests/Stages/tst_TabletStage.qml+143
@@ -20,6 +20,7 @@ import Ubuntu.Components 1.3
20import Ubuntu.Components.ListItems 1.3 as ListItem
20import Ubuntu.Components.ListItems 1.3 as ListItem
21import Unity.Application 0.1
21import Unity.Application 0.1
22import Unity.Test 0.1
22import Unity.Test 0.1
23+import Utils 0.1
23
24
24import ".."
25import ".."
25import "../../../qml/Stages"
26import "../../../qml/Stages"
@@ -78,6 +79,7 @@ Rectangle {
78 Column {
79 Column {
79 anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
80 anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
80 spacing: units.gu(1)
81 spacing: units.gu(1)
82+
81 EdgeBarrierControls {
83 EdgeBarrierControls {
82 id: edgeBarrierControls
84 id: edgeBarrierControls
83 text: "Drag here to pull out spread"
85 text: "Drag here to pull out spread"
@@ -87,6 +89,16 @@ Rectangle {
87 edgeBarrierControls.target = testCase.findChild(tabletStageLoader, "edgeBarrierController");
89 edgeBarrierControls.target = testCase.findChild(tabletStageLoader, "edgeBarrierController");
88 }
90 }
89 }
91 }
92+
93+ Button {
94+ text: testCase.sideStage ? testCase.sideStage.shown ? "Hide Side-stage" : "Show Side-stage" : ""
95+ enabled: testCase.sideStage
96+ onClicked: {
97+ if (testCase.sideStage.shown) testCase.sideStage.hide();
98+ else testCase.sideStage.show();
99+ }
100+ }
101+
90 ApplicationCheckBox {
102 ApplicationCheckBox {
91 id: webbrowserCheckBox
103 id: webbrowserCheckBox
92 appId: "webbrowser-app"
104 appId: "webbrowser-app"
@@ -112,6 +124,7 @@ Rectangle {
112 when: windowShown
124 when: windowShown
113
125
114 property Item tabletStage: tabletStageLoader.status === Loader.Ready ? tabletStageLoader.item : null
126 property Item tabletStage: tabletStageLoader.status === Loader.Ready ? tabletStageLoader.item : null
127+ property Item sideStage: tabletStage ? findChild(tabletStage, "sideStage") : null
115
128
116 function init() {
129 function init() {
117 tabletStageLoader.active = true;
130 tabletStageLoader.active = true;
@@ -129,6 +142,7 @@ Rectangle {
129 }
142 }
130
143
131 waitUntilAppSurfaceShowsUp("unity8-dash");
144 waitUntilAppSurfaceShowsUp("unity8-dash");
145+ sideStage.hideNow()
132 }
146 }
133
147
134 function cleanup() {
148 function cleanup() {
@@ -148,6 +162,7 @@ Rectangle {
148 galleryCheckBox.checked = false;
162 galleryCheckBox.checked = false;
149 dialerCheckBox.checked = false;
163 dialerCheckBox.checked = false;
150 facebookCheckBox.checked = false;
164 facebookCheckBox.checked = false;
165+ WindowStateStorage.clear();
151 }
166 }
152
167
153 function waitUntilAppSurfaceShowsUp(appId) {
168 function waitUntilAppSurfaceShowsUp(appId) {
@@ -187,6 +202,8 @@ Rectangle {
187 }
202 }
188
203
189 function test_tappingSwitchesFocusBetweenStages() {
204 function test_tappingSwitchesFocusBetweenStages() {
205+ WindowStateStorage.saveStage(dialerCheckBox.appId, ApplicationInfoInterface.SideStage)
206+
190 webbrowserCheckBox.checked = true;
207 webbrowserCheckBox.checked = true;
191 waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId);
208 waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId);
192 var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
209 var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
@@ -222,6 +239,8 @@ Rectangle {
222 }
239 }
223
240
224 function test_closeAppInSideStage() {
241 function test_closeAppInSideStage() {
242+ WindowStateStorage.saveStage(dialerCheckBox.appId, ApplicationInfoInterface.SideStage)
243+
225 dialerCheckBox.checked = true;
244 dialerCheckBox.checked = true;
226 waitUntilAppSurfaceShowsUp(dialerCheckBox.appId);
245 waitUntilAppSurfaceShowsUp(dialerCheckBox.appId);
227
246
@@ -293,6 +312,8 @@ Rectangle {
293
312
294
313
295 function test_foregroundMainAndSideStageAppsAreKeptRunning() {
314 function test_foregroundMainAndSideStageAppsAreKeptRunning() {
315+ WindowStateStorage.saveStage(facebookCheckBox.appId, ApplicationInfoInterface.SideStage)
316+ WindowStateStorage.saveStage(dialerCheckBox.appId, ApplicationInfoInterface.SideStage)
296
317
297 var stagesPriv = findInvisibleChild(tabletStage, "stagesPriv");
318 var stagesPriv = findInvisibleChild(tabletStage, "stagesPriv");
298 verify(stagesPriv);
319 verify(stagesPriv);
@@ -351,6 +372,8 @@ Rectangle {
351 }
372 }
352
373
353 function test_foregroundAppsAreSuspendedWhenStageIsSuspended() {
374 function test_foregroundAppsAreSuspendedWhenStageIsSuspended() {
375+ WindowStateStorage.saveStage(dialerCheckBox.appId, ApplicationInfoInterface.SideStage)
376+
354 webbrowserCheckBox.checked = true;
377 webbrowserCheckBox.checked = true;
355 waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId)
378 waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId)
356 var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
379 var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
@@ -384,5 +407,125 @@ Rectangle {
384 }
407 }
385 tryCompare(spreadView, "phase", 2);
408 tryCompare(spreadView, "phase", 2);
386 }
409 }
410+
411+ function test_threeFingerTapOpensSideStage_data() {
412+ return [
413+ { tag: "1 finger", touchIds: [0], result: false },
414+ { tag: "2 finger", touchIds: [0, 1], result: false },
415+ { tag: "3 finger", touchIds: [0, 1, 2], result: true },
416+ { tag: "4 finger", touchIds: [0, 1, 2, 3], result: false },
417+ ];
418+ }
419+
420+ function test_threeFingerTapOpensSideStage(data) {
421+ multiTouchTap(data.touchIds, tabletStage, tabletStage.width / 2, tabletStage.height / 2);
422+ wait(200);
423+ tryCompare(sideStage, "shown", data.result);
424+ }
425+
426+
427+ function test_threeFingerTapClosesSideStage_data() {
428+ return [
429+ { tag: "1 finger", touchIds: [0], result: true },
430+ { tag: "2 finger", touchIds: [0, 1], result: true },
431+ { tag: "3 finger", touchIds: [0, 1, 2], result: false },
432+ { tag: "4 finger", touchIds: [0, 1, 2, 3], result: true },
433+ ];
434+ }
435+
436+ function test_threeFingerTapClosesSideStage(data) {
437+ sideStage.showNow();
438+
439+ multiTouchTap(data.touchIds, tabletStage, tabletStage.width / 2, tabletStage.height / 2);
440+ wait(200);
441+ tryCompare(sideStage, "shown", data.result);
442+ }
443+
444+ function test_threeFingerDragOpensSidestage() {
445+ multiTouchDragUntil([0,1,2],
446+ tabletStage,
447+ tabletStage.width / 4,
448+ tabletStage.height / 4,
449+ units.gu(1),
450+ 0,
451+ function() { return sideStage.shown; });
452+ }
453+
454+ function test_applicationLoadsInCorrectStage_data() {
455+ return [
456+ { tag: "MainStage", stage: ApplicationInfoInterface.MainStage, mainStageAppId: "webbrowser-app", sideStageAppId: ""},
457+ { tag: "SideStage", stage: ApplicationInfoInterface.SideStage, mainStageAppId: "unity8-dash", sideStageAppId: "webbrowser-app" },
458+ ];
459+ }
460+
461+ function test_applicationLoadsInCorrectStage(data) {
462+ WindowStateStorage.saveStage(webbrowserCheckBox.appId, data.stage)
463+
464+ var stagesPriv = findInvisibleChild(tabletStage, "stagesPriv");
465+ verify(stagesPriv);
466+
467+ tryCompare(stagesPriv, "mainStageAppId", "unity8-dash");
468+ tryCompare(stagesPriv, "sideStageAppId", "");
469+
470+ webbrowserCheckBox.checked = true;
471+ waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId)
472+
473+ tryCompare(stagesPriv, "mainStageAppId", data.mainStageAppId);
474+ tryCompare(stagesPriv, "sideStageAppId", data.sideStageAppId);
475+ }
476+
477+ function test_loadSideStageByDragginFromMainStage() {
478+ sideStage.showNow();
479+ webbrowserCheckBox.checked = true;
480+ waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId);
481+
482+ var appDelegate = findChild(tabletStage, "tabletSpreadDelegate_" + webbrowserCheckBox.appId);
483+ verify(appDelegate);
484+ compare(appDelegate.stage, ApplicationInfoInterface.MainStage);
485+
486+ var pos = tabletStage.width - sideStage.width - (tabletStage.width - sideStage.width) / 2;
487+ var end_pos = tabletStage.width - sideStage.width / 2;
488+
489+ multiTouchDragUntil([0,1,2],
490+ tabletStage,
491+ pos,
492+ tabletStage.height / 2,
493+ units.gu(3),
494+ 0,
495+ function() {
496+ pos += units.gu(3);
497+ return sideStage.shown && !sideStage.showAnimation.running &&
498+ pos >= end_pos;
499+ });
500+
501+ tryCompare(appDelegate, "stage", ApplicationInfoInterface.SideStage);
502+ }
503+
504+ function test_unloadSideStageByDragginFromStageStage() {
505+ sideStage.showNow();
506+ WindowStateStorage.saveStage(webbrowserCheckBox.appId, ApplicationInfoInterface.SideStage)
507+ webbrowserCheckBox.checked = true;
508+ waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId);
509+
510+ var appDelegate = findChild(tabletStage, "tabletSpreadDelegate_" + webbrowserCheckBox.appId);
511+ verify(appDelegate);
512+ compare(appDelegate.stage, ApplicationInfoInterface.SideStage);
513+
514+ var pos = tabletStage.width - sideStage.width / 2;
515+ var end_pos = tabletStage.width - sideStage.width - (tabletStage.width - sideStage.width) / 2;
516+
517+ multiTouchDragUntil([0,1,2],
518+ tabletStage,
519+ pos,
520+ tabletStage.height / 2,
521+ -units.gu(3),
522+ 0,
523+ function() {
524+ pos -= units.gu(3);
525+ return pos <= end_pos;
526+ });
527+
528+ tryCompare(appDelegate, "stage", ApplicationInfoInterface.MainStage);
529+ }
387 }
530 }
388}
531}
tests/qmltests/tst_OrientedShell.qml+55−11
@@ -232,6 +232,9 @@ Rectangle {
232 onClicked: {
232 onClicked: {
233 orientedShellLoader.item.physicalOrientation = root.physicalOrientation0;
233 orientedShellLoader.item.physicalOrientation = root.physicalOrientation0;
234 }
234 }
235+ color: orientedShellLoader.item && orientedShellLoader.item.physicalOrientation === root.physicalOrientation0 ?
236+ UbuntuColors.green :
237+ __styleInstance.defaultColor
235 }
238 }
236 Button {
239 Button {
237 id: rotate90Button
240 id: rotate90Button
@@ -240,6 +243,9 @@ Rectangle {
240 onClicked: {
243 onClicked: {
241 orientedShellLoader.item.physicalOrientation = root.physicalOrientation90;
244 orientedShellLoader.item.physicalOrientation = root.physicalOrientation90;
242 }
245 }
246+ color: orientedShellLoader.item && orientedShellLoader.item.physicalOrientation === root.physicalOrientation90 ?
247+ UbuntuColors.green :
248+ __styleInstance.defaultColor
243 }
249 }
244 Button {
250 Button {
245 id: rotate180Button
251 id: rotate180Button
@@ -248,6 +254,9 @@ Rectangle {
248 onClicked: {
254 onClicked: {
249 orientedShellLoader.item.physicalOrientation = root.physicalOrientation180;
255 orientedShellLoader.item.physicalOrientation = root.physicalOrientation180;
250 }
256 }
257+ color: orientedShellLoader.item && orientedShellLoader.item.physicalOrientation === root.physicalOrientation180 ?
258+ UbuntuColors.green :
259+ __styleInstance.defaultColor
251 }
260 }
252 Button {
261 Button {
253 id: rotate270Button
262 id: rotate270Button
@@ -256,6 +265,9 @@ Rectangle {
256 onClicked: {
265 onClicked: {
257 orientedShellLoader.item.physicalOrientation = root.physicalOrientation270;
266 orientedShellLoader.item.physicalOrientation = root.physicalOrientation270;
258 }
267 }
268+ color: orientedShellLoader.item && orientedShellLoader.item.physicalOrientation === root.physicalOrientation270 ?
269+ UbuntuColors.green :
270+ __styleInstance.defaultColor
259 }
271 }
260 RowLayout {
272 RowLayout {
261 Layout.fillWidth: true
273 Layout.fillWidth: true
@@ -471,14 +483,46 @@ Rectangle {
471 tearDown();
483 tearDown();
472 }
484 }
473
485
474− function test_appSupportingOnlyPrimaryOrientationMakesShellStayPut_data() {
486+ function test_appSupportingOnlyPrimaryOrientationMakesPhoneShellStayPut() {
487+ loadShell("mako");
488+
489+ // unity8-dash supports only primary orientation and should be already running
490+ var dashAppWindow = findChild(shell, "appWindow_unity8-dash");
491+ verify(dashAppWindow);
492+ compare(ApplicationManager.focusedApplicationId, "unity8-dash");
493+ var dashApp = dashAppWindow.application
494+ verify(dashApp);
495+ compare(dashApp.rotatesWindowContents, false);
496+ compare(dashApp.supportedOrientations, Qt.PrimaryOrientation);
497+ compare(dashApp.stage, ApplicationInfoInterface.MainStage);
498+
499+ tryCompareFunction(function(){return dashApp.session.lastSurface != null;}, true);
500+ verify(checkAppSurfaceOrientation(dashAppWindow, dashApp, root.primaryOrientationAngle));
501+
502+ compare(shell.transformRotationAngle, root.primaryOrientationAngle);
503+ rotateTo(90);
504+
505+ verify(checkAppSurfaceOrientation(dashAppWindow, dashApp, root.primaryOrientationAngle));
506+ compare(shell.transformRotationAngle, root.primaryOrientationAngle);
507+
508+ rotateTo(180);
509+
510+ verify(checkAppSurfaceOrientation(dashAppWindow, dashApp, root.primaryOrientationAngle));
511+ compare(shell.transformRotationAngle, root.primaryOrientationAngle);
512+
513+ rotateTo(270);
514+
515+ verify(checkAppSurfaceOrientation(dashAppWindow, dashApp, root.primaryOrientationAngle));
516+ compare(shell.transformRotationAngle, root.primaryOrientationAngle);
517+ }
518+
519+ function test_appSupportingOnlyPrimaryOrientationWillOnlyRotateInLandscape_data() {
475 return [
520 return [
476− {tag: "mako", deviceName: "mako"},
477 {tag: "manta", deviceName: "manta"},
521 {tag: "manta", deviceName: "manta"},
478 {tag: "flo", deviceName: "flo"}
522 {tag: "flo", deviceName: "flo"}
479 ];
523 ];
480 }
524 }
481− function test_appSupportingOnlyPrimaryOrientationMakesShellStayPut(data) {
525+ function test_appSupportingOnlyPrimaryOrientationWillOnlyRotateInLandscape(data) {
482 loadShell(data.deviceName);
526 loadShell(data.deviceName);
483
527
484 // unity8-dash supports only primary orientation and should be already running
528 // unity8-dash supports only primary orientation and should be already running
@@ -502,13 +546,13 @@ Rectangle {
502
546
503 rotateTo(180);
547 rotateTo(180);
504
548
505− verify(checkAppSurfaceOrientation(dashAppWindow, dashApp, root.primaryOrientationAngle));
549+ verify(checkAppSurfaceOrientation(dashAppWindow, dashApp, root.primaryOrientationAngle + 180));
506− compare(shell.transformRotationAngle, root.primaryOrientationAngle);
550+ compare(shell.transformRotationAngle, root.primaryOrientationAngle + 180);
507
551
508 rotateTo(270);
552 rotateTo(270);
509
553
510− verify(checkAppSurfaceOrientation(dashAppWindow, dashApp, root.primaryOrientationAngle));
554+ verify(checkAppSurfaceOrientation(dashAppWindow, dashApp, root.primaryOrientationAngle + 180));
511− compare(shell.transformRotationAngle, root.primaryOrientationAngle);
555+ compare(shell.transformRotationAngle, root.primaryOrientationAngle + 180);
512 }
556 }
513
557
514 function test_greeterRemainsInPrimaryOrientation_data() {
558 function test_greeterRemainsInPrimaryOrientation_data() {
@@ -813,12 +857,12 @@ Rectangle {
813
857
814 var twitterApp = ApplicationManager.startApplication("twitter-webapp");
858 var twitterApp = ApplicationManager.startApplication("twitter-webapp");
815 verify(twitterApp);
859 verify(twitterApp);
860+ twitterApp.stage = ApplicationInfoInterface.SideStage;
816
861
817 // ensure the mock twitter-webapp is as we expect
862 // ensure the mock twitter-webapp is as we expect
818 compare(twitterApp.rotatesWindowContents, false);
863 compare(twitterApp.rotatesWindowContents, false);
819 compare(twitterApp.supportedOrientations, Qt.PortraitOrientation | Qt.LandscapeOrientation
864 compare(twitterApp.supportedOrientations, Qt.PortraitOrientation | Qt.LandscapeOrientation
820 | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
865 | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
821− compare(twitterApp.stage, ApplicationInfoInterface.SideStage);
822
866
823 // Wait until spreadRepeaterConnections hs caught the new SpreadDelegate and
867 // Wait until spreadRepeaterConnections hs caught the new SpreadDelegate and
824 // set up the signalSpy target accordingly.
868 // set up the signalSpy target accordingly.
@@ -852,11 +896,11 @@ Rectangle {
852
896
853 var dialerApp = ApplicationManager.startApplication("dialer-app");
897 var dialerApp = ApplicationManager.startApplication("dialer-app");
854 verify(dialerApp);
898 verify(dialerApp);
899+ dialerApp.stage = ApplicationInfoInterface.SideStage;
855
900
856 // ensure the mock dialer-app is as we expect
901 // ensure the mock dialer-app is as we expect
857 compare(dialerApp.rotatesWindowContents, false);
902 compare(dialerApp.rotatesWindowContents, false);
858 compare(dialerApp.supportedOrientations, Qt.PortraitOrientation | Qt.InvertedPortraitOrientation);
903 compare(dialerApp.supportedOrientations, Qt.PortraitOrientation | Qt.InvertedPortraitOrientation);
859− compare(dialerApp.stage, ApplicationInfoInterface.SideStage);
860
904
861 tryCompareFunction(function(){ return dialerDelegate != null; }, true);
905 tryCompareFunction(function(){ return dialerDelegate != null; }, true);
862 tryCompare(dialerDelegate, "orientationChangesEnabled", true);
906 tryCompare(dialerDelegate, "orientationChangesEnabled", true);
@@ -899,11 +943,11 @@ Rectangle {
899
943
900 var dialerApp = ApplicationManager.startApplication("dialer-app");
944 var dialerApp = ApplicationManager.startApplication("dialer-app");
901 verify(dialerApp);
945 verify(dialerApp);
946+ dialerApp.stage = ApplicationInfoInterface.SideStage;
902
947
903 // ensure the mock dialer-app is as we expect
948 // ensure the mock dialer-app is as we expect
904 compare(dialerApp.rotatesWindowContents, false);
949 compare(dialerApp.rotatesWindowContents, false);
905 compare(dialerApp.supportedOrientations, Qt.PortraitOrientation | Qt.InvertedPortraitOrientation);
950 compare(dialerApp.supportedOrientations, Qt.PortraitOrientation | Qt.InvertedPortraitOrientation);
906− compare(dialerApp.stage, ApplicationInfoInterface.SideStage);
907
951
908 tryCompareFunction(function(){ return dialerDelegate != null; }, true);
952 tryCompareFunction(function(){ return dialerDelegate != null; }, true);
909 waitUntilAppDelegateIsFullyInit(dialerDelegate);
953 waitUntilAppDelegateIsFullyInit(dialerDelegate);
@@ -919,12 +963,12 @@ Rectangle {
919
963
920 var twitterApp = ApplicationManager.startApplication("twitter-webapp");
964 var twitterApp = ApplicationManager.startApplication("twitter-webapp");
921 verify(twitterApp);
965 verify(twitterApp);
966+ twitterApp.stage = ApplicationInfoInterface.SideStage;
922
967
923 // ensure the mock twitter-webapp is as we expect
968 // ensure the mock twitter-webapp is as we expect
924 compare(twitterApp.rotatesWindowContents, false);
969 compare(twitterApp.rotatesWindowContents, false);
925 compare(twitterApp.supportedOrientations, Qt.PortraitOrientation | Qt.LandscapeOrientation
970 compare(twitterApp.supportedOrientations, Qt.PortraitOrientation | Qt.LandscapeOrientation
926 | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
971 | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
927− compare(twitterApp.stage, ApplicationInfoInterface.SideStage);
928
972
929 tryCompareFunction(function(){ return twitterDelegate != null; }, true);
973 tryCompareFunction(function(){ return twitterDelegate != null; }, true);
930 waitUntilAppDelegateIsFullyInit(twitterDelegate);
974 waitUntilAppDelegateIsFullyInit(twitterDelegate);
tests/utils/modules/Unity/Test/UnityTestCase.qml+67−11
@@ -313,7 +313,10 @@ TestCase {
313 // perform a drag in the given direction until the given condition is true
313 // perform a drag in the given direction until the given condition is true
314 // The condition is a function to be evaluated after every step
314 // The condition is a function to be evaluated after every step
315 function touchDragUntil(item, startX, startY, stepX, stepY, condition) {
315 function touchDragUntil(item, startX, startY, stepX, stepY, condition) {
316+ multiTouchDragUntil([0], item, startX, startY, stepX, stepY, condition);
317+ }
316
318
319+ function multiTouchDragUntil(touchIds, item, startX, startY, stepX, stepY, condition) {
317 var root = fetchRootItem(item);
320 var root = fetchRootItem(item);
318 var pos = item.mapToItem(root, startX, startY);
321 var pos = item.mapToItem(root, startX, startY);
319
322
@@ -326,7 +329,9 @@ TestCase {
326 stepY = stepEnd.y - stepStart.y;
329 stepY = stepEnd.y - stepStart.y;
327
330
328 var event = touchEvent(item)
331 var event = touchEvent(item)
332+ for (var i = 0; i < touchIds.length; i++) {
329− event.press(0 /* touchId */, pos.x, pos.y)
333+ event.press(touchIds[i], pos.x, pos.y)
334+ }
330 event.commit()
335 event.commit()
331
336
332 // we have to stop at some point
337 // we have to stop at some point
@@ -341,23 +346,30 @@ TestCase {
341 pos.y += stepY;
346 pos.y += stepY;
342
347
343 event = touchEvent(item);
348 event = touchEvent(item);
349+ for (i = 0; i < touchIds.length; i++) {
344− event.move(0 /* touchId */, pos.x, pos.y);
350+ event.move(touchIds[i], pos.x, pos.y);
351+ }
345 event.commit();
352 event.commit();
346
353
347 stepsDone += 1;
354 stepsDone += 1;
348 }
355 }
349
356
350 event = touchEvent(item)
357 event = touchEvent(item)
358+ for (i = 0; i < touchIds.length; i++) {
351− event.release(0 /* touchId */, pos.x, pos.y)
359+ event.release(touchIds[i], pos.x, pos.y)
360+ }
352 event.commit()
361 event.commit()
353 }
362 }
354
363
355− function touchMove(item, tox, toy) {
364+ function touchMove(item, tox, toy) { multiTouchMove(0, item, tox, toy); }
365+
366+ function multiTouchMove(touchId, item, tox, toy) {
367+ if (typeof touchId !== "number") touchId = 0;
356 var root = fetchRootItem(item)
368 var root = fetchRootItem(item)
357 var rootPoint = item.mapToItem(root, tox, toy)
369 var rootPoint = item.mapToItem(root, tox, toy)
358
370
359 var event = touchEvent(item);
371 var event = touchEvent(item);
360− event.move(0 /* touchId */, rootPoint.x, rootPoint.y);
372+ event.move(touchId, rootPoint.x, rootPoint.y);
361 event.commit();
373 event.commit();
362 }
374 }
363
375
@@ -394,21 +406,55 @@ TestCase {
394 return item
406 return item
395 }
407 }
396
408
397− function touchPress(item, x, y) {
409+ function touchPress(item, x, y) { touchPress(0, item, x, y, []); }
410+
411+ /*! \brief Release a touch point
412+
413+ \param touchId The touchId to be pressed
414+ \param item The item
415+ \param x The x coordinate of the press, defaults to horizontal center
416+ \param y The y coordinate of the press, defaults to vertical center
417+ \param stationaryPoints An array of touchIds which are "already touched"
418+ */
419+ function multiTouchPress(touchId, item, x, y, stationaryPoints) {
420+ if (typeof touchId !== "number") touchId = 0;
421+ if (typeof x !== "number") x = item.width / 2;
422+ if (typeof y !== "number") y = item.height / 2;
423+ if (typeof stationaryPoints !== "object") stationaryPoints = []
398 var root = fetchRootItem(item)
424 var root = fetchRootItem(item)
399 var rootPoint = item.mapToItem(root, x, y)
425 var rootPoint = item.mapToItem(root, x, y)
400
426
401 var event = touchEvent(item)
427 var event = touchEvent(item)
402− event.press(0 /* touchId */, rootPoint.x, rootPoint.y)
428+ event.press(touchId, rootPoint.x, rootPoint.y)
429+ for (var i = 0; i < stationaryPoints.length; i++) {
430+ event.stationary(stationaryPoints[i]);
431+ }
403 event.commit()
432 event.commit()
404 }
433 }
405
434
406− function touchRelease(item, x, y) {
435+ function touchRelease(item, x, y) { multiTouchRelease(0, item, x, y, []); }
436+
437+ /*! \brief Release a touch point
438+
439+ \param touchId The touchId to be released
440+ \param item The item
441+ \param x The x coordinate of the release, defaults to horizontal center
442+ \param y The y coordinate of the release, defaults to vertical center
443+ \param stationaryPoints An array of touchIds which are "still touched"
444+ */
445+ function multiTouchRelease(touchId, item, x, y, stationaryPoints) {
446+ if (typeof touchId !== "number") touchId = 0;
447+ if (typeof x !== "number") x = item.width / 2;
448+ if (typeof y !== "number") y = item.height / 2;
449+ if (typeof stationaryPoints !== "object") stationaryPoints = []
407 var root = fetchRootItem(item)
450 var root = fetchRootItem(item)
408 var rootPoint = item.mapToItem(root, x, y)
451 var rootPoint = item.mapToItem(root, x, y)
409
452
410 var event = touchEvent(item)
453 var event = touchEvent(item)
411− event.release(0 /* touchId */, rootPoint.x, rootPoint.y)
454+ event.release(touchId, rootPoint.x, rootPoint.y)
455+ for (var i = 0; i < stationaryPoints.length; i++) {
456+ event.stationary(stationaryPoints[i]);
457+ }
412 event.commit()
458 event.commit()
413 }
459 }
414
460
@@ -419,6 +465,11 @@ TestCase {
419 \param y The y coordinate of the tap, defaults to vertical center
465 \param y The y coordinate of the tap, defaults to vertical center
420 */
466 */
421 function tap(item, x, y) {
467 function tap(item, x, y) {
468+ multiTouchTap([0], item, x, y);
469+ }
470+
471+ function multiTouchTap(touchIds, item, x, y) {
472+ if (typeof touchIds !== "object") touchIds = [0];
422 if (typeof x !== "number") x = item.width / 2;
473 if (typeof x !== "number") x = item.width / 2;
423 if (typeof y !== "number") y = item.height / 2;
474 if (typeof y !== "number") y = item.height / 2;
424
475
@@ -426,14 +477,19 @@ TestCase {
426 var rootPoint = item.mapToItem(root, x, y)
477 var rootPoint = item.mapToItem(root, x, y)
427
478
428 var event = touchEvent(item)
479 var event = touchEvent(item)
480+ for (var i = 0; i < touchIds.length; i++) {
429− event.press(0 /* touchId */, rootPoint.x, rootPoint.y)
481+ event.press(touchIds[i], rootPoint.x, rootPoint.y)
482+ }
430 event.commit()
483 event.commit()
431
484
432 event = touchEvent(item)
485 event = touchEvent(item)
486+ for (i = 0; i < touchIds.length; i++) {
433− event.release(0 /* touchId */, rootPoint.x, rootPoint.y)
487+ event.release(touchIds[i], rootPoint.x, rootPoint.y)
488+ }
434 event.commit()
489 event.commit()
435 }
490 }
436
491
492+
437 Component.onCompleted: {
493 Component.onCompleted: {
438 var rootItem = parent;
494 var rootItem = parent;
439 while (rootItem.parent != undefined) {
495 while (rootItem.parent != undefined) {