on xenial help sign in
Sidestage load/unload redesign
commitbc2da59d559d0a0ed1b81499994b249cf99f0133 authorNick Dedekind <nick.dedekind@canonical.com> dateFri 11 Mar 2016, 21:18 (11 years ago) parent8855d4b
55 files changed+3650−425
CMakeLists.txt+1−1 CODING+2 debian/control+4−4 libs/UbuntuGestures/Timer.h+5 libs/UbuntuGestures/TouchRegistry.cpp+15−10 plugins/Cursor/Cursor.qmltypes+78 plugins/GlobalShortcut/GlobalShortcut.qmltypes+31 plugins/Ubuntu/Gestures/CMakeLists.txt+1 plugins/Ubuntu/Gestures/DirectionalDragArea.cpp+3 plugins/Ubuntu/Gestures/Gestures.qmltypes+86−45 plugins/Ubuntu/Gestures/TouchGestureArea.cpp+875 plugins/Ubuntu/Gestures/TouchGestureArea.h+229 plugins/Ubuntu/Gestures/plugin.cpp+3 plugins/Unity/InputInfo/InputInfo.qmltypes+71 plugins/Unity/Platform/Platform.qmltypes+20 plugins/Utils/CMakeLists.txt+1 plugins/Utils/globalfunctions.cpp+56 plugins/Utils/globalfunctions.h+42 plugins/Utils/plugin.cpp+9 plugins/Utils/windowstatestorage.cpp+36−4 plugins/Utils/windowstatestorage.h+3 qml/Components/Showable.qml+10 qml/Shell.qml+2−6 qml/Stages/AbstractStage.qml+1−1 qml/Stages/ApplicationWindow.qml+3−1 qml/Stages/SideStage.qml+118 qml/Stages/SpreadDelegate.qml+10−1 qml/Stages/SurfaceContainer.qml+1 qml/Stages/TabletSideStageTouchGesture.qml+154 qml/Stages/TabletStage.qml+387−171 qml/Stages/TransformedTabletSpreadDelegate.qml+84−56 qml/Stages/graphics/sidestage_drag.svg+207 qml/Stages/graphics/sidestage_open.svg+181 src/MouseTouchAdaptor.cpp+53−9 src/MouseTouchAdaptor.h+5−3 tests/libs/UbuntuGestures/tst_TouchRegistry.cpp+59 tests/mocks/Unity/Application/ApplicationManager.cpp+10−25 tests/mocks/Unity/Application/ApplicationManager.h−8 tests/mocks/Unity/Application/MirSurfaceItem.cpp+17 tests/mocks/Unity/Application/MirSurfaceItem.h+1 tests/mocks/Unity/Application/resources/MirSurfaceItem.qml+59 tests/mocks/Utils/CMakeLists.txt+1 tests/mocks/Utils/plugin.cpp+9 tests/mocks/Utils/windowstatestorage.cpp+13 tests/mocks/Utils/windowstatestorage.h+4 tests/plugins/Ubuntu/Gestures/CMakeLists.txt+7−1 tests/plugins/Ubuntu/Gestures/GestureTest.cpp+44 tests/plugins/Ubuntu/Gestures/GestureTest.h+13−1 tests/plugins/Ubuntu/Gestures/tst_DirectionalDragArea.cpp−56 tests/plugins/Ubuntu/Gestures/tst_TouchGestureArea.cpp+270 tests/plugins/Ubuntu/Gestures/tst_TouchGestureArea.qml+90 tests/plugins/Unity/Launcher/launchermodeltest.cpp+1 tests/qmltests/Stages/tst_TabletStage.qml+143 tests/qmltests/tst_OrientedShell.qml+55−11 tests/utils/modules/Unity/Test/UnityTestCase.qml+67−11
CMakeLists.txt+1−1
@@ -57,7 +57,7 @@ find_package(Qt5DBus 5.4 REQUIRED)
5757find_package(Qt5Concurrent 5.4 REQUIRED)
5858find_package(Qt5Sql 5.4 REQUIRED)
5959
60−pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=13)
60+pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=14)
6161pkg_check_modules(GIO REQUIRED gio-2.0>=2.32)
6262pkg_check_modules(GLIB REQUIRED glib-2.0>=2.32)
6363pkg_check_modules(QMENUMODEL REQUIRED qmenumodel)
CODING+2
@@ -54,6 +54,8 @@ $ ./run.sh -p OR -k
5454Notes
5555-----
5656- 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.
5759- to get the translations work, currently you have to do make install to
5860 put the *.mo files into correct structure. We'll look for a better solution
5961 later.
debian/control+4−4
@@ -29,7 +29,7 @@ Build-Depends: cmake,
2929 libqt5xmlpatterns5-dev,
3030 libsystemsettings-dev,
3131 libudev-dev,
32− libunity-api-dev (>= 7.107),
32+ libunity-api-dev (>= 7.108),
3333 libusermetricsoutput1-dev,
3434# Need those X11 libs touch emulation from mouse events in manual QML tests on a X11 desktop
3535 libx11-dev[!armhf],
@@ -101,7 +101,7 @@ Depends: dmz-cursor-theme,
101101 qml-module-qt-labs-folderlistmodel,
102102 qml-module-qtquick-xmllistmodel,
103103 qml-module-qtsysteminfo,
104− qtdeclarative5-qtmir-plugin (>= 0.4.5),
104+ qtdeclarative5-qtmir-plugin (>= 0.4.8),
105105 qtdeclarative5-ubuntu-telephony0.1,
106106 qtdeclarative5-ubuntu-web-plugin,
107107 ubuntu-system-settings,
@@ -130,7 +130,7 @@ Depends: qml-module-qtquick-layouts,
130130 qtdeclarative5-ubuntu-ui-toolkit-plugin (>= 1.3.1845) | qtdeclarative5-ubuntu-ui-toolkit-plugin-gles (>= 1.3.1845),
131131 qtdeclarative5-unity-notifications-plugin (>= 0.1.2) | unity-notifications-impl,
132132 ubuntu-thumbnailer-impl-0,
133− unity-application-impl-13,
133+ unity-application-impl-14,
134134 unity-notifications-impl-3,
135135 unity-plugin-scopes | unity-scopes-impl,
136136 unity-scopes-impl-10,
@@ -176,7 +176,7 @@ Pre-Depends: ${misc:Pre-Depends},
176176Depends: ${misc:Depends},
177177 ${shlibs:Depends},
178178Provides: unity-application-impl,
179− unity-application-impl-13,
179+ unity-application-impl-14,
180180Replaces: unity8-autopilot (<< 8.02+15.04.20150422-0ubuntu1)
181181Description: Fake environment for running Unity 8 shell
182182 Provides fake implementations of some QML modules used by Unity 8 shell
libs/UbuntuGestures/Timer.h+5
@@ -35,6 +35,11 @@ public:
3535 virtual int interval() const = 0;
3636 virtual void setInterval(int msecs) = 0;
3737 virtual void start() { m_isRunning = true; }
38+ virtual void start(int msecs)
39+ {
40+ setInterval(msecs);
41+ start();
42+ }
3843 virtual void stop() { m_isRunning = false; }
3944 bool isRunning() const { return m_isRunning; }
4045 virtual bool isSingleShot() const = 0;
libs/UbuntuGestures/TouchRegistry.cpp+15−10
@@ -359,10 +359,6 @@ void TouchRegistry::removeCandidateOwnerForTouchByIndex(Pool<TouchRegistry::Touc
359359
360360void TouchRegistry::requestTouchOwnership(int id, QQuickItem *candidate)
361361{
362− #if TOUCHREGISTRY_DEBUG
363− UG_DEBUG << "requestTouchOwnership id " << id << "candidate" << candidate;
364− #endif
365−
366362 Pool<TouchInfo>::Iterator touchInfo = findTouchInfo(id);
367363 if (!touchInfo) { qFatal("TouchRegistry: Failed to find TouchInfo"); }
368364
@@ -379,6 +375,9 @@ void TouchRegistry::requestTouchOwnership(int id, QQuickItem *candidate)
379375 break;
380376 }
381377 }
378+ #if TOUCHREGISTRY_DEBUG
379+ UG_DEBUG << "requestTouchOwnership id " << id << "candidate" << candidate << "index: " << candidateIndex;
380+ #endif
382381
383382 // add it as a candidate if not present yet
384383 if (candidateIndex < 0) {
@@ -533,16 +532,22 @@ void TouchRegistry::TouchInfo::notifyCandidatesOfOwnershipResolution()
533532 << " gained) to candidate" << candidates[0].item;
534533 #endif
535534
536− TouchOwnershipEvent gainedOwnershipEvent(id, true /*gained*/);
537− QCoreApplication::sendEvent(candidates[0].item, &gainedOwnershipEvent);
535+ // need to take a copy of the item list in case
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+ }
538541
542+ TouchOwnershipEvent gainedOwnershipEvent(id, true /*gained*/);
543+ QCoreApplication::sendEvent(items[0], &gainedOwnershipEvent);
539544
540545 TouchOwnershipEvent lostOwnershipEvent(id, false /*gained*/);
541− for (int i = 1; i < candidates.count(); ++i) {
546+ for (int i = 1; i < items.count(); ++i) {
542547 #if TOUCHREGISTRY_DEBUG
543− UG_DEBUG << "sending TouchWonershipEvent(id =" << id << " lost) to candidate"
544− << candidates[i].item;
548+ UG_DEBUG << "sending TouchOwnershipEvent(id =" << id << " lost) to candidate"
549+ << items[i];
545550 #endif
546− QCoreApplication::sendEvent(candidates[i].item, &lostOwnershipEvent);
551+ QCoreApplication::sendEvent(items[i], &lostOwnershipEvent);
547552 }
548553}
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
1111 PressedOutsideNotifier.cpp
1212 TouchDispatcher.cpp
1313 TouchGate.cpp
14+ TouchGestureArea.cpp
1415)
1516
1617add_definitions(-DUBUNTUGESTURESQML_LIBRARY)
plugins/Ubuntu/Gestures/DirectionalDragArea.cpp+3
@@ -606,6 +606,9 @@ void DirectionalDragAreaPrivate::setStatus(Status newStatus)
606606 Q_EMIT q->pressedChanged(true);
607607 break;
608608 case Recognized:
609+ if (oldStatus == WaitingForTouch) { // for immediate recognition
610+ Q_EMIT q->pressedChanged(true);
611+ }
609612 Q_EMIT q->draggingChanged(true);
610613 break;
611614 default:
plugins/Ubuntu/Gestures/Gestures.qmltypes+86−45
@@ -34,7 +34,8 @@ Module {
3434 "Leftwards": 1,
3535 "Downwards": 2,
3636 "Upwards": 3,
37− "Horizontal": 4
37+ "Horizontal": 4,
38+ "Vertical": 5
3839 }
3940 }
4041 Method {
@@ -59,14 +60,6 @@ Module {
5960 prototype: "QQuickItem"
6061 exports: ["Ubuntu.Gestures/DirectionalDragArea 0.1"]
6162 exportMetaObjectRevisions: [0]
62− Enum {
63− name: "Status"
64− values: {
65− "WaitingForTouch": 0,
66− "Undecided": 1,
67− "Recognized": 2
68− }
69− }
7063 Property { name: "direction"; type: "Direction::Type" }
7164 Property { name: "distance"; type: "double"; isReadonly: true }
7265 Property { name: "sceneDistance"; type: "double"; isReadonly: true }
@@ -74,24 +67,19 @@ Module {
7467 Property { name: "touchY"; type: "double"; isReadonly: true }
7568 Property { name: "touchSceneX"; type: "double"; isReadonly: true }
7669 Property { name: "touchSceneY"; type: "double"; isReadonly: true }
77− Property { name: "status"; type: "Status"; isReadonly: true }
7870 Property { name: "dragging"; type: "bool"; isReadonly: true }
79− Property { name: "maxDeviation"; type: "double" }
80− Property { name: "wideningAngle"; type: "double" }
81− Property { name: "distanceThreshold"; type: "double" }
82− Property { name: "minSpeed"; type: "double" }
83− Property { name: "maxSilenceTime"; type: "int" }
84− Property { name: "compositionTime"; type: "int" }
71+ Property { name: "pressed"; type: "bool"; isReadonly: true }
72+ Property { name: "immediateRecognition"; type: "bool" }
8573 Signal {
8674 name: "directionChanged"
8775 Parameter { name: "direction"; type: "Direction::Type" }
8876 }
8977 Signal {
90− name: "statusChanged"
91− Parameter { name: "value"; type: "Status" }
78+ name: "draggingChanged"
79+ Parameter { name: "value"; type: "bool" }
9280 }
9381 Signal {
94− name: "draggingChanged"
82+ name: "pressedChanged"
9583 Parameter { name: "value"; type: "bool" }
9684 }
9785 Signal {
@@ -102,30 +90,6 @@ Module {
10290 name: "sceneDistanceChanged"
10391 Parameter { name: "value"; type: "double" }
10492 }
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− }
12993 Signal {
13094 name: "touchXChanged"
13195 Parameter { name: "value"; type: "double" }
@@ -142,7 +106,35 @@ Module {
142106 name: "touchSceneYChanged"
143107 Parameter { name: "value"; type: "double" }
144108 }
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 }
146138 }
147139 Component {
148140 name: "PressedOutsideNotifier"
@@ -163,6 +155,55 @@ Module {
163155 name: "targetItemChanged"
164156 Parameter { name: "item"; type: "QQuickItem"; isPointer: true }
165157 }
166− Signal { name: "pressed" }
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 {
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" }
167208 }
168209}
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 @@
2121#include "FloatingFlickable.h"
2222#include "PressedOutsideNotifier.h"
2323#include "TouchGate.h"
24+#include "TouchGestureArea.h"
2425
2526#include <qqml.h>
2627
@@ -38,4 +39,6 @@ void UbuntuGesturesQmlPlugin::registerTypes(const char *uri)
3839 qmlRegisterType<FloatingFlickable>(uri, 0, 1, "FloatingFlickable");
3940 qmlRegisterType<PressedOutsideNotifier>(uri, 0, 1, "PressedOutsideNotifier");
4041 qmlRegisterType<TouchGate>(uri, 0, 1, "TouchGate");
42+ qmlRegisterType<TouchGestureArea>(uri, 0, 1, "TouchGestureArea");
43+ qmlRegisterUncreatableType<GestureTouchPoint>(uri, 0, 1, "GestureTouchPoint", "Cannot create GestureTouchPoints");
4144}
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
2424 timezoneFormatter.cpp
2525 inputeventgenerator.cpp
2626 deviceconfigparser.cpp
27+ globalfunctions.cpp
2728 plugin.cpp
2829 )
2930
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 @@
3838#include "applicationsfiltermodel.h"
3939#include "inputeventgenerator.h"
4040#include "deviceconfigparser.h"
41+#include "globalfunctions.h"
4142
4243static QObject *createWindowStateStorage(QQmlEngine *engine, QJSEngine *scriptEngine)
4344{
@@ -53,6 +54,13 @@ static QObject *createConstants(QQmlEngine *engine, QJSEngine *scriptEngine)
5354 return new Constants();
5455}
5556
57+static QObject *createGlobalFunctions(QQmlEngine *engine, QJSEngine *scriptEngine)
58+{
59+ Q_UNUSED(engine)
60+ Q_UNUSED(scriptEngine)
61+ return new GlobalFunctions();
62+}
63+
5664void UtilsPlugin::registerTypes(const char *uri)
5765{
5866 Q_ASSERT(uri == QLatin1String("Utils"));
@@ -72,6 +80,7 @@ void UtilsPlugin::registerTypes(const char *uri)
7280 qmlRegisterType<ApplicationsFilterModel>(uri, 0, 1, "ApplicationsFilterModel");
7381 qmlRegisterType<InputEventGenerator>(uri, 0, 1, "InputEventGenerator");
7482 qmlRegisterType<DeviceConfigParser>(uri, 0, 1, "DeviceConfigParser");
83+ qmlRegisterSingletonType<GlobalFunctions>(uri, 0, 1, "Functions", createGlobalFunctions);
7584}
7685
7786void UtilsPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
plugins/Utils/windowstatestorage.cpp+36−4
@@ -23,9 +23,14 @@
2323#include <QSqlError>
2424#include <QSqlResult>
2525#include <QRect>
26+#include <unity/shell/application/ApplicationInfoInterface.h>
2627
2728QMutex WindowStateStorage::s_mutex;
2829
30+inline QString sanitiseString(QString string) {
31+ return string.remove("\"").remove("'").remove("\\");
32+}
33+
2934WindowStateStorage::WindowStateStorage(QObject *parent):
3035 QObject(parent)
3136{
@@ -50,7 +55,7 @@ WindowStateStorage::~WindowStateStorage()
5055void WindowStateStorage::saveState(const QString &windowId, WindowStateStorage::WindowState state)
5156{
5257 const QString queryString = QStringLiteral("INSERT OR REPLACE INTO state (windowId, state) values ('%1', '%2');")
53− .arg(windowId)
58+ .arg(sanitiseString(windowId))
5459 .arg((int)state);
5560
5661 saveValue(queryString);
@@ -59,7 +64,7 @@ void WindowStateStorage::saveState(const QString &windowId, WindowStateStorage::
5964WindowStateStorage::WindowState WindowStateStorage::getState(const QString &windowId, WindowStateStorage::WindowState defaultValue) const
6065{
6166 const QString queryString = QStringLiteral("SELECT * FROM state WHERE windowId = '%1';")
62− .arg(windowId);
67+ .arg(sanitiseString(windowId));
6368
6469 QSqlQuery query = getValue(queryString);
6570
@@ -72,7 +77,7 @@ WindowStateStorage::WindowState WindowStateStorage::getState(const QString &wind
7277void WindowStateStorage::saveGeometry(const QString &windowId, const QRect rect)
7378{
7479 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))
7681 .arg(rect.x())
7782 .arg(rect.y())
7883 .arg(rect.width())
@@ -81,6 +86,28 @@ void WindowStateStorage::saveGeometry(const QString &windowId, const QRect rect)
8186 saveValue(queryString);
8287}
8388
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+
84111void WindowStateStorage::executeAsyncQuery(const QString &queryString)
85112{
86113 QMutexLocker l(&s_mutex);
@@ -97,7 +124,7 @@ void WindowStateStorage::executeAsyncQuery(const QString &queryString)
97124QRect WindowStateStorage::getGeometry(const QString &windowId, const QRect defaultValue) const
98125{
99126 QString queryString = QStringLiteral("SELECT * FROM geometry WHERE windowId = '%1';")
100− .arg(windowId);
127+ .arg(sanitiseString(windowId));
101128
102129 QSqlQuery query = getValue(queryString);
103130
@@ -124,6 +151,11 @@ void WindowStateStorage::initdb()
124151 QSqlQuery query;
125152 query.exec(QStringLiteral("CREATE TABLE state(windowId TEXT UNIQUE, state INTEGER);"));
126153 }
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+ }
127159}
128160
129161void WindowStateStorage::saveValue(const QString &queryString)
plugins/Utils/windowstatestorage.h+3
@@ -38,6 +38,9 @@ public:
3838 Q_INVOKABLE void saveGeometry(const QString &windowId, const QRect rect);
3939 Q_INVOKABLE QRect getGeometry(const QString &windowId, const QRect defaultValue) const;
4040
41+ Q_INVOKABLE void saveStage(const QString &appId, int stage);
42+ Q_INVOKABLE int getStage(const QString &appId) const;
43+
4144private:
4245 void initdb();
4346
qml/Components/Showable.qml+10
@@ -31,6 +31,7 @@ Item {
3131 property bool required
3232 property bool __shouldShow: false
3333 property bool __skipShowAnimation: false
34+ property bool __skipHideAnimation: false
3435
3536 property list<QtObject> hides
3637 property var showAnimation
@@ -120,12 +121,21 @@ Item {
120121 if (!hideAnimation.running) {
121122 hideAnimation.restart()
122123 }
124+ if (__skipHideAnimation) {
125+ hideAnimation.complete();
126+ }
123127 } else {
124128 visible = false
125129 required = false
126130 }
127131
128132 shown = false
133+ __skipHideAnimation = false;
134+ }
135+
136+ function hideNow() {
137+ __skipHideAnimation = true;
138+ hide();
129139 }
130140
131141 Connections {
qml/Shell.qml+2−6
@@ -120,9 +120,7 @@ Item {
120120 if (ApplicationManager.findApplication(appId)) {
121121 ApplicationManager.requestFocusApplication(appId);
122122 } else {
123− var execFlags = shell.usageScenario === "phone" ? ApplicationManager.ForceMainStage
124− : ApplicationManager.NoFlag;
125− ApplicationManager.startApplication(appId, execFlags);
123+ ApplicationManager.startApplication(appId);
126124 }
127125 }
128126
@@ -547,9 +545,7 @@ Item {
547545 greeterShown: greeter.shown
548546 }
549547
550− readonly property bool topmostApplicationIsFullscreen:
551− ApplicationManager.focusedApplicationId &&
552− ApplicationManager.findApplication(ApplicationManager.focusedApplicationId).fullscreen
548+ readonly property bool topmostApplicationIsFullscreen: mainApp && mainApp.fullscreen
553549
554550 fullscreenMode: (topmostApplicationIsFullscreen && !lightDM.greeter.active && launcher.progress == 0)
555551 || greeter.hasLockedApp
qml/Stages/AbstractStage.qml+1−1
@@ -44,7 +44,7 @@ Rectangle {
4444
4545 // To be read from outside
4646 property var mainApp: null
47− property int mainAppWindowOrientationAngle
47+ property int mainAppWindowOrientationAngle: 0
4848 property bool orientationChangesEnabled
4949 property int supportedOrientations: Qt.PortraitOrientation
5050 | Qt.LandscapeOrientation
qml/Stages/ApplicationWindow.qml+3−1
@@ -24,12 +24,14 @@ FocusScope {
2424 implicitHeight: sessionContainer.implicitHeight
2525
2626 // to be read from outside
27− readonly property bool fullscreen: application ? application.fullscreen : false
2827 property alias interactive: sessionContainer.interactive
2928 property bool orientationChangesEnabled: d.supportsSurfaceResize ? d.surfaceOldEnoughToBeResized : true
3029 readonly property string title: sessionContainer.surface && sessionContainer.surface.name !== "" ?
3130 sessionContainer.surface.name : d.name
3231
32+ // overridable from outside
33+ property bool fullscreen: application ? application.fullscreen : false
34+
3335 // to be set from outside
3436 property QtObject application
3537 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 {
3232 readonly property alias appWindowOrientationAngle: appWindowWithShadow.orientationAngle
3333 readonly property alias appWindowRotation: appWindowWithShadow.rotation
3434 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
3540
3641 // to be set from outside
3742 property bool interactive: true
@@ -45,6 +50,9 @@ FocusScope {
4550 property QtObject orientations
4651 property bool highlightShown: false
4752
53+ // overrideable from outside
54+ property alias fullscreen: appWindow.fullscreen
55+
4856 function matchShellOrientation() {
4957 if (!root.application)
5058 return;
@@ -154,7 +162,8 @@ FocusScope {
154162 if (!root.application || root.application.rotatesWindowContents) {
155163 return 0;
156164 }
157− var supportedOrientations = root.application.supportedOrientations;
165+
166+ var supportedOrientations = root.supportedOrientations;
158167
159168 if (supportedOrientations === Qt.PrimaryOrientation) {
160169 supportedOrientations = root.orientations.primary;
qml/Stages/SurfaceContainer.qml+1
@@ -118,6 +118,7 @@ FocusScope {
118118
119119
120120 TouchGate {
121+ objectName: "touchGate-"+name
121122 targetItem: surfaceItem
122123 anchors.fill: root
123124 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 {
4646 if (delta < 0) { delta += 360; }
4747 delta = delta % 360;
4848
49− var supportedOrientations = spreadDelegate.application.supportedOrientations;
49+ var supportedOrientations = spreadDelegate.supportedOrientations;
5050 if (supportedOrientations === Qt.PrimaryOrientation) {
5151 supportedOrientations = spreadDelegate.orientations.primary;
5252 }
@@ -78,9 +78,24 @@ AbstractStage {
7878
7979 orientationChangesEnabled: priv.mainAppOrientationChangesEnabled
8080
81− supportedOrientations: mainApp ? mainApp.supportedOrientations
82− : (Qt.PortraitOrientation | Qt.LandscapeOrientation
83− | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation)
81+ supportedOrientations: {
82+ if (mainApp) {
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
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 |
96+ Qt.InvertedLandscapeOrientation;
97+ }
98+ }
8499
85100 onWidthChanged: {
86101 spreadView.selectedIndex = -1;
@@ -88,13 +103,6 @@ AbstractStage {
88103 spreadView.contentX = -spreadView.shift;
89104 }
90105
91− onShellOrientationChanged: {
92− if (shellOrientation == Qt.PortraitOrientation || shellOrientation == Qt.InvertedPortraitOrientation) {
93− ApplicationManager.focusApplication(priv.mainStageAppId);
94− priv.sideStageAppId = "";
95− }
96− }
97−
98106 onInverseProgressChanged: {
99107 // This can't be a simple binding because that would be triggered after this handler
100108 // while we need it active before doing the anition left/right
@@ -170,20 +178,7 @@ AbstractStage {
170178
171179 property int highlightIndex: 0
172180
173− onFocusedAppIdChanged: {
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− }
181+ onFocusedAppIdChanged: updateStageApps()
187182
188183 onFocusedAppDelegateChanged: {
189184 if (focusedAppDelegate) {
@@ -229,6 +224,46 @@ AbstractStage {
229224 onHighlightIndexChanged: {
230225 spreadView.contentX = highlightIndex * spreadView.contentWidth / (spreadRepeater.count + 2)
231226 }
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();
232267 }
233268
234269 Connections {
@@ -256,7 +291,8 @@ AbstractStage {
256291 ApplicationManager.focusApplication("unity8-dash")
257292 }
258293 if (priv.sideStageAppId == appId) {
259− priv.sideStageAppId = "";
294+ var app = priv.getTopApp(ApplicationInfoInterface.SideStage);
295+ priv.sideStageAppId = app === null ? "" : app.appId;
260296 }
261297
262298 if (ApplicationManager.count == 0) {
@@ -267,6 +303,7 @@ AbstractStage {
267303 // lets make sure the spread doesn't mess up by the changing app list.
268304 spreadView.phase = 0;
269305 spreadView.contentX = -spreadView.shift;
306+
270307 ApplicationManager.focusApplication(ApplicationManager.get(0).appId);
271308 }
272309 }
@@ -322,6 +359,7 @@ AbstractStage {
322359 property int selectedIndex: -1
323360 property int draggedDelegateCount: 0
324361 property int closingIndex: -1
362+ property var selectedApplication: selectedIndex !== -1 ? ApplicationManager.get(selectedIndex) : null
325363
326364 // FIXME: Workaround Flickable's not keepping its contentX still when resized
327365 onContentXChanged: { forceItToRemainStillIfBeingResized(); }
@@ -343,15 +381,8 @@ AbstractStage {
343381 }
344382 }
345383
346− property bool sideStageDragging: sideStageDragHandle.dragging
347− property real sideStageDragProgress: sideStageDragHandle.progress
348−
349− onSideStageDragProgressChanged: {
350− if (sideStageDragProgress == 1) {
351− ApplicationManager.focusApplication(priv.mainStageAppId);
352− priv.sideStageAppId = "";
353− }
354− }
384+ property real sideStageDragProgress: sideStage.progress
385+ property bool surfaceDragging: triGestureArea.recognisedDrag
355386
356387 // In case the ApplicationManager already holds an app when starting up we're missing animations
357388 // Make sure we end up in the same state
@@ -402,7 +433,7 @@ AbstractStage {
402433 }
403434 ]
404435 state: {
405− if (priv.mainStageAppId && !priv.sideStageAppId) {
436+ if ((priv.mainStageAppId && !priv.sideStageAppId) || !priv.sideStageEnabled) {
406437 return "main";
407438 }
408439 if (!priv.mainStageAppId && priv.sideStageAppId) {
@@ -419,12 +450,26 @@ AbstractStage {
419450 // Flickabe.contentX wiggles during resizes. Don't react to it.
420451 return;
421452 }
422− if (spreadView.phase == 0 && spreadView.shiftedContentX > spreadView.width * spreadView.positionMarker2) {
423− spreadView.phase = 1;
424− } else if (spreadView.phase == 1 && spreadView.shiftedContentX > spreadView.width * spreadView.positionMarker4) {
425− spreadView.phase = 2;
426− } else if (spreadView.phase == 1 && spreadView.shiftedContentX < spreadView.width * spreadView.positionMarker2) {
427− spreadView.phase = 0;
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.
462+ if (root.spreadEnabled && shiftedContentX > width * positionMarker2) {
463+ phase = 1;
464+ }
465+ break;
466+ case 1:
467+ if (shiftedContentX < width * positionMarker2) {
468+ phase = 0;
469+ } else if (shiftedContentX >= width * positionMarker4 && !spreadDragArea.dragging) {
470+ phase = 2;
471+ }
472+ break;
428473 }
429474 }
430475
@@ -446,6 +491,7 @@ AbstractStage {
446491 }
447492
448493 function snapTo(index) {
494+ snapAnimation.stop();
449495 spreadView.selectedIndex = index;
450496 snapAnimation.targetContentX = -shift;
451497 snapAnimation.start();
@@ -455,55 +501,41 @@ AbstractStage {
455501 // We don't want to really reorder them in the model because that allows us to keep track
456502 // of the last focused order.
457503 function indexToZIndex(index) {
504+ // only shuffle when we've got a main and overlay
505+ if (state !== "mainAndOverlay") return index;
506+
458507 var app = ApplicationManager.get(index);
459508 if (!app) {
460509 return index;
461510 }
462511
463− var active = app.appId == priv.mainStageAppId || app.appId == priv.sideStageAppId;
464− if (active && app.stage == ApplicationInfoInterface.MainStage) {
465− // if this app is active, and its the MainStage, always put it to index 0
512+ // don't shuffle indexes greater than "actives or next"
513+ if (index > 2) return index;
514+
515+ if (app.appId === priv.mainStageAppId) {
516+ // Active main stage always at 0
466517 return 0;
467518 }
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− }
473519
474− // Precondition now: There's an active MS app and this is SS app:
475− if (spreadView.nextInStack >= 0 && ApplicationManager.get(spreadView.nextInStack).stage == ApplicationInfoInterface.MainStage) {
476− // If the next app coming from the right is a MS app, we need to elevate this SS ap above it.
477− // Put it to at least level 2, or higher if there's more apps coming in before this one.
478− return Math.max(index, 2);
479− } else {
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.
520+ if (spreadView.nextInStack > 0) {
521+ var nextAppInStack = ApplicationManager.get(spreadView.nextInStack);
522+
523+ if (index === spreadView.nextInStack) {
524+ // this is the next app in stack.
525+
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+ }
481530 return 1;
482531 }
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) {
491− // This is a SS app and the next one to come in from the right:
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;
532+ if (nextAppInStack.stage === ApplicationInfoInterface.SideStage) {
533+ // if the next app in stack is a sidestage app, it must order on top of other side stage app
534+ return 1;
495535 }
496− // Or if there's only one other active app, put it on top of that.
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;
536+ return Math.min(2, ApplicationManager.count-1);
504537 }
505− // don't touch all others... (mostly index > 3 + simple cases where the above doesn't shuffle much)
506− return index;
538+ return Math.min(index+1, ApplicationManager.count-1);
507539 }
508540
509541 SequentialAnimation {
@@ -521,8 +553,12 @@ AbstractStage {
521553 script: {
522554 if (spreadView.selectedIndex >= 0) {
523555 var newIndex = spreadView.selectedIndex;
556+ var application = ApplicationManager.get(newIndex);
557+ if (application.stage === ApplicationInfoInterface.SideStage) {
558+ sideStage.showNow();
559+ }
524560 spreadView.selectedIndex = -1;
525− ApplicationManager.focusApplication(ApplicationManager.get(newIndex).appId);
561+ ApplicationManager.focusApplication(application.appId);
526562 spreadView.phase = 0;
527563 spreadView.contentX = -spreadView.shift;
528564 }
@@ -545,87 +581,82 @@ AbstractStage {
545581 spreadView.snapTo(0);
546582 }
547583
548− Rectangle {
549− id: sideStageBackground
550− color: "black"
551− width: spreadView.sideStageWidth * (1 - sideStageDragHandle.progress)
552− height: priv.landscapeHeight
553− x: spreadView.width - width
554− z: spreadView.indexToZIndex(priv.indexOf(priv.sideStageAppId))
555− opacity: spreadView.phase == 0 ? 1 : 0
556− Behavior on opacity { UbuntuNumberAnimation {} }
584+ DropArea {
585+ objectName: "MainStageDropArea"
586+ anchors {
587+ left: parent.left
588+ top: parent.top
589+ bottom: parent.bottom
590+ }
591+ width: spreadView.width - sideStage.width
592+ enabled: priv.sideStageEnabled
593+
594+ onDropped: {
595+ priv.setAppStage(drag.source.appId, ApplicationInfoInterface.MainStage, true);
596+ ApplicationManager.focusApplication(drag.source.appId);
597+ }
598+ keys: "SideStage"
557599 }
558600
559− Item {
560− id: sideStageDragHandle
561− anchors.right: sideStageBackground.left
562− anchors.top: sideStageBackground.top
563− width: units.gu(2)
601+ SideStage {
602+ id: sideStage
603+ objectName: "sideStage"
564604 height: priv.landscapeHeight
565− z: sideStageBackground.z
566− opacity: spreadView.phase <= 0 && spreadView.sideStageVisible ? 1 : 0
567− property real progress: 0
568− property bool dragging: false
605+ x: spreadView.width - width
606+ z: {
607+ if (!priv.mainStageAppId) return 0;
569608
570− Behavior on opacity { UbuntuNumberAnimation {} }
609+ if (priv.sideStageAppId && spreadView.nextInStack > 0) {
610+ var nextAppInStack = ApplicationManager.get(spreadView.nextInStack);
571611
572− Connections {
573− target: spreadView
574− onSideStageVisibleChanged: {
575− if (spreadView.sideStageVisible) {
576− sideStageDragHandle.progress = 0;
612+ if (nextAppInStack.stage === ApplicationInfoInterface.MainStage) {
613+ // if the next app in stack is a main stage app, put the sidestage on top of it.
614+ return 2;
577615 }
616+ return 1;
578617 }
618+
619+ return 1;
579620 }
621+ visible: progress != 0
622+ enabled: priv.sideStageEnabled && sideStageDropArea.dropAllowed
623+ opacity: priv.sideStageEnabled && !spreadView.active ? 1 : 0
624+ Behavior on opacity { UbuntuNumberAnimation {} }
580625
581− Image {
582− anchors.centerIn: parent
583− width: sideStageDragHandleMouseArea.pressed ? parent.width * 2 : parent.width
584− height: parent.height
585− source: "graphics/sidestage_handle@20.png"
586− Behavior on width { UbuntuNumberAnimation {} }
626+ onShownChanged: {
627+ if (!shown && ApplicationManager.focusedApplicationId == priv.sideStageAppId) {
628+ ApplicationManager.requestFocusApplication(priv.mainStageAppId);
629+ }
630+ priv.updateStageApps();
631+ if (shown && priv.sideStageAppId) {
632+ ApplicationManager.requestFocusApplication(priv.sideStageAppId);
633+ }
587634 }
588635
589− MouseArea {
590− id: sideStageDragHandleMouseArea
636+ DropArea {
637+ id: sideStageDropArea
638+ objectName: "SideStageDropArea"
591639 anchors.fill: parent
592− enabled: spreadView.shiftedContentX == 0
593− property int startX
594− property var gesturePoints: new Array()
595− property real totalDiff
596−
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);
640+
641+ property bool dropAllowed: true
642+
643+ onEntered: {
644+ dropAllowed = drag.keys != "Disabled";
610645 }
611− onReleased: {
612− if (priv.mainStageAppId) {
613− var oneWayFlick = priv.evaluateOneWayFlick(gesturePoints);
614− sideStageDragSnapAnimation.to = sideStageDragHandle.progress > 0.5 || oneWayFlick ? 1 : 0;
615− sideStageDragSnapAnimation.start();
616− } else {
617− sideStageDragHandle.dragging = false;
646+ onExited: {
647+ dropAllowed = true;
648+ }
649+ onDropped: {
650+ if (drop.keys == "MainStage") {
651+ priv.setAppStage(drop.source.appId, ApplicationInfoInterface.SideStage, true);
652+ ApplicationManager.requestFocusApplication(drop.source.appId);
618653 }
619654 }
620− }
621− UbuntuNumberAnimation {
622− id: sideStageDragSnapAnimation
623− target: sideStageDragHandle
624− property: "progress"
625−
626− onRunningChanged: {
627− if (!running) {
628− sideStageDragHandle.dragging = false;
655+ drag {
656+ onSourceChanged: {
657+ if (!sideStageDropArea.drag.source) {
658+ dropAllowed = true;
659+ }
629660 }
630661 }
631662 }
@@ -640,29 +671,17 @@ AbstractStage {
640671 id: spreadTile
641672 objectName: model.appId ? "tabletSpreadDelegate_" + model.appId
642673 : "tabletSpreadDelegate_null";
643− width: {
644− if (wantsMainStage) {
645− return spreadView.width;
646− } else {
647− return spreadView.sideStageWidth;
648− }
649− }
650− height: {
651− if (wantsMainStage) {
652− return spreadView.height;
653− } else {
654− return priv.landscapeHeight;
655− }
656− }
657− active: model.appId == priv.mainStageAppId || model.appId == priv.sideStageAppId
658− zIndex: spreadView.indexToZIndex(index)
674+ width: spreadView.width
675+ height: spreadView.height
676+ active: appId == priv.mainStageAppId || appId == priv.sideStageAppId
677+ zIndex: selected && stage == ApplicationInfoInterface.MainStage ? 0 : spreadView.indexToZIndex(index)
659678 selected: spreadView.selectedIndex == index
660679 otherSelected: spreadView.selectedIndex >= 0 && !selected
661− isInSideStage: priv.sideStageAppId == model.appId
680+ isInSideStage: priv.sideStageAppId === appId
662681 interactive: !spreadView.interactive && spreadView.phase === 0 && root.interactive
663682 swipeToCloseEnabled: spreadView.interactive && !snapAnimation.running
664683 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
666685 application: ApplicationManager.get(index)
667686 closeable: !isDash
668687 highlightShown: root.altTabPressed && priv.highlightIndex == zIndex
@@ -671,6 +690,35 @@ AbstractStage {
671690
672691 readonly property bool isDash: model.appId == "unity8-dash"
673692
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+
674722 Binding {
675723 target: spreadTile.application
676724 property: "exemptFromLifecycle"
@@ -701,6 +749,28 @@ AbstractStage {
701749 enabled: spreadView.closingIndex >= 0
702750 UbuntuNumberAnimation {}
703751 }
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+ }
704774
705775 // This is required because none of the bindings are triggered in some cases:
706776 // When an app is closed, it might happen that ApplicationManager.get(nextInStack)
@@ -725,7 +795,7 @@ AbstractStage {
725795
726796 // TODO: Hiding tile when progress is such that it will be off screen.
727797 property bool occluded: {
728− if (spreadView.active) return false;
798+ if (spreadView.active && !offScreen) return false;
729799 else if (spreadTile.active) return false;
730800 else if (xTranslateAnimating) return false;
731801 else if (z <= 1 && priv.focusedAppDelegateIsDislocated) return false;
@@ -749,17 +819,87 @@ AbstractStage {
749819 return progress;
750820 }
751821
752− shellOrientationAngle: wantsMainStage ? root.shellOrientationAngle : 0
753− shellOrientation: wantsMainStage ? root.shellOrientation : Qt.PortraitOrientation
754− orientations: Orientations {
755− primary: spreadTile.wantsMainStage ? root.orientations.primary : Qt.PortraitOrientation
756− native_: spreadTile.wantsMainStage ? root.orientations.native_ : Qt.PortraitOrientation
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
842+ shellOrientation: Qt.PortraitOrientation
843+ orientations: sideStageOrientations
844+ }
845+ }
846+ ]
847+
848+ Orientations {
849+ id: sideStageOrientations
850+ primary: Qt.PortraitOrientation
851+ native_: Qt.PortraitOrientation
757852 portrait: root.orientations.portrait
758853 invertedPortrait: root.orientations.invertedPortrait
759854 landscape: root.orientations.landscape
760855 invertedLandscape: root.orientations.invertedLandscape
761856 }
762857
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+
763903 onClicked: {
764904 if (spreadView.phase == 2) {
765905 spreadView.snapTo(index);
@@ -779,6 +919,16 @@ AbstractStage {
779919 ApplicationManager.stopApplication(ApplicationManager.get(index).appId);
780920 }
781921
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+
782932 Binding {
783933 target: root
784934 when: model.appId == priv.mainStageAppId
@@ -803,6 +953,71 @@ AbstractStage {
803953 }
804954 }
805955
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+
8061021 //eat touch events during the right edge gesture
8071022 MouseArea {
8081023 anchors.fill: parent
@@ -812,7 +1027,8 @@ AbstractStage {
8121027 DirectionalDragArea {
8131028 id: spreadDragArea
8141029 objectName: "spreadDragArea"
815− anchors { top: parent.top; right: parent.right; bottom: parent.bottom }
1030+ x: parent.width - root.dragAreaWidth
1031+ anchors { top: parent.top; bottom: parent.bottom }
8161032 width: root.dragAreaWidth
8171033 direction: Direction.Leftwards
8181034 enabled: (spreadView.phase != 2 && root.spreadEnabled) || dragging
qml/Stages/TransformedTabletSpreadDelegate.qml+84−56
@@ -31,6 +31,7 @@ SpreadDelegate {
3131 // Set this to true when this tile a currently active on either the MS or the SS.
3232 property bool active: false
3333
34+ property int stage
3435 property int zIndex
3536 property real progress: 0
3637 property real animatedProgress: 0
@@ -49,10 +50,11 @@ SpreadDelegate {
4950
5051 property int dragOffset: 0
5152 readonly property alias xTranslateAnimating: xTranslateAnimation.running
53+ readonly property bool offScreen: priv.xTranslate >= 0
5254
5355 dropShadow: spreadView.active ||
5456 (active
55− && (model.stage == ApplicationInfoInterface.MainStage || !priv.shellIsLandscape)
57+ && (stage == ApplicationInfoInterface.MainStage || !priv.shellIsLandscape)
5658 && priv.xTranslate != 0)
5759
5860 onSelectedChanged: {
@@ -141,9 +143,9 @@ SpreadDelegate {
141143 enabled: !spreadView.active &&
142144 !snapAnimation.running &&
143145 model.appId !== "unity8-dash" &&
144− !spreadView.sideStageDragging &&
145146 spreadView.animateX &&
146− !spreadView.beingResized
147+ !spreadView.beingResized &&
148+ priv.state !== "sideStage"
147149 UbuntuNumberAnimation {
148150 id: xTranslateAnimation
149151 duration: UbuntuAnimation.FastDuration
@@ -153,21 +155,20 @@ SpreadDelegate {
153155 property real xTranslate: {
154156 var newTranslate = 0;
155157
156− if (otherSelected) {
157− return priv.selectedXTranslate;
158− }
159−
160− if (isSelected) {
161− if (model.stage == ApplicationInfoInterface.MainStage) {
158+ // selected app or opposite stage active app.
159+ if (isSelected || (otherSelected && root.active && spreadView.selectedApplication && spreadView.selectedApplication.stage !== stage)) {
160+ if (stage == ApplicationInfoInterface.MainStage) {
162161 return linearAnimation(selectedProgress, negativeProgress, selectedXTranslate, -spreadView.width, root.progress);
163162 } else {
164163 return linearAnimation(selectedProgress, negativeProgress, selectedXTranslate, -spreadView.sideStageWidth, root.progress);
165164 }
165+ } else if (otherSelected) {
166+ return selectedXTranslate;
166167 }
167168
168169 // 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
169170 // 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 &&
171172 ApplicationManager.get(spreadView.nextInStack).stage === ApplicationInfoInterface.MainStage;
172173
173174 if (active) {
@@ -177,36 +178,45 @@ SpreadDelegate {
177178 newTranslate += linearAnimation(0, spreadView.positionMarker2, 0, -units.gu(4), root.animatedProgress);
178179 }
179180 newTranslate += root.dragOffset;
180− }
181− if (!spreadView.active && model.appId == "unity8-dash" && !root.active) {
181+ } else if (!spreadView.active && model.appId == "unity8-dash") {
182182 newTranslate -= root.width;
183183 }
184184
185− if (nextInStack && spreadView.phase == 0) {
186− if (model.stage == ApplicationInfoInterface.MainStage) {
187− if (spreadView.sideStageVisible && root.progress > 0) {
188− // Move it so it appears from behind the side stage immediately
189− newTranslate += -spreadView.sideStageWidth;
185+ if (!spreadView.active) {
186+ return newTranslate;
187+ }
188+
189+ var shadowOffset = units.gu(2);
190+
191+ if (spreadView.phase == 0) {
192+ if (nextInStack) {
193+ if (stage == ApplicationInfoInterface.MainStage) {
194+ if (spreadView.sideStageVisible && root.progress > 0) {
195+ // Move it so it appears from behind the side stage immediately
196+ newTranslate += -spreadView.sideStageWidth;
197+ }
190198 }
191− }
192199
193− if (model.stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
194− // 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);
196− } else {
197− newTranslate += linearAnimation(0, spreadView.positionMarker2, 0, -spreadView.sideStageWidth * spreadView.snapPosition, root.animatedProgress);
200+ if (stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
201+ // This is when we only drag the side stage in, without rotation or snapping
202+ newTranslate = linearAnimation(0, spreadView.positionMarker2, 0, -spreadView.sideStageWidth, root.animatedProgress);
203+ } else {
204+ newTranslate += linearAnimation(0, spreadView.positionMarker2, 0, -spreadView.sideStageWidth * spreadView.snapPosition, root.animatedProgress);
205+ }
206+ } else if (!root.active) {
207+ newTranslate += shadowOffset;
198208 }
199209 }
200210
201211 if (spreadView.phase == 1) {
202212 if (nextInStack) {
203− if (model.stage == ApplicationInfoInterface.MainStage) {
213+ if (stage == ApplicationInfoInterface.MainStage) {
204214 var startValue = -spreadView.sideStageWidth * spreadView.snapPosition + (spreadView.sideStageVisible ? -spreadView.sideStageWidth : 0);
205215 newTranslate += linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, startValue, priv.phase2StartTranslate, root.animatedProgress);
206216 } else {
207217 var endValue = -spreadView.width + spreadView.width * root.zIndex / 6;
208218 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);
210220 } else {
211221 newTranslate += linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, -spreadView.sideStageWidth * spreadView.snapPosition, priv.phase2StartTranslate, root.animatedProgress);
212222 }
@@ -215,16 +225,20 @@ SpreadDelegate {
215225 var startProgress = spreadView.positionMarker2 - (zIndex * spreadView.positionMarker2 / 2);
216226 var endProgress = spreadView.positionMarker4 - (zIndex * spreadView.tileDistance / spreadView.width);
217227 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);
219229 } else {
220230 var startProgress = spreadView.positionMarker2 - (zIndex * spreadView.positionMarker2 / 2);
221231 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);
223233 }
224234 }
225235
226236 if (spreadView.phase == 2) {
227− newTranslate = -easingCurve.value * (spreadView.width - root.zIndex * animatedEndDistance);
237+ if (easingCurve.value == 0 && !nextInStack) {
238+ newTranslate = shadowOffset;
239+ } else {
240+ newTranslate = -easingCurve.value * (spreadView.width - root.zIndex * animatedEndDistance);
241+ }
228242 }
229243
230244 return newTranslate;
@@ -235,33 +249,30 @@ SpreadDelegate {
235249 return 1;
236250 }
237251
238− if (otherSelected) {
239− return selectedScale;
240− }
241−
242− if (isSelected) {
252+ // selected app or opposite stage active app.
253+ if (isSelected || (otherSelected && root.active && spreadView.selectedApplication && spreadView.selectedApplication.stage !== stage)) {
243254 return linearAnimation(selectedProgress, negativeProgress, selectedScale, 1, root.progress);
255+ } else if (otherSelected) {
256+ return selectedScale;
244257 }
245258
246259 if (spreadView.phase == 0) {
247260 if (nextInStack) {
248− if (model.stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
261+ if (stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
249262 return 1;
250263 } else {
251264 var targetScale = root.dragStartScale - ((root.dragStartScale - 1) * spreadView.snapPosition);
252265 return linearAnimation(0, spreadView.positionMarker2, root.dragStartScale, targetScale, root.animatedProgress);
253266 }
254− } else if (active) {
255− return 1;
256267 } else {
257− return linearAnimation(0, spreadView.positionMarker2, root.startScale, root.endScale, root.progress);
268+ return 1;
258269 }
259270 }
260271
261272 if (spreadView.phase == 1) {
262273 if (nextInStack) {
263274 var startScale = 1;
264− if (model.stage !== ApplicationInfoInterface.SideStage || spreadView.sideStageVisible) {
275+ if (stage !== ApplicationInfoInterface.SideStage || spreadView.sideStageVisible) {
265276 startScale = root.dragStartScale - ((root.dragStartScale - 1) * spreadView.snapPosition);
266277 }
267278 return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, startScale, priv.phase2StartScale, root.animatedProgress);
@@ -283,21 +294,22 @@ SpreadDelegate {
283294 return 0;
284295 }
285296
286− if (otherSelected) {
287− return selectedAngle;
288− }
289− if (isSelected) {
297+ // selected app or opposite stage active app.
298+ if (isSelected || (otherSelected && root.active && spreadView.selectedApplication && spreadView.selectedApplication.stage !== stage)) {
290299 return linearAnimation(selectedProgress, negativeProgress, selectedAngle, 0, root.progress);
300+ } else if (otherSelected) {
301+ return selectedAngle;
291302 }
292303
293304 // 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 &&
295307 (ApplicationManager.get(spreadView.nextInStack).stage === ApplicationInfoInterface.MainStage ||
296− model.stage == ApplicationInfoInterface.SideStage);
308+ stage == ApplicationInfoInterface.SideStage);
297309
298310 if (spreadView.phase == 0) {
299311 if (nextInStack) {
300− if (model.stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
312+ if (stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
301313 return 0;
302314 } else {
303315 return linearAnimation(0, spreadView.positionMarker2, root.startAngle, root.startAngle * (1-spreadView.snapPosition), root.animatedProgress);
@@ -309,7 +321,7 @@ SpreadDelegate {
309321 }
310322 if (spreadView.phase == 1) {
311323 if (nextInStack) {
312− if (model.stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
324+ if (stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
313325 return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, 0, priv.phase2StartAngle, root.animatedProgress);
314326 } else {
315327 return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, root.startAngle * (1-spreadView.snapPosition), priv.phase2StartAngle, root.animatedProgress);
@@ -328,31 +340,47 @@ SpreadDelegate {
328340 }
329341
330342 property real opacityTransform: {
331− if (otherSelected && spreadView.phase == 2) {
343+ // animate opacity for items not snapping into view.
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+ }
332351 return linearAnimation(selectedProgress, negativeProgress, selectedOpacity, 0, root.progress);
333352 }
334−
335353 return 1;
336354 }
337355
338356 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)) {
340359 return linearAnimation(selectedProgress, negativeProgress, selectedTopMarginProgress, 0, root.progress);
341360 }
342− switch (spreadView.phase) {
343− case 0:
361+
362+ if (spreadView.phase == 0) {
344363 return 0;
345− case 1:
346− return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4,
347− 0, priv.phase2StartTopMarginProgress, root.progress);
364+ } else if (spreadView.phase == 1) {
365+ if (nextInStack) {
366+ return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4,
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);
348372 }
349373 return 1;
350374 }
351375
352376 states: [
353377 State {
354− name: "sideStageDragging"; when: spreadView.sideStageDragging && root.isInSideStage
355− PropertyChanges { target: priv; xTranslate: -spreadView.sideStageWidth + spreadView.sideStageWidth * spreadView.sideStageDragProgress }
378+ name: "sideStage";
379+ when: root.isInSideStage && spreadView.shiftedContentX == 0 && spreadView.phase == 0
380+ PropertyChanges {
381+ target: priv;
382+ xTranslate: -spreadView.sideStageWidth + spreadView.sideStageWidth * (1-spreadView.sideStageDragProgress)
383+ }
356384 }
357385 ]
358386 }
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;
7171namespace {
7272MouseTouchAdaptor *g_instance = nullptr;
7373
74+const Qt::KeyboardModifiers TRI_PRESS_MODIFIER = Qt::ShiftModifier|Qt::ControlModifier|Qt::AltModifier;
75+
7476Qt::MouseButton translateMouseButton(xcb_button_t detail)
7577{
7678 switch (detail) {
@@ -81,10 +83,25 @@ Qt::MouseButton translateMouseButton(xcb_button_t detail)
8183 default: return Qt::NoButton;
8284 }
8385}
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+}
8498} // end of anonymous namespace
8599
86100MouseTouchAdaptor::MouseTouchAdaptor()
87− : QObject(nullptr), m_leftButtonIsPressed(false), m_enabled(true)
101+ : QObject(nullptr)
102+ , m_leftButtonIsPressed(false)
103+ , m_triPressModifier(false)
104+ , m_enabled(true)
88105{
89106 QCoreApplication::instance()->installNativeEventFilter(this);
90107
@@ -192,17 +209,20 @@ bool MouseTouchAdaptor::xi2HandleEvent(xcb_ge_event_t *event)
192209 return handleButtonPress(
193210 static_cast<WId>(xiDeviceEvent->event),
194211 xiDeviceEvent->detail,
212+ xiDeviceEvent->mods.base_mods,
195213 fixed1616ToReal(xiDeviceEvent->event_x),
196214 fixed1616ToReal(xiDeviceEvent->event_y));
197215 case XI_ButtonRelease:
198216 return handleButtonRelease(
199217 static_cast<WId>(xiDeviceEvent->event),
200218 xiDeviceEvent->detail,
219+ xiDeviceEvent->mods.base_mods,
201220 fixed1616ToReal(xiDeviceEvent->event_x),
202221 fixed1616ToReal(xiDeviceEvent->event_y));
203222 case XI_Motion:
204223 return handleMotionNotify(
205224 static_cast<WId>(xiDeviceEvent->event),
225+ xiDeviceEvent->mods.base_mods,
206226 fixed1616ToReal(xiDeviceEvent->event_x),
207227 fixed1616ToReal(xiDeviceEvent->event_y));
208228 return true;
@@ -232,17 +252,18 @@ bool MouseTouchAdaptor::nativeEventFilter(const QByteArray & eventType,
232252 switch (xcbEvent->response_type & ~0x80) {
233253 case XCB_BUTTON_PRESS: {
234254 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,
236256 pressEvent->event_x, pressEvent->event_y);
237257 }
238258 case XCB_BUTTON_RELEASE: {
239259 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,
241261 releaseEvent->event_x, releaseEvent->event_y);
242262 }
243263 case XCB_MOTION_NOTIFY: {
244264 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);
246267 }
247268 case XCB_GE_GENERIC:
248269 if (m_xi2Enabled) {
@@ -255,9 +276,10 @@ bool MouseTouchAdaptor::nativeEventFilter(const QByteArray & eventType,
255276 };
256277}
257278
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)
259280{
260281 Qt::MouseButton button = translateMouseButton(detail);
282+ Qt::KeyboardModifiers qtMod = translateMofidier(modifiers);
261283
262284 // Just eat the event if it wasn't a left mouse press
263285 if (button != Qt::LeftButton)
@@ -270,19 +292,24 @@ bool MouseTouchAdaptor::handleButtonPress(WId windowId, uint32_t detail, int x,
270292 QTouchEventSequence touchEvent = QTest::touchEvent(targetWindow, m_touchDevice,
271293 false /* autoCommit */);
272294 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+ }
273300 touchEvent.commit(false /* processEvents */);
274301
275302 m_leftButtonIsPressed = true;
276303 return true;
277304}
278305
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)
280307{
281308 Qt::MouseButton button = translateMouseButton(detail);
282309
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
284311 if (button != Qt::LeftButton)
285− return true;
312+ return false;
286313
287314 QWindow *targetWindow = findQWindowWithXWindowID(windowId);
288315
@@ -291,17 +318,23 @@ bool MouseTouchAdaptor::handleButtonRelease(WId windowId, uint32_t detail, int x
291318 QTouchEventSequence touchEvent = QTest::touchEvent(targetWindow, m_touchDevice,
292319 false /* autoCommit */);
293320 touchEvent.release(0 /* touchId */, windowPos);
321+ if (m_triPressModifier) {
322+ touchEvent.release(1, windowPos);
323+ touchEvent.release(2, windowPos);
324+ }
294325 touchEvent.commit(false /* processEvents */);
295326
296327 m_leftButtonIsPressed = false;
328+ m_triPressModifier = false;
297329 return true;
298330}
299331
300−bool MouseTouchAdaptor::handleMotionNotify(WId windowId, int x, int y)
332+bool MouseTouchAdaptor::handleMotionNotify(WId windowId, uint32_t modifiers, int x, int y)
301333{
302334 if (!m_leftButtonIsPressed) {
303335 return true;
304336 }
337+ Qt::KeyboardModifiers qtMod = translateMofidier(modifiers);
305338
306339 QWindow *targetWindow = findQWindowWithXWindowID(windowId);
307340
@@ -310,6 +343,17 @@ bool MouseTouchAdaptor::handleMotionNotify(WId windowId, int x, int y)
310343 QTouchEventSequence touchEvent = QTest::touchEvent(targetWindow, m_touchDevice,
311344 false /* autoCommit */);
312345 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+ }
313357 touchEvent.commit(false /* processEvents */);
314358
315359 return true;
src/MouseTouchAdaptor.h+5−3
@@ -51,13 +51,15 @@ private:
5151 void fetchXInput2Info();
5252 bool xi2HandleEvent(xcb_ge_event_t *event);
5353
54− bool handleButtonPress(WId windowId, uint32_t detail, int x, int y);
55− bool handleButtonRelease(WId windowId, uint32_t detail, int x, int y);
56− bool handleMotionNotify(WId windowId, 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, uint32_t modifiers, int x, int y);
56+ bool handleMotionNotify(WId windowId, uint32_t modifiers, int x, int y);
5757 QWindow *findQWindowWithXWindowID(WId windowId);
5858
5959 QTouchDevice *m_touchDevice;
6060 bool m_leftButtonIsPressed;
61+ bool m_triPressModifier;
62+
6163
6264 bool m_enabled;
6365
tests/libs/UbuntuGestures/tst_TouchRegistry.cpp+59
@@ -42,6 +42,10 @@ public:
4242 QSet<int> ownedTouches;
4343 QSet<int> lostTouches;
4444 QList<TouchMemento> unownedTouchEvents;
45+
46+Q_SIGNALS:
47+ void gainedOwnership();
48+ void lostOwnership();
4549};
4650
4751class tst_TouchRegistry : public QObject
@@ -68,6 +72,7 @@ private Q_SLOTS:
6872 void removeOldUndecidedCandidates();
6973 void interimOwnerWontGetUnownedTouchEvents();
7074 void candidateVanishes();
75+ void candicateOwnershipReentrace();
7176
7277private:
7378 TouchRegistry *touchRegistry;
@@ -864,6 +869,58 @@ void tst_TouchRegistry::candidateVanishes()
864869 QVERIFY(mainCandidate.ownedTouches.contains(0));
865870}
866871
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+
867924////////////// TouchMemento //////////
868925
869926TouchMemento::TouchMemento(const QTouchEvent *touchEvent)
@@ -897,8 +954,10 @@ bool DummyCandidate::event(QEvent *e)
897954
898955 if (touchOwnershipEvent->gained()) {
899956 ownedTouches.insert(touchOwnershipEvent->touchId());
957+ Q_EMIT gainedOwnership();
900958 } else {
901959 lostTouches.insert(touchOwnershipEvent->touchId());
960+ Q_EMIT lostOwnership();
902961 }
903962 return true;
904963 } else if (e->type() == UnownedTouchEvent::unownedTouchEventType()) {
tests/mocks/Unity/Application/ApplicationManager.cpp+10−25
@@ -151,10 +151,6 @@ void ApplicationManager::add(ApplicationInfo *application) {
151151
152152 beginInsertRows(QModelIndex(), m_runningApplications.size(), m_runningApplications.size());
153153 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
158154
159155 connect(application, &ApplicationInfo::sessionChanged, this, [application, this]() {
160156 QModelIndex appIndex = findIndex(application);
@@ -171,6 +167,16 @@ void ApplicationManager::add(ApplicationInfo *application) {
171167 if (!appIndex.isValid()) return;
172168 Q_EMIT dataChanged(appIndex, appIndex, QVector<int>() << ApplicationManager::RoleState);
173169 });
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
174180}
175181
176182void ApplicationManager::remove(ApplicationInfo *application) {
@@ -202,23 +208,11 @@ void ApplicationManager::move(int from, int to) {
202208
203209ApplicationInfo* ApplicationManager::startApplication(const QString &appId,
204210 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)
212211{
213212 Q_UNUSED(arguments)
214213 ApplicationInfo *application = add(appId);
215214 if (!application)
216215 return 0;
217−
218− if (flags.testFlag(ApplicationManager::ForceMainStage)
219− && application->stage() == ApplicationInfo::SideStage) {
220− application->setStage(ApplicationInfo::MainStage);
221− }
222216 application->setState(ApplicationInfo::Starting);
223217
224218 return application;
@@ -317,7 +311,6 @@ void ApplicationManager::buildListOfAvailableApplications()
317311 application->setAppId("unity8-dash");
318312 application->setName("Unity 8 Mock Dash");
319313 application->setScreenshotId("unity8-dash");
320− application->setStage(ApplicationInfo::MainStage);
321314 application->setSupportedOrientations(Qt::PrimaryOrientation);
322315 m_availableApplications.append(application);
323316
@@ -326,7 +319,6 @@ void ApplicationManager::buildListOfAvailableApplications()
326319 application->setName("Dialer");
327320 application->setScreenshotId("dialer");
328321 application->setIconId("dialer-app");
329− application->setStage(ApplicationInfo::SideStage);
330322 application->setSupportedOrientations(Qt::PortraitOrientation
331323 | Qt::InvertedPortraitOrientation);
332324 m_availableApplications.append(application);
@@ -350,7 +342,6 @@ void ApplicationManager::buildListOfAvailableApplications()
350342 application->setScreenshotId("gallery");
351343 application->setIconId("gallery");
352344 application->setFullscreen(true);
353− application->setStage(ApplicationInfo::MainStage);
354345 m_availableApplications.append(application);
355346
356347 application = new ApplicationInfo(this);
@@ -358,7 +349,6 @@ void ApplicationManager::buildListOfAvailableApplications()
358349 application->setName("Facebook");
359350 application->setScreenshotId("facebook");
360351 application->setIconId("facebook");
361− application->setStage(ApplicationInfo::SideStage);
362352 m_availableApplications.append(application);
363353
364354 application = new ApplicationInfo(this);
@@ -374,7 +364,6 @@ void ApplicationManager::buildListOfAvailableApplications()
374364 application->setName("Twitter");
375365 application->setScreenshotId("twitter");
376366 application->setIconId("twitter");
377− application->setStage(ApplicationInfo::SideStage);
378367 m_availableApplications.append(application);
379368
380369 application = new ApplicationInfo(this);
@@ -390,7 +379,6 @@ void ApplicationManager::buildListOfAvailableApplications()
390379 application->setIconId("gmail");
391380 application->setScreenshotId("gmail-webapp.svg");
392381 application->setFullscreen(false);
393− application->setStage(ApplicationInfo::MainStage);
394382 application->setSupportedOrientations(Qt::PortraitOrientation
395383 | Qt::LandscapeOrientation
396384 | Qt::InvertedPortraitOrientation
@@ -403,7 +391,6 @@ void ApplicationManager::buildListOfAvailableApplications()
403391 application->setIconId("soundcloud");
404392 application->setScreenshotId("music");
405393 application->setFullscreen(false);
406− application->setStage(ApplicationInfo::MainStage);
407394 application->setSupportedOrientations(Qt::PortraitOrientation
408395 | Qt::LandscapeOrientation
409396 | Qt::InvertedPortraitOrientation
@@ -423,14 +410,12 @@ void ApplicationManager::buildListOfAvailableApplications()
423410 application->setAppId("notes-app");
424411 application->setName("Notepad");
425412 application->setIconId("notepad");
426− application->setStage(ApplicationInfo::SideStage);
427413 m_availableApplications.append(application);
428414
429415 application = new ApplicationInfo(this);
430416 application->setAppId("calendar-app");
431417 application->setName("Calendar");
432418 application->setIconId("calendar");
433− application->setStage(ApplicationInfo::SideStage);
434419 m_availableApplications.append(application);
435420
436421 application = new ApplicationInfo(this);
tests/mocks/Unity/Application/ApplicationManager.h−8
@@ -46,11 +46,6 @@ class ApplicationManager : public ApplicationManagerInterface {
4646 RoleSession = RoleExemptFromLifecycle+1,
4747 RoleFullscreen,
4848 };
49− enum Flag {
50− NoFlag = 0x0,
51− ForceMainStage = 0x1,
52− };
53− Q_DECLARE_FLAGS(ExecFlags, Flag)
5449
5550 // QAbstractItemModel methods.
5651 int rowCount(const QModelIndex& parent = QModelIndex()) const override;
@@ -65,7 +60,6 @@ class ApplicationManager : public ApplicationManagerInterface {
6560 Q_INVOKABLE bool focusApplication(const QString &appId) override;
6661 Q_INVOKABLE void unfocusCurrentApplication() override;
6762 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());
6963 Q_INVOKABLE bool stopApplication(const QString &appId) override;
7064
7165 QString focusedApplicationId() const override;
@@ -98,8 +92,6 @@ class ApplicationManager : public ApplicationManagerInterface {
9892 static ApplicationManager *the_application_manager;
9993};
10094
101−Q_DECLARE_OPERATORS_FOR_FLAGS(ApplicationManager::ExecFlags)
102−
10395Q_DECLARE_METATYPE(ApplicationManager*)
10496
10597#endif // APPLICATION_MANAGER_H
tests/mocks/Unity/Application/MirSurfaceItem.cpp+17
@@ -156,6 +156,10 @@ void MirSurfaceItem::createQmlContentItem()
156156
157157void MirSurfaceItem::touchEvent(QTouchEvent * event)
158158{
159+ if (event->type() == QEvent::TouchBegin) {
160+ m_touchTrail.clear();
161+ }
162+
159163 if (event->touchPointStates() & Qt::TouchPointPressed) {
160164 ++m_touchPressCount;
161165 Q_EMIT touchPressCountChanged(m_touchPressCount);
@@ -163,6 +167,19 @@ void MirSurfaceItem::touchEvent(QTouchEvent * event)
163167 ++m_touchReleaseCount;
164168 Q_EMIT touchReleaseCountChanged(m_touchReleaseCount);
165169 }
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+ }
166183}
167184
168185void MirSurfaceItem::mousePressEvent(QMouseEvent * event)
tests/mocks/Unity/Application/MirSurfaceItem.h+1
@@ -123,6 +123,7 @@ private:
123123 int m_touchReleaseCount;
124124 int m_mousePressCount;
125125 int m_mouseReleaseCount;
126+ QVariantMap m_touchTrail;
126127
127128 FillMode m_fillMode{Stretch};
128129
tests/mocks/Unity/Application/resources/MirSurfaceItem.qml+59
@@ -29,6 +29,9 @@ Rectangle {
2929 property alias screenshotSource: screenshotImage.source
3030 property int orientationAngle
3131
32+ property var touchTrail
33+ onTouchTrailChanged: canvas.requestPaint()
34+
3235 Image {
3336 id: screenshotImage
3437 anchors.fill: parent
@@ -67,4 +70,60 @@ Rectangle {
6770 width: (rotation == 0 || rotation == 180 ? parent.width : parent.height)
6871 height:(rotation == 0 || rotation == 180 ? parent.height : parent.width)
6972 }
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+ }
70129}
tests/mocks/Utils/CMakeLists.txt+1
@@ -19,6 +19,7 @@ set(QMLPLUGIN_SRC
1919 ${CMAKE_SOURCE_DIR}/plugins/Utils/applicationsfiltermodel.cpp
2020 ${CMAKE_SOURCE_DIR}/plugins/Utils/inputeventgenerator.cpp
2121 ${CMAKE_SOURCE_DIR}/plugins/Utils/deviceconfigparser.cpp
22+ ${CMAKE_SOURCE_DIR}/plugins/Utils/globalfunctions.cpp
2223 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationManagerInterface.h
2324 constants.cpp
2425 plugin.cpp
tests/mocks/Utils/plugin.cpp+9
@@ -38,6 +38,7 @@
3838#include <applicationsfiltermodel.h>
3939#include <inputeventgenerator.h>
4040#include <deviceconfigparser.h>
41+#include <globalfunctions.h>
4142
4243static QObject *createWindowStateStorage(QQmlEngine *engine, QJSEngine *scriptEngine)
4344{
@@ -53,6 +54,13 @@ static QObject *createConstants(QQmlEngine *engine, QJSEngine *scriptEngine)
5354 return new Constants();
5455}
5556
57+static QObject *createGlobalFunctions(QQmlEngine *engine, QJSEngine *scriptEngine)
58+{
59+ Q_UNUSED(engine)
60+ Q_UNUSED(scriptEngine)
61+ return new GlobalFunctions();
62+}
63+
5664void FakeUtilsPlugin::registerTypes(const char *uri)
5765{
5866 Q_ASSERT(uri == QLatin1String("Utils"));
@@ -69,6 +77,7 @@ void FakeUtilsPlugin::registerTypes(const char *uri)
6977 qmlRegisterType<ApplicationsFilterModel>(uri, 0, 1, "ApplicationsFilterModel");
7078 qmlRegisterType<InputEventGenerator>(uri, 0, 1, "InputEventGenerator");
7179 qmlRegisterType<DeviceConfigParser>(uri, 0, 1, "DeviceConfigParser");
80+ qmlRegisterSingletonType<GlobalFunctions>(uri, 0, 1, "Functions", createGlobalFunctions);
7281}
7382
7483void FakeUtilsPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
tests/mocks/Utils/windowstatestorage.cpp+13
@@ -16,6 +16,8 @@
1616
1717#include "windowstatestorage.h"
1818
19+#include <unity/shell/application/ApplicationInfoInterface.h>
20+
1921WindowStateStorage::WindowStateStorage(QObject *parent):
2022 QObject(parent)
2123{
@@ -45,10 +47,21 @@ QRect WindowStateStorage::getGeometry(const QString &windowId, const QRect &defa
4547 return m_geometry.value(windowId).toRect();
4648}
4749
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+
4860void WindowStateStorage::clear()
4961{
5062 m_state.clear();
5163 m_geometry.clear();
64+ m_stage.clear();
5265}
5366
5467void WindowStateStorage::saveState(const QString &windowId, WindowState state)
tests/mocks/Utils/windowstatestorage.h+4
@@ -36,6 +36,9 @@ public:
3636 Q_INVOKABLE void saveGeometry(const QString &windowId, const QRect &rect);
3737 Q_INVOKABLE QRect getGeometry(const QString &windowId, const QRect &defaultValue);
3838
39+ Q_INVOKABLE void saveStage(const QString &appId, int stage);
40+ Q_INVOKABLE int getStage(const QString &appId) const;
41+
3942 // Only in the mock, to easily restore a fresh state
4043 Q_INVOKABLE void clear();
4144
@@ -47,5 +50,6 @@ private:
4750 QVariantMap geometry() const;
4851
4952 QHash<QString, WindowState> m_state;
53+ QHash<QString, int> m_stage;
5054 QVariantMap m_geometry;
5155};
tests/plugins/Ubuntu/Gestures/CMakeLists.txt+7−1
@@ -20,6 +20,11 @@ macro(build_gesture_test CLASSNAME)
2020 target_link_libraries(${CLASSNAME}TestExec UbuntuGesturesQml UbuntuGestures)
2121endmacro()
2222
23+set(UNITY_IMPORT_PATHS
24+ ${CMAKE_BINARY_DIR}/tests/utils/modules
25+ ${UNITY_PLUGINPATH}
26+)
27+
2328macro(add_gesture_unit_test CLASSNAME)
2429 build_gesture_test(${CLASSNAME})
2530 add_unity8_unittest(${CLASSNAME} ${CLASSNAME}TestExec
@@ -36,7 +41,7 @@ macro(add_gesture_ui_test CLASSNAME)
3641 DEPENDS UbuntuGesturesTest-qmlfiles
3742 ${ARGN}
3843 )
39− add_manual_qml_test(. ${CLASSNAME} IMPORT_PATHS ${UNITY_PLUGINPATH})
44+ add_manual_qml_test(. ${CLASSNAME} IMPORT_PATHS ${UNITY_IMPORT_PATHS})
4045endmacro()
4146
4247add_gesture_ui_test(DirectionalDragArea)
@@ -46,3 +51,4 @@ add_gesture_ui_test(TouchDispatcher)
4651add_gesture_ui_test(TouchGate)
4752add_gesture_unit_test(Damper)
4853add_gesture_unit_test(AxisVelocityCalculator)
54+add_gesture_ui_test(TouchGestureArea)
tests/plugins/Ubuntu/Gestures/GestureTest.cpp+44
@@ -17,6 +17,7 @@
1717#include "GestureTest.h"
1818
1919#include <qpa/qwindowsysteminterface.h>
20+#include <private/qquickwindow_p.h>
2021#include <QQmlEngine>
2122#include <QQuickView>
2223#include <QtTest>
@@ -72,6 +73,49 @@ void GestureTest::cleanup()
7273 m_view = nullptr;
7374}
7475
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+
75119////////////////////////// TouchMemento /////////////////////////////
76120
77121TouchMemento::TouchMemento(const QTouchEvent *touchEvent)
tests/plugins/Ubuntu/Gestures/GestureTest.h+13−1
@@ -72,7 +72,7 @@ class GestureTest : public QObject
7272{
7373 Q_OBJECT
7474public:
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
7676 GestureTest(const QString &qmlFilename);
7777
7878protected Q_SLOTS:
@@ -80,6 +80,18 @@ protected Q_SLOTS:
8080 virtual void init(); // called right before each and every test function is executed
8181 virtual void cleanup(); // called right after each and every test function is executed
8282
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+
8395protected:
8496 QTouchDevice *m_device;
8597 QQuickView *m_view;
tests/plugins/Ubuntu/Gestures/tst_DirectionalDragArea.cpp−56
@@ -20,7 +20,6 @@
2020#include <QtQml/QQmlEngine>
2121#include <QPointer>
2222#include <private/qquickmousearea_p.h>
23−#include <private/qquickwindow_p.h>
2423
2524
2625#include <DirectionalDragArea.h>
@@ -109,18 +108,6 @@ private Q_SLOTS:
109108 void makoRightEdgeDrag_verticalDownwards();
110109 void makoLeftEdgeDrag_slowStart();
111110 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);
124111};
125112
126113tst_DirectionalDragArea::tst_DirectionalDragArea()
@@ -141,49 +128,6 @@ void tst_DirectionalDragArea::init()
141128 QTRY_COMPARE(m_view->height(), (int)m_view->rootObject()->height());
142129}
143130
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−
187131namespace {
188132QPointF calculateInitialTouchPos(DirectionalDragArea *edgeDragArea)
189133{
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:
4848 QString comment() const override { return "this is a mock"; }
4949 QUrl icon() const override { return QUrl(); }
5050 ApplicationInfoInterface::Stage stage() const override { return ApplicationInfoInterface::MainStage; }
51+ void setStage(ApplicationInfoInterface::Stage) override {}
5152 ApplicationInfoInterface::State state() const override { return ApplicationInfoInterface::Running; }
5253 bool focused() const override { return m_focused; }
5354 QString splashTitle() const override { return QString(); }
tests/qmltests/Stages/tst_TabletStage.qml+143
@@ -20,6 +20,7 @@ import Ubuntu.Components 1.3
2020import Ubuntu.Components.ListItems 1.3 as ListItem
2121import Unity.Application 0.1
2222import Unity.Test 0.1
23+import Utils 0.1
2324
2425import ".."
2526import "../../../qml/Stages"
@@ -78,6 +79,7 @@ Rectangle {
7879 Column {
7980 anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
8081 spacing: units.gu(1)
82+
8183 EdgeBarrierControls {
8284 id: edgeBarrierControls
8385 text: "Drag here to pull out spread"
@@ -87,6 +89,16 @@ Rectangle {
8789 edgeBarrierControls.target = testCase.findChild(tabletStageLoader, "edgeBarrierController");
8890 }
8991 }
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+
90102 ApplicationCheckBox {
91103 id: webbrowserCheckBox
92104 appId: "webbrowser-app"
@@ -112,6 +124,7 @@ Rectangle {
112124 when: windowShown
113125
114126 property Item tabletStage: tabletStageLoader.status === Loader.Ready ? tabletStageLoader.item : null
127+ property Item sideStage: tabletStage ? findChild(tabletStage, "sideStage") : null
115128
116129 function init() {
117130 tabletStageLoader.active = true;
@@ -129,6 +142,7 @@ Rectangle {
129142 }
130143
131144 waitUntilAppSurfaceShowsUp("unity8-dash");
145+ sideStage.hideNow()
132146 }
133147
134148 function cleanup() {
@@ -148,6 +162,7 @@ Rectangle {
148162 galleryCheckBox.checked = false;
149163 dialerCheckBox.checked = false;
150164 facebookCheckBox.checked = false;
165+ WindowStateStorage.clear();
151166 }
152167
153168 function waitUntilAppSurfaceShowsUp(appId) {
@@ -187,6 +202,8 @@ Rectangle {
187202 }
188203
189204 function test_tappingSwitchesFocusBetweenStages() {
205+ WindowStateStorage.saveStage(dialerCheckBox.appId, ApplicationInfoInterface.SideStage)
206+
190207 webbrowserCheckBox.checked = true;
191208 waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId);
192209 var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
@@ -222,6 +239,8 @@ Rectangle {
222239 }
223240
224241 function test_closeAppInSideStage() {
242+ WindowStateStorage.saveStage(dialerCheckBox.appId, ApplicationInfoInterface.SideStage)
243+
225244 dialerCheckBox.checked = true;
226245 waitUntilAppSurfaceShowsUp(dialerCheckBox.appId);
227246
@@ -293,6 +312,8 @@ Rectangle {
293312
294313
295314 function test_foregroundMainAndSideStageAppsAreKeptRunning() {
315+ WindowStateStorage.saveStage(facebookCheckBox.appId, ApplicationInfoInterface.SideStage)
316+ WindowStateStorage.saveStage(dialerCheckBox.appId, ApplicationInfoInterface.SideStage)
296317
297318 var stagesPriv = findInvisibleChild(tabletStage, "stagesPriv");
298319 verify(stagesPriv);
@@ -351,6 +372,8 @@ Rectangle {
351372 }
352373
353374 function test_foregroundAppsAreSuspendedWhenStageIsSuspended() {
375+ WindowStateStorage.saveStage(dialerCheckBox.appId, ApplicationInfoInterface.SideStage)
376+
354377 webbrowserCheckBox.checked = true;
355378 waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId)
356379 var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
@@ -384,5 +407,125 @@ Rectangle {
384407 }
385408 tryCompare(spreadView, "phase", 2);
386409 }
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+ }
387530 }
388531}
tests/qmltests/tst_OrientedShell.qml+55−11
@@ -232,6 +232,9 @@ Rectangle {
232232 onClicked: {
233233 orientedShellLoader.item.physicalOrientation = root.physicalOrientation0;
234234 }
235+ color: orientedShellLoader.item && orientedShellLoader.item.physicalOrientation === root.physicalOrientation0 ?
236+ UbuntuColors.green :
237+ __styleInstance.defaultColor
235238 }
236239 Button {
237240 id: rotate90Button
@@ -240,6 +243,9 @@ Rectangle {
240243 onClicked: {
241244 orientedShellLoader.item.physicalOrientation = root.physicalOrientation90;
242245 }
246+ color: orientedShellLoader.item && orientedShellLoader.item.physicalOrientation === root.physicalOrientation90 ?
247+ UbuntuColors.green :
248+ __styleInstance.defaultColor
243249 }
244250 Button {
245251 id: rotate180Button
@@ -248,6 +254,9 @@ Rectangle {
248254 onClicked: {
249255 orientedShellLoader.item.physicalOrientation = root.physicalOrientation180;
250256 }
257+ color: orientedShellLoader.item && orientedShellLoader.item.physicalOrientation === root.physicalOrientation180 ?
258+ UbuntuColors.green :
259+ __styleInstance.defaultColor
251260 }
252261 Button {
253262 id: rotate270Button
@@ -256,6 +265,9 @@ Rectangle {
256265 onClicked: {
257266 orientedShellLoader.item.physicalOrientation = root.physicalOrientation270;
258267 }
268+ color: orientedShellLoader.item && orientedShellLoader.item.physicalOrientation === root.physicalOrientation270 ?
269+ UbuntuColors.green :
270+ __styleInstance.defaultColor
259271 }
260272 RowLayout {
261273 Layout.fillWidth: true
@@ -471,14 +483,46 @@ Rectangle {
471483 tearDown();
472484 }
473485
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() {
475520 return [
476− {tag: "mako", deviceName: "mako"},
477521 {tag: "manta", deviceName: "manta"},
478522 {tag: "flo", deviceName: "flo"}
479523 ];
480524 }
481− function test_appSupportingOnlyPrimaryOrientationMakesShellStayPut(data) {
525+ function test_appSupportingOnlyPrimaryOrientationWillOnlyRotateInLandscape(data) {
482526 loadShell(data.deviceName);
483527
484528 // unity8-dash supports only primary orientation and should be already running
@@ -502,13 +546,13 @@ Rectangle {
502546
503547 rotateTo(180);
504548
505− verify(checkAppSurfaceOrientation(dashAppWindow, dashApp, root.primaryOrientationAngle));
506− compare(shell.transformRotationAngle, root.primaryOrientationAngle);
549+ verify(checkAppSurfaceOrientation(dashAppWindow, dashApp, root.primaryOrientationAngle + 180));
550+ compare(shell.transformRotationAngle, root.primaryOrientationAngle + 180);
507551
508552 rotateTo(270);
509553
510− verify(checkAppSurfaceOrientation(dashAppWindow, dashApp, root.primaryOrientationAngle));
511− compare(shell.transformRotationAngle, root.primaryOrientationAngle);
554+ verify(checkAppSurfaceOrientation(dashAppWindow, dashApp, root.primaryOrientationAngle + 180));
555+ compare(shell.transformRotationAngle, root.primaryOrientationAngle + 180);
512556 }
513557
514558 function test_greeterRemainsInPrimaryOrientation_data() {
@@ -813,12 +857,12 @@ Rectangle {
813857
814858 var twitterApp = ApplicationManager.startApplication("twitter-webapp");
815859 verify(twitterApp);
860+ twitterApp.stage = ApplicationInfoInterface.SideStage;
816861
817862 // ensure the mock twitter-webapp is as we expect
818863 compare(twitterApp.rotatesWindowContents, false);
819864 compare(twitterApp.supportedOrientations, Qt.PortraitOrientation | Qt.LandscapeOrientation
820865 | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
821− compare(twitterApp.stage, ApplicationInfoInterface.SideStage);
822866
823867 // Wait until spreadRepeaterConnections hs caught the new SpreadDelegate and
824868 // set up the signalSpy target accordingly.
@@ -852,11 +896,11 @@ Rectangle {
852896
853897 var dialerApp = ApplicationManager.startApplication("dialer-app");
854898 verify(dialerApp);
899+ dialerApp.stage = ApplicationInfoInterface.SideStage;
855900
856901 // ensure the mock dialer-app is as we expect
857902 compare(dialerApp.rotatesWindowContents, false);
858903 compare(dialerApp.supportedOrientations, Qt.PortraitOrientation | Qt.InvertedPortraitOrientation);
859− compare(dialerApp.stage, ApplicationInfoInterface.SideStage);
860904
861905 tryCompareFunction(function(){ return dialerDelegate != null; }, true);
862906 tryCompare(dialerDelegate, "orientationChangesEnabled", true);
@@ -899,11 +943,11 @@ Rectangle {
899943
900944 var dialerApp = ApplicationManager.startApplication("dialer-app");
901945 verify(dialerApp);
946+ dialerApp.stage = ApplicationInfoInterface.SideStage;
902947
903948 // ensure the mock dialer-app is as we expect
904949 compare(dialerApp.rotatesWindowContents, false);
905950 compare(dialerApp.supportedOrientations, Qt.PortraitOrientation | Qt.InvertedPortraitOrientation);
906− compare(dialerApp.stage, ApplicationInfoInterface.SideStage);
907951
908952 tryCompareFunction(function(){ return dialerDelegate != null; }, true);
909953 waitUntilAppDelegateIsFullyInit(dialerDelegate);
@@ -919,12 +963,12 @@ Rectangle {
919963
920964 var twitterApp = ApplicationManager.startApplication("twitter-webapp");
921965 verify(twitterApp);
966+ twitterApp.stage = ApplicationInfoInterface.SideStage;
922967
923968 // ensure the mock twitter-webapp is as we expect
924969 compare(twitterApp.rotatesWindowContents, false);
925970 compare(twitterApp.supportedOrientations, Qt.PortraitOrientation | Qt.LandscapeOrientation
926971 | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
927− compare(twitterApp.stage, ApplicationInfoInterface.SideStage);
928972
929973 tryCompareFunction(function(){ return twitterDelegate != null; }, true);
930974 waitUntilAppDelegateIsFullyInit(twitterDelegate);
tests/utils/modules/Unity/Test/UnityTestCase.qml+67−11
@@ -313,7 +313,10 @@ TestCase {
313313 // perform a drag in the given direction until the given condition is true
314314 // The condition is a function to be evaluated after every step
315315 function touchDragUntil(item, startX, startY, stepX, stepY, condition) {
316+ multiTouchDragUntil([0], item, startX, startY, stepX, stepY, condition);
317+ }
316318
319+ function multiTouchDragUntil(touchIds, item, startX, startY, stepX, stepY, condition) {
317320 var root = fetchRootItem(item);
318321 var pos = item.mapToItem(root, startX, startY);
319322
@@ -326,7 +329,9 @@ TestCase {
326329 stepY = stepEnd.y - stepStart.y;
327330
328331 var event = touchEvent(item)
329− event.press(0 /* touchId */, pos.x, pos.y)
332+ for (var i = 0; i < touchIds.length; i++) {
333+ event.press(touchIds[i], pos.x, pos.y)
334+ }
330335 event.commit()
331336
332337 // we have to stop at some point
@@ -341,23 +346,30 @@ TestCase {
341346 pos.y += stepY;
342347
343348 event = touchEvent(item);
344− event.move(0 /* touchId */, pos.x, pos.y);
349+ for (i = 0; i < touchIds.length; i++) {
350+ event.move(touchIds[i], pos.x, pos.y);
351+ }
345352 event.commit();
346353
347354 stepsDone += 1;
348355 }
349356
350357 event = touchEvent(item)
351− event.release(0 /* touchId */, pos.x, pos.y)
358+ for (i = 0; i < touchIds.length; i++) {
359+ event.release(touchIds[i], pos.x, pos.y)
360+ }
352361 event.commit()
353362 }
354363
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;
356368 var root = fetchRootItem(item)
357369 var rootPoint = item.mapToItem(root, tox, toy)
358370
359371 var event = touchEvent(item);
360− event.move(0 /* touchId */, rootPoint.x, rootPoint.y);
372+ event.move(touchId, rootPoint.x, rootPoint.y);
361373 event.commit();
362374 }
363375
@@ -394,21 +406,55 @@ TestCase {
394406 return item
395407 }
396408
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 = []
398424 var root = fetchRootItem(item)
399425 var rootPoint = item.mapToItem(root, x, y)
400426
401427 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+ }
403432 event.commit()
404433 }
405434
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 = []
407450 var root = fetchRootItem(item)
408451 var rootPoint = item.mapToItem(root, x, y)
409452
410453 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+ }
412458 event.commit()
413459 }
414460
@@ -419,6 +465,11 @@ TestCase {
419465 \param y The y coordinate of the tap, defaults to vertical center
420466 */
421467 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];
422473 if (typeof x !== "number") x = item.width / 2;
423474 if (typeof y !== "number") y = item.height / 2;
424475
@@ -426,14 +477,19 @@ TestCase {
426477 var rootPoint = item.mapToItem(root, x, y)
427478
428479 var event = touchEvent(item)
429− event.press(0 /* touchId */, rootPoint.x, rootPoint.y)
480+ for (var i = 0; i < touchIds.length; i++) {
481+ event.press(touchIds[i], rootPoint.x, rootPoint.y)
482+ }
430483 event.commit()
431484
432485 event = touchEvent(item)
433− event.release(0 /* touchId */, rootPoint.x, rootPoint.y)
486+ for (i = 0; i < touchIds.length; i++) {
487+ event.release(touchIds[i], rootPoint.x, rootPoint.y)
488+ }
434489 event.commit()
435490 }
436491
492+
437493 Component.onCompleted: {
438494 var rootItem = parent;
439495 while (rootItem.parent != undefined) {