on xenial help sign in
Merge
commitf6c200d660f65f7b73df902901a43219ce925a2a authorAlbert Astals Cid <albert.astals@canonical.com> dateFri 13 Feb 2015, 11:44 (12 years ago) parent23f472f23c05f9
143 files changed+5568−1446
debian/changelog+30−21 debian/control+1−1 debian/unity8-private.install+1 debian/unity8.install+1 plugins/AccountsService/AccountsServiceDBusAdaptor.cpp+9 plugins/AccountsService/AccountsServiceDBusAdaptor.h+12 plugins/AccountsService/com.canonical.unity.AccountsService.xml+1−2 plugins/CMakeLists.txt+1 plugins/Greeter/CMakeLists.txt+6 plugins/Greeter/Unity/CMakeLists.txt+1 plugins/Greeter/Unity/Launcher/CMakeLists.txt+38 plugins/Greeter/Unity/Launcher/Launcher.qmltypes+179 plugins/Greeter/Unity/Launcher/launcheritem.cpp+171 plugins/Greeter/Unity/Launcher/launcheritem.h+74 plugins/Greeter/Unity/Launcher/launchermodelas.cpp+277 plugins/Greeter/Unity/Launcher/launchermodelas.h+75 plugins/Greeter/Unity/Launcher/plugin.cpp+46 plugins/Greeter/Unity/Launcher/plugin.h+32 plugins/Greeter/Unity/Launcher/qmldir+3 plugins/Greeter/Unity/Launcher/quicklistentry.cpp+56 plugins/Greeter/Unity/Launcher/quicklistentry.h+43 plugins/Greeter/Unity/Launcher/quicklistmodel.cpp+70 plugins/Greeter/Unity/Launcher/quicklistmodel.h+55 plugins/Ubuntu/Gestures/TouchGate.cpp+2 plugins/Ubuntu/Gestures/TouchGate.h+2 plugins/Unity/Indicators/indicator.cpp+23−8 plugins/Unity/Indicators/indicator.h+5−1 plugins/Unity/Indicators/indicatorsmanager.cpp+17−3 plugins/Unity/Indicators/indicatorsmanager.h+6−1 plugins/Unity/Indicators/indicatorsmodel.cpp+25−2 plugins/Unity/Indicators/indicatorsmodel.h+6−1 plugins/Unity/Indicators/modelactionrootstate.cpp+4−2 plugins/Unity/Indicators/unitymenumodelcache.cpp+8−11 plugins/Unity/Indicators/unitymenumodelcache.h+2−2 plugins/Unity/Launcher/CMakeLists.txt+2−1 plugins/Unity/Launcher/asadapter.cpp+60 tests/mocks/Unity/Notifications/MockNotificationTypes.h → plugins/Unity/Launcher/asadapter.h+24−14 plugins/Unity/Launcher/launchermodel.cpp+20 plugins/Unity/Launcher/launchermodel.h+5 plugins/Utils/CMakeLists.txt+2−1 plugins/Utils/plugin.cpp+9 plugins/Utils/windowstatestorage.cpp+96 plugins/Utils/windowstatestorage.h+38 po/gd.po+45−40 po/gl.po+7−7 po/nl.po+4−4 qml/CMakeLists.txt+3−2 qml/Components/EdgeDemo.qml−254 qml/Components/EdgeDemoOverlay.qml−273 qml/Dash/GenericScopeView.qml+1 qml/Dash/PageHeader.qml+2 qml/Greeter/GreeterContent.qml+1 qml/Launcher/Launcher.qml+13−7 qml/Launcher/LauncherPanel.qml+7−5 qml/Notifications/Notification.qml+36−6 qml/Notifications/Notifications.qml+2−6 qml/Panel/Panel.qml+5−3 qml/Shell.qml+49−24 qml/Stages/ApplicationWindow.qml+17−2 qml/Stages/DesktopStage.qml+2−2 qml/Stages/PhoneStage.qml+64−34 qml/Stages/SessionContainer.qml+22−7 qml/Stages/SpreadDelegate.qml+2−1 qml/Stages/SurfaceContainer.qml+11−25 qml/Stages/TabletStage.qml+14 qml/Stages/TransformedSpreadDelegate.qml+27−21 qml/Stages/TransformedTabletSpreadDelegate.qml+1−1 qml/Stages/WindowMoveResizeArea.qml+33−24 qml/Tutorial/Arrow.qml+56 qml/Tutorial/Slider.qml+123 qml/Tutorial/Tick.qml+29 qml/Tutorial/Tutorial.qml+92 qml/Tutorial/TutorialBottom.qml+104 qml/Tutorial/TutorialBottomFinish.qml+41 qml/Tutorial/TutorialContent.qml+124 qml/Tutorial/TutorialLeft.qml+91 qml/Tutorial/TutorialLeftFinish.qml+41 qml/Tutorial/TutorialPage.qml+245 qml/Tutorial/TutorialRight.qml+223 qml/Tutorial/graphics/camera.png qml/Tutorial/graphics/chevron.png qml/Tutorial/graphics/dialer.png qml/Tutorial/graphics/facebook.png qml/Tutorial/graphics/gallery.png qml/Tutorial/graphics/tick.png tests/autopilot/unity8/shell/emulators/edges_demo.py−140 tests/autopilot/unity8/shell/emulators/tutorial.py+74 tests/autopilot/unity8/shell/fixture_setup.py+6−6 tests/autopilot/unity8/shell/tests/test_edges_demo.py−51 tests/autopilot/unity8/shell/tests/test_tutorial.py+57 tests/mocks/Unity/Application/MirSurfaceItem.qml+16−1 tests/mocks/Unity/Indicators/fakeindicatorsmodel.cpp+13−1 tests/mocks/Unity/Indicators/fakeindicatorsmodel.h+6 tests/mocks/Unity/Launcher/CMakeLists.txt+1−1 tests/mocks/Unity/Launcher/MockLauncherModel.cpp+10 tests/mocks/Unity/Launcher/MockLauncherModel.h+3 tests/mocks/Unity/Notifications/CMakeLists.txt+2−1 tests/mocks/Unity/Notifications/MockActionModel.cpp+6−4 tests/mocks/Unity/Notifications/MockActionModel.h+4−2 tests/mocks/Unity/Notifications/MockNotification.cpp+178 tests/mocks/Unity/Notifications/MockNotification.h+94 tests/mocks/Unity/Notifications/MockNotificationModel.cpp+172 tests/mocks/Unity/Notifications/MockNotificationModel.h+72 tests/mocks/Unity/Notifications/plugin.cpp+5−3 tests/mocks/Unity/Notifications/plugin.h+1−2 tests/mocks/libusermetrics/UserMetrics.cpp+8 tests/plugins/CMakeLists.txt+1 tests/plugins/Greeter/CMakeLists.txt+1 tests/plugins/Greeter/Unity/CMakeLists.txt+1 tests/plugins/Greeter/Unity/Launcher/AccountsServiceDBusAdaptor.cpp+48 tests/plugins/Greeter/Unity/Launcher/AccountsServiceDBusAdaptor.h+53 tests/plugins/Greeter/Unity/Launcher/CMakeLists.txt+56 tests/plugins/Greeter/Unity/Launcher/launchermodelastest.cpp+169 tests/plugins/Unity/Indicators/indicatorsmanagertest.cpp+30−5 tests/plugins/Unity/Indicators/indicatorsmodeltest.cpp+6−3 tests/plugins/Unity/Indicators/sharedunitymenumodeltest.cpp+2−2 tests/plugins/Unity/Launcher/AccountsServiceDBusAdaptor.cpp+42 tests/plugins/Unity/Launcher/AccountsServiceDBusAdaptor.h+47 tests/plugins/Unity/Launcher/CMakeLists.txt+3−1 tests/plugins/Unity/Launcher/launchermodeltest.cpp+65−1 tests/qmltests/CMakeLists.txt+2−1 tests/qmltests/Components/tst_EdgeDemoOverlay.qml−123 tests/qmltests/Launcher/tst_Launcher.qml+46−19 tests/mocks/Unity/Notifications/MockNotificationTypes.cpp → tests/qmltests/Notifications/Notification.qml+11−6 tests/qmltests/Notifications/tst_Notifications.qml+271−213 tests/qmltests/Notifications/tst_OptionToggle.qml+11−6 tests/qmltests/Notifications/tst_SwipeToAct.qml+4 tests/qmltests/Panel/tst_Panel.qml+33−2 tests/qmltests/Stages/ApplicationCheckBox.qml+45 tests/qmltests/Stages/RecursingChildSessionControl.qml+3 tests/qmltests/Stages/tst_ApplicationWindow.qml+6−1 tests/qmltests/Stages/tst_PhoneStage.qml+4 tests/qmltests/Stages/tst_SessionContainer.qml+14−7 tests/qmltests/Stages/tst_TabletStage.qml+154 tests/qmltests/Stages/tst_WindowMoveResizeArea.qml+117−18 tests/qmltests/Tutorial/tst_Tutorial.qml+348 tests/qmltests/tst_Shell.qml+55−5 tests/qmltests/tst_ShellWithPin.qml+16 tests/qmltests/tst_TabletShell.qml+2−2 tests/uqmlscene/ActiveFocusLogger.cpp+48 tests/uqmlscene/ActiveFocusLogger.h+37 tests/uqmlscene/CMakeLists.txt+1 tests/uqmlscene/main.cpp+13
debian/changelog+30−21
@@ -1,31 +1,40 @@
1−unity8 (8.02+15.04.20150205-0ubuntu1) vivid; urgency=medium
1+unity8 (8.02+15.04.20150211-0ubuntu1) vivid; urgency=medium
22
3− [ Andrea Cimitan ]
4− * Fix continue button in wifi wizard page, adds qml-module-
5− qtsysteminfo as unity8 dep (LP: #1363400)
6− * Background needs to be specified to be visible in horizontal cards
7− (LP: #1411748)
8−
9− [ CI Train Bot ]
10− * Resync trunk
3+ [ Michael Terry ]
4+ * Redesign tutorial to match latest spec (just removing obsolete pages
5+ and redesigning look, no new pages yet)
6+ * Add new right-edge and bottom-edge screens to the first-boot edge
7+ tutorial (LP: #1383297)
8+ * Support switching the indicator profile on the fly, as the greeter
9+ appears and disappears. (But don't use it yet, not until the
10+ settings panel to control this is in place.)
11+ * Clip the infographic to its bounding box, instead of letting it
12+ bleed past the welcome page (visible when dragging the welcome page
13+ to the side).
1114
12− [ Michał Sawicz ]
13− * Add workaround for gcc ICE.
15+ [ Michael Zanetti ]
16+ * Add an AccountService based launcher model to be used with split
17+ greeter
18+ * Launcher fixes for windowed mode
19+ * save and restore window size and position
20+ * Fix resizing windows when making it smaller than minSize and then
21+ larger again.
1422
15− [ Albert Astals ]
16− * Implement proper updateRanges for horizontal items (i.e. Carousel,
17− Horizontal List)
23+ [ handsome_feng<445865575@qq.com> ]
24+ * makes the header fully reveal when tapping the search icon. (LP:
25+ #1379327)
1826
19− [ Michael Terry ]
20− * Don't accept multiple "Finish" clicks during the last step of the
21− wizard
27+ [ Mirco Müller ]
28+ * Allow swipe-to-dismiss for contracted snap-decision notifications,
29+ interactive notifications and ephemeral notifications. (LP:
30+ #1355422, #1334855)
2231
2332 [ Daniel d'Andrada ]
24− * Unify all liblightdm mocks
25− * Ensure the greeter password field is not covered by the keyboard
26− * Don't show() the lockscreen if it's already being shown
33+ * Make indicators bar eat all events
34+ * Improve Launcher tests, making them more reliable
35+ * Tapping on a surface gives it active focus
2736
28− -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Thu, 05 Feb 2015 10:30:13 +0000
37+ -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Wed, 11 Feb 2015 17:13:16 +0000
2938
3039unity8 (8.02+15.04.20150123.3-0ubuntu1) vivid; urgency=low
3140
debian/control+1−1
@@ -27,7 +27,7 @@ Build-Depends: cmake,
2727 libqmenumodel-dev (>= 0.2.9),
2828 libqt5xmlpatterns5-dev,
2929 libsystemsettings-dev,
30− libunity-api-dev (>= 7.94),
30+ libunity-api-dev (>= 7.95),
3131 libusermetricsoutput1-dev,
3232 libxcb1-dev,
3333 pkg-config,
debian/unity8-private.install+1
@@ -1,6 +1,7 @@
11usr/lib/*/libunity8-private.*
22usr/lib/*/unity8/qml/AccountsService
33usr/lib/*/unity8/qml/Dash
4+usr/lib/*/unity8/qml/Greeter
45usr/lib/*/unity8/qml/LightDM
56usr/lib/*/unity8/qml/Lights
67usr/lib/*/unity8/qml/Powerd
debian/unity8.install+1
@@ -10,5 +10,6 @@ usr/share/unity8/Launcher
1010usr/share/unity8/Panel
1111usr/share/unity8/Shell.qml
1212usr/share/unity8/Stages
13+usr/share/unity8/Tutorial
1314usr/share/unity8/Wizard
1415usr/share/url-dispatcher/urls/unity8-dash.url-dispatcher
plugins/AccountsService/AccountsServiceDBusAdaptor.cpp+9
@@ -57,6 +57,15 @@ void AccountsServiceDBusAdaptor::setUserProperty(const QString &user, const QStr
5757 }
5858}
5959
60+void AccountsServiceDBusAdaptor::setUserPropertyAsync(const QString &user, const QString &interface, const QString &property, const QVariant &value)
61+{
62+ QDBusInterface *iface = getUserInterface(user);
63+ if (iface != nullptr && iface->isValid()) {
64+ // The value needs to be carefully wrapped
65+ iface->asyncCall("Set", interface, property, QVariant::fromValue(QDBusVariant(value)));
66+ }
67+}
68+
6069void AccountsServiceDBusAdaptor::propertiesChangedSlot(const QString &interface, const QVariantMap &changed, const QStringList &invalid)
6170{
6271 // Merge changed and invalidated together
plugins/AccountsService/AccountsServiceDBusAdaptor.h+12
@@ -19,6 +19,7 @@
1919#ifndef UNITY_ACCOUNTSSERVICEDBUSADAPTOR_H
2020#define UNITY_ACCOUNTSSERVICEDBUSADAPTOR_H
2121
22+#include <QDBusArgument>
2223#include <QDBusContext>
2324#include <QDBusInterface>
2425#include <QMap>
@@ -33,7 +34,18 @@ public:
3334 explicit AccountsServiceDBusAdaptor(QObject *parent = 0);
3435
3536 Q_INVOKABLE QVariant getUserProperty(const QString &user, const QString &interface, const QString &property);
37+
38+ template <typename T>
39+ inline T getUserProperty(const QString &user, const QString &interface, const QString &property) {
40+ QVariant variant = getUserProperty(user, interface, property);
41+ if (variant.isValid() && variant.canConvert<QDBusArgument>()) {
42+ return qdbus_cast<T>(variant.value<QDBusArgument>());
43+ }
44+ return T();
45+ }
46+
3647 Q_INVOKABLE void setUserProperty(const QString &user, const QString &interface, const QString &property, const QVariant &value);
48+ Q_INVOKABLE void setUserPropertyAsync(const QString &user, const QString &interface, const QString &property, const QVariant &value);
3749
3850Q_SIGNALS:
3951 void propertiesChanged(const QString &user, const QString &interface, const QStringList &changed);
plugins/AccountsService/com.canonical.unity.AccountsService.xml+1−2
@@ -14,8 +14,7 @@
1414 <annotation name="org.freedesktop.Accounts.DefaultValue" value="true"/>
1515 </property>
1616
17− <!-- Should have been named LauncherItems, sorry folks. -mterry -->
18− <property name="launcher-items" type="aa{sv}" access="readwrite">
17+ <property name="LauncherItems" type="aa{sv}" access="readwrite">
1918 <annotation name="org.freedesktop.Accounts.DefaultValue" value="[{'defaults': <true>}]"/>
2019 </property>
2120
plugins/CMakeLists.txt+1
@@ -12,6 +12,7 @@ macro(add_unity8_plugin PLUGIN VERSION PATH)
1212endmacro()
1313
1414add_subdirectory(AccountsService)
15+add_subdirectory(Greeter)
1516add_subdirectory(LightDM)
1617add_subdirectory(Lights)
1718add_subdirectory(Dash)
plugins/Greeter/CMakeLists.txtnew file+6
@@ -0,0 +1,6 @@
1+macro(add_unity8_greeter_plugin PLUGIN VERSION PATH)
2+ export_qmlfiles(${PLUGIN} ${PATH} DESTINATION ${SHELL_INSTALL_QML}/Greeter TARGET_PREFIX Greeter ${ARGN})
3+ export_qmlplugin(${PLUGIN} ${VERSION} ${PATH} DESTINATION ${SHELL_INSTALL_QML}/Greeter TARGET_PREFIX Greeter ${ARGN})
4+endmacro()
5+
6+add_subdirectory(Unity)
plugins/Greeter/Unity/CMakeLists.txtnew file+1
@@ -0,0 +1 @@
1+add_subdirectory(Launcher)
plugins/Greeter/Unity/Launcher/CMakeLists.txtnew file+38
@@ -0,0 +1,38 @@
1+include(FindPkgConfig)
2+pkg_check_modules(LAUNCHER_API REQUIRED unity-shell-launcher=6)
3+pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=5)
4+pkg_check_modules(GSETTINGS_QT REQUIRED gsettings-qt)
5+
6+add_definitions(-DSM_BUSNAME=systemBus)
7+
8+include_directories(
9+ ${CMAKE_CURRENT_SOURCE_DIR}
10+ ${CMAKE_SOURCE_DIR}/plugins/AccountsService
11+ ${GSETTINGS_QT_INCLUDE_DIRS}
12+ ${libunity8-private_SOURCE_DIR}
13+)
14+
15+set(QMLLAUNCHERPLUGINAS_SRC
16+ plugin.cpp
17+ launchermodelas.cpp
18+ launcheritem.cpp
19+ quicklistmodel.cpp
20+ quicklistentry.cpp
21+ ${CMAKE_SOURCE_DIR}/plugins/AccountsService/AccountsServiceDBusAdaptor.cpp
22+ ${LAUNCHER_API_INCLUDEDIR}/unity/shell/launcher/LauncherItemInterface.h
23+ ${LAUNCHER_API_INCLUDEDIR}/unity/shell/launcher/LauncherModelInterface.h
24+ ${LAUNCHER_API_INCLUDEDIR}/unity/shell/launcher/QuickListModelInterface.h
25+ )
26+
27+add_library(UnityLauncherAS-qml MODULE
28+ ${QMLLAUNCHERPLUGINAS_SRC}
29+ )
30+
31+target_link_libraries(UnityLauncherAS-qml
32+ unity8-private
33+ ${GSETTINGS_QT_LDFLAGS}
34+ )
35+
36+qt5_use_modules(UnityLauncherAS-qml DBus Qml Gui)
37+
38+add_unity8_greeter_plugin(Unity.Launcher 0.1 Unity/Launcher TARGETS UnityLauncherAS-qml)
plugins/Greeter/Unity/Launcher/Launcher.qmltypesnew file+179
@@ -0,0 +1,179 @@
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.Launcher 0.1 plugins'
8+
9+Module {
10+ Component {
11+ name: "LauncherItem"
12+ prototype: "unity::shell::launcher::LauncherItemInterface"
13+ exports: ["Unity.Launcher/LauncherItem 0.1"]
14+ isCreatable: false
15+ exportMetaObjectRevisions: [0]
16+ }
17+ Component {
18+ name: "LauncherModel"
19+ prototype: "unity::shell::launcher::LauncherModelInterface"
20+ exports: ["Unity.Launcher/LauncherModel 0.1"]
21+ isCreatable: false
22+ isSingleton: true
23+ exportMetaObjectRevisions: [0]
24+ Method {
25+ name: "requestRemove"
26+ Parameter { name: "appId"; type: "string" }
27+ }
28+ Method {
29+ name: "get"
30+ type: "unity::shell::launcher::LauncherItemInterface*"
31+ Parameter { name: "index"; type: "int" }
32+ }
33+ Method {
34+ name: "move"
35+ Parameter { name: "oldIndex"; type: "int" }
36+ Parameter { name: "newIndex"; type: "int" }
37+ }
38+ Method {
39+ name: "pin"
40+ Parameter { name: "appId"; type: "string" }
41+ Parameter { name: "index"; type: "int" }
42+ }
43+ Method {
44+ name: "pin"
45+ Parameter { name: "appId"; type: "string" }
46+ }
47+ Method {
48+ name: "quickListActionInvoked"
49+ Parameter { name: "appId"; type: "string" }
50+ Parameter { name: "actionIndex"; type: "int" }
51+ }
52+ Method {
53+ name: "setUser"
54+ Parameter { name: "username"; type: "string" }
55+ }
56+ Method {
57+ name: "getUrlForAppId"
58+ type: "string"
59+ Parameter { name: "appId"; type: "string" }
60+ }
61+ }
62+ Component {
63+ name: "QuickListModel"
64+ prototype: "unity::shell::launcher::QuickListModelInterface"
65+ exports: ["Unity.Launcher/QuickListModel 0.1"]
66+ isCreatable: false
67+ exportMetaObjectRevisions: [0]
68+ }
69+ Component {
70+ name: "unity::shell::launcher::LauncherItemInterface"
71+ prototype: "QObject"
72+ exports: ["Unity.Launcher/LauncherItemInterface 0.1"]
73+ isCreatable: false
74+ exportMetaObjectRevisions: [0]
75+ Property { name: "appId"; type: "string"; isReadonly: true }
76+ Property { name: "name"; type: "string"; isReadonly: true }
77+ Property { name: "icon"; type: "string"; isReadonly: true }
78+ Property { name: "pinned"; type: "bool"; isReadonly: true }
79+ Property { name: "running"; type: "bool"; isReadonly: true }
80+ Property { name: "recent"; type: "bool"; isReadonly: true }
81+ Property { name: "progress"; type: "int"; isReadonly: true }
82+ Property { name: "count"; type: "int"; isReadonly: true }
83+ Property { name: "countVisible"; type: "bool"; isReadonly: true }
84+ Property { name: "focused"; type: "bool"; isReadonly: true }
85+ Property {
86+ name: "quickList"
87+ type: "unity::shell::launcher::QuickListModelInterface"
88+ isReadonly: true
89+ isPointer: true
90+ }
91+ Signal {
92+ name: "nameChanged"
93+ Parameter { name: "name"; type: "string" }
94+ }
95+ Signal {
96+ name: "iconChanged"
97+ Parameter { name: "icon"; type: "string" }
98+ }
99+ Signal {
100+ name: "pinnedChanged"
101+ Parameter { name: "pinned"; type: "bool" }
102+ }
103+ Signal {
104+ name: "runningChanged"
105+ Parameter { name: "running"; type: "bool" }
106+ }
107+ Signal {
108+ name: "recentChanged"
109+ Parameter { name: "running"; type: "bool" }
110+ }
111+ Signal {
112+ name: "progressChanged"
113+ Parameter { name: "progress"; type: "int" }
114+ }
115+ Signal {
116+ name: "countChanged"
117+ Parameter { name: "count"; type: "int" }
118+ }
119+ Signal {
120+ name: "countVisibleChanged"
121+ Parameter { name: "countVisible"; type: "bool" }
122+ }
123+ Signal {
124+ name: "focusedChanged"
125+ Parameter { name: "focused"; type: "bool" }
126+ }
127+ }
128+ Component {
129+ name: "unity::shell::launcher::LauncherModelInterface"
130+ prototype: "QAbstractListModel"
131+ exports: ["Unity.Launcher/LauncherModelInterface 0.1"]
132+ isCreatable: false
133+ exportMetaObjectRevisions: [0]
134+ Property {
135+ name: "applicationManager"
136+ type: "unity::shell::application::ApplicationManagerInterface"
137+ isPointer: true
138+ }
139+ Method {
140+ name: "move"
141+ Parameter { name: "oldIndex"; type: "int" }
142+ Parameter { name: "newIndex"; type: "int" }
143+ }
144+ Method {
145+ name: "get"
146+ type: "unity::shell::launcher::LauncherItemInterface*"
147+ Parameter { name: "index"; type: "int" }
148+ }
149+ Method {
150+ name: "pin"
151+ Parameter { name: "appId"; type: "string" }
152+ Parameter { name: "index"; type: "int" }
153+ }
154+ Method {
155+ name: "pin"
156+ Parameter { name: "appId"; type: "string" }
157+ }
158+ Method {
159+ name: "requestRemove"
160+ Parameter { name: "appId"; type: "string" }
161+ }
162+ Method {
163+ name: "quickListActionInvoked"
164+ Parameter { name: "appId"; type: "string" }
165+ Parameter { name: "actionIndex"; type: "int" }
166+ }
167+ Method {
168+ name: "setUser"
169+ Parameter { name: "username"; type: "string" }
170+ }
171+ }
172+ Component {
173+ name: "unity::shell::launcher::QuickListModelInterface"
174+ prototype: "QAbstractListModel"
175+ exports: ["Unity.Launcher/QuickListInterface 0.1"]
176+ isCreatable: false
177+ exportMetaObjectRevisions: [0]
178+ }
179+}
plugins/Greeter/Unity/Launcher/launcheritem.cppnew file+171
@@ -0,0 +1,171 @@
1+/*
2+ * Copyright 2014-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 "launcheritem.h"
18+#include "quicklistmodel.h"
19+
20+#include <libintl.h>
21+
22+LauncherItem::LauncherItem(const QString &appId, const QString &name, const QString &icon, QObject *parent) :
23+ LauncherItemInterface(parent),
24+ m_appId(appId),
25+ m_name(name),
26+ m_icon(icon),
27+ m_pinned(false),
28+ m_running(false),
29+ m_recent(false),
30+ m_progress(-1),
31+ m_count(0),
32+ m_countVisible(false),
33+ m_focused(false),
34+ m_quickList(new QuickListModel(this))
35+{
36+ QuickListEntry nameAction;
37+ nameAction.setActionId("launch_item");
38+ nameAction.setText(m_name);
39+ m_quickList->appendAction(nameAction);
40+}
41+
42+QString LauncherItem::appId() const
43+{
44+ return m_appId;
45+}
46+
47+QString LauncherItem::name() const
48+{
49+ return m_name;
50+}
51+
52+void LauncherItem::setName(const QString &name)
53+{
54+ if (m_name != name) {
55+ m_name = name;
56+ QuickListEntry entry;
57+ entry.setActionId("launch_item");
58+ entry.setText(m_name);
59+ m_quickList->updateAction(entry);
60+ Q_EMIT nameChanged(name);
61+ }
62+}
63+
64+QString LauncherItem::icon() const
65+{
66+ return m_icon;
67+}
68+
69+void LauncherItem::setIcon(const QString &icon)
70+{
71+ if (m_icon != icon) {
72+ m_icon = icon;
73+ Q_EMIT iconChanged(icon);
74+ }
75+}
76+
77+bool LauncherItem::pinned() const
78+{
79+ return m_pinned;
80+}
81+
82+void LauncherItem::setPinned(bool pinned)
83+{
84+ if (m_pinned != pinned) {
85+ m_pinned = pinned;
86+ Q_EMIT pinnedChanged(pinned);
87+ }
88+}
89+
90+bool LauncherItem::running() const
91+{
92+ return m_running;
93+}
94+
95+void LauncherItem::setRunning(bool running)
96+{
97+ if (m_running != running) {
98+ m_running = running;
99+ Q_EMIT runningChanged(running);
100+ }
101+}
102+
103+bool LauncherItem::recent() const
104+{
105+ return m_recent;
106+}
107+
108+void LauncherItem::setRecent(bool recent)
109+{
110+ if (m_recent != recent) {
111+ m_recent = recent;
112+ Q_EMIT recentChanged(recent);
113+ }
114+}
115+
116+int LauncherItem::progress() const
117+{
118+ return m_progress;
119+}
120+
121+void LauncherItem::setProgress(int progress)
122+{
123+ if (m_progress != progress) {
124+ m_progress = progress;
125+ Q_EMIT progressChanged(progress);
126+ }
127+}
128+
129+int LauncherItem::count() const
130+{
131+ return m_count;
132+}
133+
134+void LauncherItem::setCount(int count)
135+{
136+ if (m_count != count) {
137+ m_count = count;
138+ Q_EMIT countChanged(count);
139+ }
140+}
141+
142+bool LauncherItem::countVisible() const
143+{
144+ return m_countVisible;
145+}
146+
147+void LauncherItem::setCountVisible(bool countVisible)
148+{
149+ if (m_countVisible != countVisible) {
150+ m_countVisible = countVisible;
151+ Q_EMIT countVisibleChanged(countVisible);
152+ }
153+}
154+
155+bool LauncherItem::focused() const
156+{
157+ return m_focused;
158+}
159+
160+void LauncherItem::setFocused(bool focused)
161+{
162+ if (m_focused != focused) {
163+ m_focused = focused;
164+ Q_EMIT focusedChanged(focused);
165+ }
166+}
167+
168+unity::shell::launcher::QuickListModelInterface *LauncherItem::quickList() const
169+{
170+ return m_quickList;
171+}
plugins/Greeter/Unity/Launcher/launcheritem.hnew file+74
@@ -0,0 +1,74 @@
1+/*
2+ * Copyright 2014-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 LAUNCHERITEM_H
18+#define LAUNCHERITEM_H
19+
20+#include "quicklistmodel.h"
21+
22+#include <unity/shell/launcher/LauncherItemInterface.h>
23+
24+class QuickListModel;
25+
26+using namespace unity::shell::launcher;
27+
28+class LauncherItem: public LauncherItemInterface
29+{
30+ Q_OBJECT
31+public:
32+ LauncherItem(const QString &appId, const QString &name, const QString &icon, QObject *parent = 0);
33+
34+ QString appId() const override;
35+ QString name() const override;
36+ QString icon() const override;
37+ bool pinned() const override;
38+ bool running() const override;
39+ bool recent() const override;
40+ int progress() const override;
41+ int count() const override;
42+ bool countVisible() const override;
43+ bool focused() const override;
44+
45+ unity::shell::launcher::QuickListModelInterface *quickList() const override;
46+
47+private:
48+ void setName(const QString &name);
49+ void setIcon(const QString &icon);
50+ void setPinned(bool pinned);
51+ void setRunning(bool running);
52+ void setRecent(bool recent);
53+ void setProgress(int progress);
54+ void setCount(int count);
55+ void setCountVisible(bool countVisible);
56+ void setFocused(bool focused);
57+
58+private:
59+ QString m_appId;
60+ QString m_name;
61+ QString m_icon;
62+ bool m_pinned;
63+ bool m_running;
64+ bool m_recent;
65+ int m_progress;
66+ int m_count;
67+ bool m_countVisible;
68+ bool m_focused;
69+ QuickListModel *m_quickList;
70+
71+ friend class LauncherModel;
72+};
73+
74+#endif // LAUNCHERITEM_H
plugins/Greeter/Unity/Launcher/launchermodelas.cppnew file+277
@@ -0,0 +1,277 @@
1+/*
2+ * Copyright 2014-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 "launchermodelas.h"
18+#include "launcheritem.h"
19+#include "AccountsServiceDBusAdaptor.h"
20+#include <unity/shell/application/ApplicationInfoInterface.h>
21+
22+#include <QDesktopServices>
23+#include <QDebug>
24+#include <QDBusArgument>
25+
26+using namespace unity::shell::application;
27+
28+LauncherModel::LauncherModel(QObject *parent):
29+ LauncherModelInterface(parent),
30+ m_accounts(new AccountsServiceDBusAdaptor(this)),
31+ m_onlyPinned(true)
32+{
33+ connect(m_accounts, &AccountsServiceDBusAdaptor::propertiesChanged, this, &LauncherModel::propertiesChanged);
34+ refresh();
35+}
36+
37+LauncherModel::~LauncherModel()
38+{
39+ while (!m_list.empty()) {
40+ m_list.takeFirst()->deleteLater();
41+ }
42+}
43+
44+int LauncherModel::rowCount(const QModelIndex &parent) const
45+{
46+ Q_UNUSED(parent)
47+ return m_list.count();
48+}
49+
50+QVariant LauncherModel::data(const QModelIndex &index, int role) const
51+{
52+ LauncherItem *item = m_list.at(index.row());
53+ switch(role) {
54+ case RoleAppId:
55+ return item->appId();
56+ case RoleName:
57+ return item->name();
58+ case RoleIcon:
59+ return item->icon();
60+ case RolePinned:
61+ return item->pinned();
62+ case RoleCount:
63+ return item->count();
64+ case RoleCountVisible:
65+ return item->countVisible();
66+ case RoleProgress:
67+ return item->progress();
68+ case RoleFocused:
69+ return item->focused();
70+ }
71+
72+ return QVariant();
73+}
74+
75+unity::shell::launcher::LauncherItemInterface *LauncherModel::get(int index) const
76+{
77+ if (index < 0 || index >= m_list.count()) {
78+ return 0;
79+ }
80+ return m_list.at(index);
81+}
82+
83+void LauncherModel::move(int oldIndex, int newIndex)
84+{
85+ Q_UNUSED(oldIndex)
86+ Q_UNUSED(newIndex)
87+ qWarning() << "This is a read only implementation. Cannot move items.";
88+}
89+
90+void LauncherModel::pin(const QString &appId, int index)
91+{
92+ Q_UNUSED(appId)
93+ Q_UNUSED(index)
94+ qWarning() << "This is a read only implementation. Cannot pin items";
95+}
96+
97+void LauncherModel::requestRemove(const QString &appId)
98+{
99+ Q_UNUSED(appId)
100+ qWarning() << "This is a read only implementation. Cannot remove items";
101+}
102+
103+void LauncherModel::quickListActionInvoked(const QString &appId, int actionIndex)
104+{
105+ int index = findApplication(appId);
106+ if (index < 0) {
107+ return;
108+ }
109+
110+ LauncherItem *item = m_list.at(index);
111+ QuickListModel *model = qobject_cast<QuickListModel*>(item->quickList());
112+ if (model) {
113+ QString actionId = model->get(actionIndex).actionId();
114+
115+ // Check if this is one of the launcher actions we handle ourselves
116+ if (actionId == "launch_item") {
117+ QDesktopServices::openUrl(getUrlForAppId(appId));
118+
119+ // Nope, we don't know this action, let the backend forward it to the application
120+ } else {
121+ // TODO: forward quicklist action to app, possibly via m_dbusIface
122+ }
123+ }
124+}
125+
126+void LauncherModel::setUser(const QString &username)
127+{
128+ if (m_user != username) {
129+ m_user = username;
130+ refresh();
131+ }
132+}
133+
134+QString LauncherModel::getUrlForAppId(const QString &appId) const
135+{
136+ // appId is either an appId or a legacy app name. Let's find out which
137+ if (appId.isEmpty()) {
138+ return QString();
139+ }
140+
141+ if (!appId.contains("_")) {
142+ return "application:///" + appId + ".desktop";
143+ }
144+
145+ QStringList parts = appId.split('_');
146+ QString package = parts.value(0);
147+ QString app = parts.value(1, "first-listed-app");
148+ return "appid://" + package + "/" + app + "/current-user-version";
149+}
150+
151+ApplicationManagerInterface *LauncherModel::applicationManager() const
152+{
153+ return nullptr;
154+}
155+
156+void LauncherModel::setApplicationManager(unity::shell::application::ApplicationManagerInterface *appManager)
157+{
158+ Q_UNUSED(appManager)
159+ qWarning() << "This plugin syncs all its states from AccountsService. Not using ApplicationManager.";
160+ return;
161+}
162+
163+bool LauncherModel::onlyPinned() const
164+{
165+ return m_onlyPinned;
166+}
167+
168+void LauncherModel::setOnlyPinned(bool onlyPinned)
169+{
170+ if (m_onlyPinned != onlyPinned) {
171+ m_onlyPinned = onlyPinned;
172+ Q_EMIT onlyPinnedChanged();
173+ refresh();
174+ }
175+}
176+
177+int LauncherModel::findApplication(const QString &appId)
178+{
179+ for (int i = 0; i < m_list.count(); ++i) {
180+ LauncherItem *item = m_list.at(i);
181+ if (item->appId() == appId) {
182+ return i;
183+ }
184+ }
185+ return -1;
186+}
187+
188+typedef QList<QVariantMap> VariantMapList;
189+void LauncherModel::refresh()
190+{
191+ QList<QVariantMap> items;
192+
193+ if (m_accounts && !m_user.isEmpty()) {
194+ items = m_accounts->getUserProperty<VariantMapList>(m_user, "com.canonical.unity.AccountsService", "LauncherItems");
195+ }
196+
197+ // First walk through all the existing items and see if we need to remove something
198+ QList<LauncherItem*> toBeRemoved;
199+
200+ Q_FOREACH (LauncherItem* item, m_list) {
201+ bool found = false;
202+ Q_FOREACH(const QVariant &asItem, items) {
203+ if (asItem.toMap().value("id").toString() == item->appId()) {
204+ // Only keep and update it if we either show unpinned or it is pinned
205+ if (!m_onlyPinned || asItem.toMap().value("pinned").toBool()) {
206+ found = true;
207+ item->setName(asItem.toMap().value("name").toString());
208+ item->setIcon(asItem.toMap().value("icon").toString());
209+ item->setCount(asItem.toMap().value("count").toInt());
210+ item->setCountVisible(asItem.toMap().value("countVisible").toBool());
211+ int idx = m_list.indexOf(item);
212+ Q_EMIT dataChanged(index(idx), index(idx), QVector<int>() << RoleName << RoleIcon);
213+ }
214+ break;
215+ }
216+ }
217+ if (!found) {
218+ toBeRemoved.append(item);
219+ }
220+ }
221+
222+ Q_FOREACH (LauncherItem* item, toBeRemoved) {
223+ int idx = m_list.indexOf(item);
224+ beginRemoveRows(QModelIndex(), idx, idx);
225+ m_list.takeAt(idx)->deleteLater();
226+ endRemoveRows();
227+ }
228+
229+ // Now walk through list and see if we need to add something
230+ int skipped = 0;
231+ for (int asIndex = 0; asIndex < items.count(); ++asIndex) {
232+ QVariant entry = items.at(asIndex);
233+
234+ if (m_onlyPinned && !entry.toMap().value("pinned").toBool()) {
235+ // Skipping it as we only show pinned and it is not
236+ skipped++;
237+ continue;
238+ }
239+ int newPosition = asIndex - skipped;
240+
241+ int itemIndex = -1;
242+ for (int i = 0; i < m_list.count(); ++i) {
243+ if (m_list.at(i)->appId() == entry.toMap().value("id").toString()) {
244+ itemIndex = i;
245+ break;
246+ }
247+ }
248+
249+ if (itemIndex == -1) {
250+ // Need to add it. Just add it into the addedIndex to keep same ordering as the list in AS.
251+ LauncherItem *item = new LauncherItem(entry.toMap().value("id").toString(),
252+ entry.toMap().value("name").toString(),
253+ entry.toMap().value("icon").toString(),
254+ this);
255+ item->setPinned(true);
256+ item->setCount(entry.toMap().value("count").toInt());
257+ item->setCountVisible(entry.toMap().value("countVisible").toBool());
258+ beginInsertRows(QModelIndex(), newPosition, newPosition);
259+ m_list.insert(newPosition, item);
260+ endInsertRows();
261+ } else if (itemIndex != newPosition) {
262+ // The item is already there, but it is in a different place than in the settings.
263+ // Move it to the addedIndex
264+ beginMoveRows(QModelIndex(), itemIndex, itemIndex, QModelIndex(), newPosition);
265+ m_list.move(itemIndex, newPosition);
266+ endMoveRows();
267+ }
268+ }
269+}
270+
271+void LauncherModel::propertiesChanged(const QString &user, const QString &interface, const QStringList &changed)
272+{
273+ if (user != m_user || interface != "com.canonical.unity.AccountsService" || !changed.contains("LauncherItems")) {
274+ return;
275+ }
276+ refresh();
277+}
plugins/Greeter/Unity/Launcher/launchermodelas.hnew file+75
@@ -0,0 +1,75 @@
1+/*
2+ * Copyright 2014-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 LAUNCHERMODEL_H
18+#define LAUNCHERMODEL_H
19+
20+#include <unity/shell/launcher/LauncherModelInterface.h>
21+#include <unity/shell/application/ApplicationManagerInterface.h>
22+
23+#include <QAbstractListModel>
24+
25+class LauncherItem;
26+class GSettings;
27+class AccountsServiceDBusAdaptor;
28+
29+using namespace unity::shell::launcher;
30+using namespace unity::shell::application;
31+
32+class LauncherModel: public LauncherModelInterface
33+{
34+ Q_OBJECT
35+
36+public:
37+ LauncherModel(QObject *parent = 0);
38+ ~LauncherModel();
39+
40+ int rowCount(const QModelIndex &parent = QModelIndex()) const;
41+
42+ QVariant data(const QModelIndex &index, int role) const;
43+
44+ Q_INVOKABLE unity::shell::launcher::LauncherItemInterface* get(int index) const;
45+ Q_INVOKABLE void move(int oldIndex, int newIndex);
46+ Q_INVOKABLE void pin(const QString &appId, int index = -1);
47+ Q_INVOKABLE void quickListActionInvoked(const QString &appId, int actionIndex);
48+ Q_INVOKABLE void setUser(const QString &username);
49+ Q_INVOKABLE QString getUrlForAppId(const QString &appId) const;
50+
51+ unity::shell::application::ApplicationManagerInterface* applicationManager() const;
52+ void setApplicationManager(unity::shell::application::ApplicationManagerInterface *appManager);
53+
54+ bool onlyPinned() const override;
55+ void setOnlyPinned(bool onlyPinned) override;
56+
57+ int findApplication(const QString &appId);
58+
59+public Q_SLOTS:
60+ void requestRemove(const QString &appId) override;
61+ Q_INVOKABLE void refresh();
62+
63+private Q_SLOTS:
64+ void propertiesChanged(const QString &user, const QString &interface, const QStringList &changed);
65+
66+private:
67+ QString m_user;
68+ QList<LauncherItem*> m_list;
69+ AccountsServiceDBusAdaptor *m_accounts;
70+ bool m_onlyPinned;
71+
72+ friend class LauncherModelASTest;
73+};
74+
75+#endif // LAUNCHERMODEL_H
plugins/Greeter/Unity/Launcher/plugin.cppnew file+46
@@ -0,0 +1,46 @@
1+/*
2+ * Copyright 2014-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+// Qt
18+#include <QtQml/qqml.h>
19+
20+// self
21+#include "plugin.h"
22+
23+// local
24+#include "launchermodelas.h"
25+#include "launcheritem.h"
26+
27+
28+using namespace unity::shell::launcher;
29+
30+static QObject* modelProvider(QQmlEngine* /* engine */, QJSEngine* /* scriptEngine */)
31+{
32+ return new LauncherModel();
33+}
34+
35+void UnityLauncherPlugin::registerTypes(const char *uri)
36+{
37+ Q_ASSERT(uri == QLatin1String("Unity.Launcher"));
38+
39+ qmlRegisterUncreatableType<LauncherModelInterface>(uri, 0, 1, "LauncherModelInterface", "Abstract Interface. Cannot be instantiated.");
40+ qmlRegisterUncreatableType<LauncherItemInterface>(uri, 0, 1, "LauncherItemInterface", "Abstract Interface. Cannot be instantiated.");
41+ qmlRegisterUncreatableType<QuickListModelInterface>(uri, 0, 1, "QuickListInterface", "Abstract Interface. Cannot be instantiated.");
42+
43+ qmlRegisterSingletonType<LauncherModel>(uri, 0, 1, "LauncherModel", modelProvider);
44+ qmlRegisterUncreatableType<LauncherItem>(uri, 0, 1, "LauncherItem", "Can't create new Launcher Items in QML. Get them from the LauncherModel.");
45+ qmlRegisterUncreatableType<QuickListModel>(uri, 0, 1, "QuickListModel", "Can't create a QuickListModel in QML. Get them from the LauncherItems.");
46+}
plugins/Greeter/Unity/Launcher/plugin.hnew file+32
@@ -0,0 +1,32 @@
1+/*
2+ * Copyright (C) 2014-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 General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef UNITY_LAUNCHER_PLUGIN_H
18+#define UNITY_LAUNCHER_PLUGIN_H
19+
20+#include <QtQml/QQmlEngine>
21+#include <QtQml/QQmlExtensionPlugin>
22+
23+class UnityLauncherPlugin : public QQmlExtensionPlugin
24+{
25+ Q_OBJECT
26+ Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface")
27+
28+public:
29+ void registerTypes(const char *uri);
30+};
31+
32+#endif
plugins/Greeter/Unity/Launcher/qmldirnew file+3
@@ -0,0 +1,3 @@
1+module Unity.Launcher
2+plugin UnityLauncherAS-qml
3+typeinfo Launcher.qmltypes
plugins/Greeter/Unity/Launcher/quicklistentry.cppnew file+56
@@ -0,0 +1,56 @@
1+/* Copyright (C) 2014-2015 Canonical, Ltd.
2+ *
3+ * This program is free software; you can redistribute it and/or modify
4+ * it under the terms of the GNU General Public License as published by
5+ * the Free Software Foundation; version 3.
6+ *
7+ * This program is distributed in the hope that it will be useful,
8+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
9+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10+ * GNU General Public License for more details.
11+ *
12+ * You should have received a copy of the GNU General Public License
13+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
14+ */
15+
16+#include "quicklistentry.h"
17+
18+QuickListEntry::QuickListEntry()
19+{
20+
21+}
22+
23+QString QuickListEntry::actionId() const
24+{
25+ return m_actionId;
26+}
27+
28+void QuickListEntry::setActionId(const QString &actionId)
29+{
30+ m_actionId = actionId;
31+}
32+
33+QString QuickListEntry::text() const
34+{
35+ return m_text;
36+}
37+
38+void QuickListEntry::setText(const QString &text)
39+{
40+ m_text = text;
41+}
42+
43+QString QuickListEntry::icon() const
44+{
45+ return m_icon;
46+}
47+
48+void QuickListEntry::setIcon(const QString &icon)
49+{
50+ m_icon = icon;
51+}
52+
53+bool QuickListEntry::clickable() const
54+{
55+ return !m_actionId.isEmpty();
56+}
plugins/Greeter/Unity/Launcher/quicklistentry.hnew file+43
@@ -0,0 +1,43 @@
1+/* Copyright (C) 2014-2015 Canonical, Ltd.
2+ *
3+ * This program is free software; you can redistribute it and/or modify
4+ * it under the terms of the GNU General Public License as published by
5+ * the Free Software Foundation; version 3.
6+ *
7+ * This program is distributed in the hope that it will be useful,
8+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
9+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10+ * GNU General Public License for more details.
11+ *
12+ * You should have received a copy of the GNU General Public License
13+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
14+ */
15+
16+#ifndef QUICKLISTENTRY_H
17+#define QUICKLISTENTRY_H
18+
19+#include <QString>
20+
21+class QuickListEntry
22+{
23+public:
24+ QuickListEntry();
25+
26+ QString actionId() const;
27+ void setActionId(const QString &actionId);
28+
29+ QString text() const;
30+ void setText(const QString &text);
31+
32+ QString icon() const;
33+ void setIcon(const QString &icon);
34+
35+ bool clickable() const;
36+
37+private:
38+ QString m_actionId;
39+ QString m_text;
40+ QString m_icon;
41+};
42+
43+#endif // QUICKLISTENTRY
plugins/Greeter/Unity/Launcher/quicklistmodel.cppnew file+70
@@ -0,0 +1,70 @@
1+/*
2+ * Copyright 2014-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 "quicklistmodel.h"
18+
19+QuickListModel::QuickListModel(QObject *parent) :
20+ QuickListModelInterface(parent)
21+{
22+
23+}
24+
25+QuickListModel::~QuickListModel()
26+{
27+
28+}
29+
30+void QuickListModel::appendAction(const QuickListEntry &entry)
31+{
32+ beginInsertRows(QModelIndex(), m_list.count(), m_list.count());
33+ m_list.append(entry);
34+ endInsertRows();
35+}
36+
37+void QuickListModel::updateAction(const QuickListEntry &entry)
38+{
39+ for (int i = 0; i < m_list.count(); ++i) {
40+ if (m_list.at(i).actionId() == entry.actionId()) {
41+ m_list.replace(i, entry);
42+ Q_EMIT dataChanged(index(i), index(i));
43+ return;
44+ }
45+ }
46+}
47+
48+QuickListEntry QuickListModel::get(int index) const
49+{
50+ return m_list.at(index);
51+}
52+
53+int QuickListModel::rowCount(const QModelIndex &index) const
54+{
55+ Q_UNUSED(index)
56+ return m_list.count();
57+}
58+
59+QVariant QuickListModel::data(const QModelIndex &index, int role) const
60+{
61+ switch (role) {
62+ case RoleLabel:
63+ return m_list.at(index.row()).text();
64+ case RoleIcon:
65+ return m_list.at(index.row()).icon();
66+ case RoleClickable:
67+ return m_list.at(index.row()).clickable();
68+ }
69+ return QVariant();
70+}
plugins/Greeter/Unity/Launcher/quicklistmodel.hnew file+55
@@ -0,0 +1,55 @@
1+/*
2+ * Copyright 2014-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 QUICKLISTMODEL_H
18+#define QUICKLISTMODEL_H
19+
20+#include "quicklistentry.h"
21+
22+#include <unity/shell/launcher/QuickListModelInterface.h>
23+
24+using namespace unity::shell::launcher;
25+
26+class QuickListModel: public QuickListModelInterface
27+{
28+ Q_OBJECT
29+
30+public:
31+ explicit QuickListModel(QObject *parent = 0);
32+
33+ ~QuickListModel();
34+
35+ void appendAction(const QuickListEntry &entry);
36+
37+ /**
38+ * @brief Update an existing action
39+ * @param entry The new, updated entry
40+ *
41+ * This will only do something if entry.actionId is found in the model.
42+ * To add a new entry, use appendAction().
43+ */
44+ void updateAction(const QuickListEntry &entry);
45+
46+ QuickListEntry get(int index) const;
47+
48+ int rowCount(const QModelIndex &parent = QModelIndex()) const;
49+ QVariant data(const QModelIndex &index, int role) const;
50+
51+private:
52+ QList<QuickListEntry> m_list;
53+};
54+
55+#endif // QUICKLISTMODEL_H
plugins/Ubuntu/Gestures/TouchGate.cpp+2
@@ -53,6 +53,8 @@ void TouchGate::touchEvent(QTouchEvent *event)
5353 m_touchInfoMap[touchPoint.id()].ownership = OwnershipRequested;
5454 m_touchInfoMap[touchPoint.id()].ended = false;
5555 TouchRegistry::instance()->requestTouchOwnership(touchPoint.id(), this);
56+
57+ Q_EMIT pressed();
5658 }
5759
5860 goodToGo &= m_touchInfoMap.contains(touchPoint.id())
plugins/Ubuntu/Gestures/TouchGate.h+2
@@ -53,6 +53,8 @@ public:
5353Q_SIGNALS:
5454 void targetItemChanged(QQuickItem *item);
5555
56+ void pressed();
57+
5658protected:
5759 void touchEvent(QTouchEvent *event) override;
5860private:
plugins/Unity/Indicators/indicator.cpp+23−8
@@ -31,24 +31,26 @@ Indicator::~Indicator()
3131{
3232}
3333
34−void Indicator::init(const QString &profile, const QString& busName, const QSettings& settings)
34+void Indicator::init(const QString& busName, const QSettings& settings)
3535{
36− setId(settings.value("Indicator Service/Name").toString());
36+ // Save all keys we care about from the QSettings object. It's annoying
37+ // that we can't just copy the object.
38+ m_settings.clear();
39+ Q_FOREACH(const QString& key, settings.allKeys()) {
40+ if (key.endsWith("/Position") || key.endsWith("/ObjectPath")) {
41+ m_settings.insert(key, settings.value(key));
42+ }
43+ }
3744
38− QVariant pos = settings.value(profile + "/Position");
39− if (!pos.isValid())
40− pos = settings.value("Indicator Service/Position", QVariant::fromValue(0));
41− setPosition(pos.toInt());
45+ setId(settings.value("Indicator Service/Name").toString());
4246
4347 QString actionObjectPath = settings.value("Indicator Service/ObjectPath").toString();
44− QString menuObjectPath = settings.value(profile + "/ObjectPath").toString();
4548
4649 QVariantMap properties;
4750 properties.clear();
4851 properties.insert("busType", 1);
4952 properties.insert("busName", busName);
5053 properties.insert("actionsObjectPath", actionObjectPath);
51− properties.insert("menuObjectPath", menuObjectPath);
5254 setIndicatorProperties(properties);
5355}
5456
@@ -78,6 +80,19 @@ void Indicator::setPosition(int position)
7880 }
7981}
8082
83+void Indicator::setProfile(const QString& profile)
84+{
85+ QVariant pos = m_settings.value(profile + "/Position");
86+ if (!pos.isValid())
87+ pos = m_settings.value("Indicator Service/Position", QVariant::fromValue(0));
88+ setPosition(pos.toInt());
89+
90+ QString menuObjectPath = m_settings.value(profile + "/ObjectPath").toString();
91+ QVariantMap map = m_properties.toMap();
92+ map.insert("menuObjectPath", menuObjectPath);
93+ setIndicatorProperties(map);
94+}
95+
8196QVariant Indicator::indicatorProperties() const
8297{
8398 return m_properties;
plugins/Unity/Indicators/indicator.h+5−1
@@ -35,12 +35,15 @@ public:
3535 Indicator(QObject *parent = 0);
3636 virtual ~Indicator();
3737
38− void init(const QString &profile, const QString& busName, const QSettings& settings);
38+ void init(const QString& busName, const QSettings& settings);
3939
4040 QString identifier() const;
4141 int position() const;
4242 QVariant indicatorProperties() const;
4343
44+public Q_SLOTS:
45+ void setProfile(const QString& profile);
46+
4447Q_SIGNALS:
4548 void identifierChanged(const QString &identifier);
4649 void positionChanged(int position);
@@ -55,6 +58,7 @@ private:
5558 QString m_identifier;
5659 int m_position;
5760 QVariant m_properties;
61+ QVariantMap m_settings;
5862};
5963
6064#endif // INDICATOR_H
plugins/Unity/Indicators/indicatorsmanager.cpp+17−3
@@ -54,10 +54,9 @@ IndicatorsManager::~IndicatorsManager()
5454
5555}
5656
57−void IndicatorsManager::load(const QString& profile)
57+void IndicatorsManager::load()
5858{
5959 unload();
60− m_profile = profile;
6160
6261 QStringList xdgLocations = shellDataDirs();//QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation);
6362
@@ -216,6 +215,19 @@ void IndicatorsManager::setLoaded(bool loaded)
216215 }
217216}
218217
218+QString IndicatorsManager::profile() const
219+{
220+ return m_profile;
221+}
222+
223+void IndicatorsManager::setProfile(const QString& profile)
224+{
225+ if (m_profile != profile) {
226+ m_profile = profile;
227+ Q_EMIT profileChanged(m_profile);
228+ }
229+}
230+
219231void IndicatorsManager::startVerify(const QString& path)
220232{
221233 QHashIterator<QString, IndicatorData*> iter(m_indicatorsData);
@@ -268,7 +280,9 @@ Indicator::Ptr IndicatorsManager::indicator(const QString& indicator_name)
268280 Indicator::Ptr new_indicator(new Indicator(this));
269281 data->m_indicator = new_indicator;
270282 QSettings settings(data->m_fileInfo.absoluteFilePath(), QSettings::IniFormat, this);
271− new_indicator->init(m_profile, data->m_fileInfo.fileName(), settings);
283+ new_indicator->init(data->m_fileInfo.fileName(), settings);
284+ new_indicator->setProfile(m_profile);
285+ QObject::connect(this, SIGNAL(profileChanged(const QString&)), new_indicator.data(), SLOT(setProfile(const QString&)));
272286 return new_indicator;
273287}
274288
plugins/Unity/Indicators/indicatorsmanager.h+6−1
@@ -32,13 +32,17 @@ class UNITYINDICATORS_EXPORT IndicatorsManager : public QObject
3232{
3333 Q_OBJECT
3434 Q_PROPERTY(bool loaded READ isLoaded NOTIFY loadedChanged)
35+ Q_PROPERTY(QString profile READ profile WRITE setProfile NOTIFY profileChanged)
3536public:
3637 explicit IndicatorsManager(QObject* parent = 0);
3738 ~IndicatorsManager();
3839
39− Q_INVOKABLE void load(const QString& profile);
40+ Q_INVOKABLE void load();
4041 Q_INVOKABLE void unload();
4142
43+ QString profile() const;
44+ void setProfile(const QString& profile);
45+
4246 Indicator::Ptr indicator(const QString& indicator_name);
4347
4448 QList<Indicator::Ptr> indicators();
@@ -47,6 +51,7 @@ public:
4751
4852Q_SIGNALS:
4953 void loadedChanged(bool);
54+ void profileChanged(const QString&);
5055
5156 void indicatorLoaded(const QString& indicator_name);
5257 void indicatorAboutToBeUnloaded(const QString& indicator_name);
plugins/Unity/Indicators/indicatorsmodel.cpp+25−2
@@ -56,6 +56,7 @@ IndicatorsModel::IndicatorsModel(QObject *parent)
5656 m_manager = new IndicatorsManager(this);
5757 QObject::connect(m_manager, SIGNAL(indicatorLoaded(const QString&)), this, SLOT(onIndicatorLoaded(const QString&)));
5858 QObject::connect(m_manager, SIGNAL(indicatorAboutToBeUnloaded(const QString&)), this, SLOT(onIndicatorAboutToBeUnloaded(const QString&)));
59+ QObject::connect(m_manager, SIGNAL(profileChanged(const QString&)), this, SIGNAL(profileChanged()));
5960
6061 QObject::connect(this, SIGNAL(rowsInserted(const QModelIndex &, int, int)), this, SIGNAL(countChanged()));
6162 QObject::connect(this, SIGNAL(rowsRemoved(const QModelIndex &, int, int)), this, SIGNAL(countChanged()));
@@ -80,15 +81,37 @@ int IndicatorsModel::count() const
8081 return rowCount();
8182}
8283
84+/*!
85+ \qmlproperty IndicatorsModel::profile
86+ The indicator profile to use for the model.
87+
88+ \b Note: methods should only be called after the Component has completed.
89+*/
90+QString IndicatorsModel::profile() const
91+{
92+ return m_manager->profile();
93+}
94+
95+/*!
96+ \qmlproperty IndicatorsModel::setProfile
97+ Set the indicator profile to use for the model.
98+
99+ \b Note: methods should only be called after the Component has completed.
100+*/
101+void IndicatorsModel::setProfile(const QString &profile)
102+{
103+ m_manager->setProfile(profile);
104+}
105+
83106/*!
84107 \qmlmethod IndicatorsModel::unload()
85108
86109 Load all indicators.
87110*/
88−void IndicatorsModel::load(const QString& profile)
111+void IndicatorsModel::load()
89112{
90113 m_indicators.clear();
91− m_manager->load(profile);
114+ m_manager->load();
92115}
93116
94117/*!
plugins/Unity/Indicators/indicatorsmodel.h+6−1
@@ -34,17 +34,21 @@ class UNITYINDICATORS_EXPORT IndicatorsModel : public QAbstractListModel
3434 Q_OBJECT
3535 Q_ENUMS(Roles)
3636 Q_PROPERTY(int count READ count NOTIFY countChanged)
37+ Q_PROPERTY(QString profile READ profile WRITE setProfile NOTIFY profileChanged)
3738
3839public:
3940
4041 IndicatorsModel(QObject *parent=0);
4142 ~IndicatorsModel();
4243
43− Q_INVOKABLE void load(const QString& profile);
44+ Q_INVOKABLE void load();
4445 Q_INVOKABLE void unload();
4546
4647 Q_INVOKABLE QVariant data(int row, int role) const;
4748
49+ QString profile() const;
50+ void setProfile(const QString& profile);
51+
4852 /* QAbstractItemModel */
4953 QHash<int, QByteArray> roleNames() const;
5054 int columnCount(const QModelIndex &parent = QModelIndex()) const;
@@ -55,6 +59,7 @@ public:
5559
5660Q_SIGNALS:
5761 void countChanged();
62+ void profileChanged();
5863 void indicatorDataChanged(const QVariant& data);
5964
6065private Q_SLOTS:
plugins/Unity/Indicators/modelactionrootstate.cpp+4−2
@@ -71,7 +71,7 @@ void ModelActionRootState::setMenu(UnityMenuModel* menu)
7171
7272bool ModelActionRootState::valid() const
7373{
74− return m_menu && m_menu->rowCount() > 0;
74+ return !currentState().empty();
7575}
7676
7777void ModelActionRootState::onModelRowsAdded(const QModelIndex& parent, int start, int end)
@@ -121,7 +121,9 @@ void ModelActionRootState::updateActionState()
121121 m_menu->setActionStateParser(oldParser);
122122
123123 setCurrentState(state);
124− } else {
124+ } else if (!m_menu) {
125125 setCurrentState(QVariantMap());
126126 }
127+ // else if m_menu->rowCount() == 0, let's leave existing cache in place
128+ // until the new menu comes in, to avoid flashing the UI empty for a moment
127129}
plugins/Unity/Indicators/unitymenumodelcache.cpp+8−11
@@ -46,17 +46,14 @@ QSharedPointer<UnityMenuModel> UnityMenuModelCache::model(const QByteArray& path
4646 QQmlEngine::setObjectOwnership(model, QQmlEngine::CppOwnership);
4747
4848 QSharedPointer<UnityMenuModel> menuModel(model);
49− connect(model, &QObject::destroyed, this, [this] {
50− QMutableHashIterator<QByteArray, QWeakPointer<UnityMenuModel>> iter(m_registry);
51− while(iter.hasNext()) {
52− auto keyVal = iter.next();
53− if (keyVal.value().isNull()) {
54− iter.remove();
55− break;
56− }
57− }
58− });
59− m_registry[path] = menuModel.toWeakRef();
49+
50+ // Keep a shared pointer (rather than weak pointer which would cause the
51+ // model to be deleted when all shared pointers we give out are deleted).
52+ // We want to keep all models cached because when we switch indicator
53+ // profiles, we will be switching paths often. And we want to keep the
54+ // old model around, ready to be used. Otherwise the UI might momentarily
55+ // wait as we populate the model from DBus yet again.
56+ m_registry[path] = menuModel;
6057
6158 menuModel->setMenuObjectPath(path);
6259 return menuModel;
plugins/Unity/Indicators/unitymenumodelcache.h+2−2
@@ -25,7 +25,7 @@
2525#include <QObject>
2626#include <QHash>
2727#include <QPointer>
28−#include <QWeakPointer>
28+#include <QSharedPointer>
2929
3030class UnityMenuModel;
3131
@@ -43,7 +43,7 @@ public:
4343 Q_INVOKABLE virtual bool contains(const QByteArray& path);
4444
4545protected:
46− QHash<QByteArray, QWeakPointer<UnityMenuModel>> m_registry;
46+ QHash<QByteArray, QSharedPointer<UnityMenuModel>> m_registry;
4747 static QPointer<UnityMenuModelCache> theCache;
4848};
4949
plugins/Unity/Launcher/CMakeLists.txt+2−1
@@ -1,4 +1,4 @@
1−pkg_check_modules(LAUNCHER_API REQUIRED unity-shell-launcher=5)
1+pkg_check_modules(LAUNCHER_API REQUIRED unity-shell-launcher=6)
22pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=5)
33pkg_check_modules(GSETTINGS_QT REQUIRED gsettings-qt)
44
@@ -20,6 +20,7 @@ set(QMLLAUNCHERPLUGIN_SRC
2020 dbusinterface.cpp
2121 gsettings.cpp
2222 desktopfilehandler.cpp
23+ asadapter.cpp
2324 ${CMAKE_SOURCE_DIR}/plugins/AccountsService/AccountsServiceDBusAdaptor.cpp
2425 ${LAUNCHER_API_INCLUDEDIR}/unity/shell/launcher/LauncherItemInterface.h
2526 ${LAUNCHER_API_INCLUDEDIR}/unity/shell/launcher/LauncherModelInterface.h
plugins/Unity/Launcher/asadapter.cppnew file+60
@@ -0,0 +1,60 @@
1+/*
2+ * Copyright 2014-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 "asadapter.h"
18+#include "launcheritem.h"
19+#include "AccountsServiceDBusAdaptor.h"
20+
21+#include <QDebug>
22+
23+ASAdapter::ASAdapter()
24+{
25+ m_accounts = new AccountsServiceDBusAdaptor();
26+ m_user = qgetenv("USER");
27+ if (m_user.isEmpty()) {
28+ qWarning() << "$USER not valid. Account Service integration will not work.";
29+ }
30+}
31+
32+ASAdapter::~ASAdapter()
33+{
34+ m_accounts->deleteLater();
35+}
36+
37+void ASAdapter::syncItems(QList<LauncherItem *> m_list)
38+{
39+ if (m_accounts && !m_user.isEmpty()) {
40+ QList<QVariantMap> items;
41+
42+ Q_FOREACH(LauncherItem *item, m_list) {
43+ items << itemToVariant(item);
44+ }
45+
46+ m_accounts->setUserPropertyAsync(m_user, "com.canonical.unity.AccountsService", "LauncherItems", QVariant::fromValue(items));
47+ }
48+}
49+
50+QVariantMap ASAdapter::itemToVariant(LauncherItem *item) const
51+{
52+ QVariantMap details;
53+ details.insert("id", item->appId());
54+ details.insert("name", item->name());
55+ details.insert("icon", item->icon());
56+ details.insert("count", item->count());
57+ details.insert("countVisible", item->countVisible());
58+ details.insert("pinned", item->pinned());
59+ return details;
60+}
tests/mocks/Unity/Notifications/MockNotificationTypes.h → plugins/Unity/Launcher/asadapter.hrenamed+24−14
@@ -1,5 +1,5 @@
11/*
2− * Copyright 2014 Canonical Ltd.
2+ * Copyright 2014-2015 Canonical Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU Lesser General Public License as published by
@@ -12,25 +12,35 @@
1212 *
1313 * You should have received a copy of the GNU Lesser General Public License
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− *
16− * Authors:
17− * Mirco Mueller <mirco.mueller@canonical.com>
1815 */
1916
20−#ifndef MOCK_NOTIFICATION_TYPES_H
21−#define MOCK_NOTIFICATION_TYPES_H
17+#ifndef ASADAPTER_H
18+#define ASADAPTER_H
2219
23−#include <QObject>
20+#include <QVariantMap>
2421
25−class MockNotification : public QObject {
26− Q_OBJECT
27− Q_ENUMS(Type)
22+class LauncherItem;
23+class AccountsServiceDBusAdaptor;
24+class QDBusInterface;
2825
26+class ASAdapter
27+{
2928public:
30− MockNotification(QObject *parent=nullptr);
31− virtual ~MockNotification();
29+ ASAdapter();
30+ ~ASAdapter();
31+
32+ void syncItems(QList<LauncherItem*> m_list);
33+
34+private:
35+ QVariantMap itemToVariant(LauncherItem *item) const;
36+
37+private:
38+ AccountsServiceDBusAdaptor *m_accounts;
39+ QString m_user;
40+
41+ QDBusInterface *m_userInterface;
3242
33− enum Type { PlaceHolder, Confirmation, Ephemeral, Interactive, SnapDecision };
43+ friend class LauncherModelTest;
3444};
3545
36−#endif // MOCK_NOTIFICATION_TYPES_H
46+#endif
plugins/Unity/Launcher/launchermodel.cpp+20
@@ -22,6 +22,7 @@
2222#include "gsettings.h"
2323#include "desktopfilehandler.h"
2424#include "dbusinterface.h"
25+#include "asadapter.h"
2526
2627#include <unity/shell/application/ApplicationInfoInterface.h>
2728
@@ -34,6 +35,7 @@ LauncherModel::LauncherModel(QObject *parent):
3435 LauncherModelInterface(parent),
3536 m_settings(new GSettings(this)),
3637 m_dbusIface(new DBusInterface(this)),
38+ m_asAdapter(new ASAdapter()),
3739 m_appManager(0)
3840{
3941 connect(m_dbusIface, &DBusInterface::countChanged, this, &LauncherModel::countChanged);
@@ -50,6 +52,8 @@ LauncherModel::~LauncherModel()
5052 while (!m_list.empty()) {
5153 m_list.takeFirst()->deleteLater();
5254 }
55+
56+ delete m_asAdapter;
5357}
5458
5559int LauncherModel::rowCount(const QModelIndex &parent) const
@@ -261,6 +265,15 @@ void LauncherModel::setApplicationManager(unity::shell::application::Application
261265 }
262266}
263267
268+bool LauncherModel::onlyPinned() const
269+{
270+ return false;
271+}
272+
273+void LauncherModel::setOnlyPinned(bool onlyPinned) {
274+ Q_UNUSED(onlyPinned);
275+ qWarning() << "This launcher implementation does not support showing only pinned apps";
276+}
264277
265278void LauncherModel::storeAppList()
266279{
@@ -271,6 +284,7 @@ void LauncherModel::storeAppList()
271284 }
272285 }
273286 m_settings->setStoredApplications(appIds);
287+ m_asAdapter->syncItems(m_list);
274288}
275289
276290void LauncherModel::unpin(const QString &appId)
@@ -321,6 +335,7 @@ void LauncherModel::countChanged(const QString &appId, int count)
321335 LauncherItem *item = m_list.at(idx);
322336 item->setCount(count);
323337 Q_EMIT dataChanged(index(idx), index(idx), QVector<int>() << RoleCount);
338+ m_asAdapter->syncItems(m_list);
324339 }
325340}
326341
@@ -352,6 +367,7 @@ void LauncherModel::countVisibleChanged(const QString &appId, int countVisible)
352367 Q_EMIT hint();
353368 }
354369 }
370+ m_asAdapter->syncItems(m_list);
355371}
356372
357373void LauncherModel::refresh()
@@ -433,6 +449,8 @@ void LauncherModel::refresh()
433449 if (changed) {
434450 Q_EMIT hint();
435451 }
452+
453+ m_asAdapter->syncItems(m_list);
436454}
437455
438456void LauncherModel::applicationAdded(const QModelIndex &parent, int row)
@@ -467,6 +485,7 @@ void LauncherModel::applicationAdded(const QModelIndex &parent, int row)
467485 beginInsertRows(QModelIndex(), m_list.count(), m_list.count());
468486 m_list.append(item);
469487 endInsertRows();
488+ m_asAdapter->syncItems(m_list);
470489 }
471490}
472491
@@ -486,6 +505,7 @@ void LauncherModel::applicationRemoved(const QModelIndex &parent, int row)
486505 beginRemoveRows(QModelIndex(), appIndex, appIndex);
487506 m_list.takeAt(appIndex)->deleteLater();
488507 endRemoveRows();
508+ m_asAdapter->syncItems(m_list);
489509 }
490510}
491511
plugins/Unity/Launcher/launchermodel.h+5
@@ -28,6 +28,7 @@
2828class LauncherItem;
2929class GSettings;
3030class DBusInterface;
31+class ASAdapter;
3132
3233using namespace unity::shell::launcher;
3334using namespace unity::shell::application;
@@ -54,6 +55,9 @@ public:
5455 unity::shell::application::ApplicationManagerInterface* applicationManager() const;
5556 void setApplicationManager(unity::shell::application::ApplicationManagerInterface *appManager);
5657
58+ bool onlyPinned() const override;
59+ void setOnlyPinned(bool onlyPinned) override;
60+
5761 int findApplication(const QString &appId);
5862
5963public Q_SLOTS:
@@ -79,6 +83,7 @@ private:
7983
8084 GSettings *m_settings;
8185 DBusInterface *m_dbusIface;
86+ ASAdapter *m_asAdapter;
8287
8388 ApplicationManagerInterface *m_appManager;
8489
plugins/Utils/CMakeLists.txt+2−1
@@ -15,6 +15,7 @@ set(QMLPLUGIN_SRC
1515 unitymenumodelpaths.cpp
1616 windowkeysfilter.cpp
1717 easingcurve.cpp
18+ windowstatestorage.cpp
1819 plugin.cpp
1920 )
2021
@@ -30,6 +31,6 @@ target_link_libraries(Utils-qml ${GIO_LDFLAGS})
3031# files directly in targets.
3132set_target_properties(Utils-qml PROPERTIES COMPILE_FLAGS -fvisibility=default)
3233
33−qt5_use_modules(Utils-qml Qml Quick DBus Network Gui)
34+qt5_use_modules(Utils-qml Qml Quick DBus Network Gui Sql Concurrent)
3435
3536add_unity8_plugin(Utils 0.1 Utils TARGETS Utils-qml)
plugins/Utils/plugin.cpp+9
@@ -33,6 +33,14 @@
3333#include "unitymenumodelpaths.h"
3434#include "windowkeysfilter.h"
3535#include "easingcurve.h"
36+#include "windowstatestorage.h"
37+
38+static QObject *createWindowStateStorage(QQmlEngine *engine, QJSEngine *scriptEngine)
39+{
40+ Q_UNUSED(engine)
41+ Q_UNUSED(scriptEngine)
42+ return new WindowStateStorage();
43+}
3644
3745void UtilsPlugin::registerTypes(const char *uri)
3846{
@@ -46,6 +54,7 @@ void UtilsPlugin::registerTypes(const char *uri)
4654 qmlRegisterType<GDateTimeFormatter>(uri, 0, 1, "GDateTimeFormatter");
4755 qmlRegisterType<EasingCurve>(uri, 0, 1, "EasingCurve");
4856 qmlRegisterType<RelativeTimeFormatter>(uri, 0, 1, "RelativeTimeFormatter");
57+ qmlRegisterSingletonType<WindowStateStorage>(uri, 0, 1, "WindowStateStorage", createWindowStateStorage);
4958}
5059
5160void UtilsPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
plugins/Utils/windowstatestorage.cppnew file+96
@@ -0,0 +1,96 @@
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 "windowstatestorage.h"
18+
19+#include <QtConcurrent>
20+#include <QDebug>
21+#include <QSqlQuery>
22+#include <QSqlError>
23+#include <QSqlResult>
24+#include <QRect>
25+
26+QMutex WindowStateStorage::s_mutex;
27+
28+WindowStateStorage::WindowStateStorage(QObject *parent):
29+ QObject(parent)
30+{
31+ QString dbPath = QDir::homePath() + "/.cache/unity8/";
32+ m_db = QSqlDatabase::addDatabase("QSQLITE");
33+ QDir dir;
34+ dir.mkpath(dbPath);
35+ m_db.setDatabaseName(dbPath + "windowstatestorage.sqlite");
36+ initdb();
37+}
38+
39+void WindowStateStorage::saveGeometry(const QString &windowId, const QRect &rect)
40+{
41+ QString queryString = QString("INSERT OR REPLACE INTO geometry (windowId, x, y, width, height) values ('%1', '%2', '%3', '%4', '%5');")
42+ .arg(windowId)
43+ .arg(rect.x())
44+ .arg(rect.y())
45+ .arg(rect.width())
46+ .arg(rect.height());
47+
48+ QtConcurrent::run(executeAsyncQuery, queryString);
49+}
50+
51+void WindowStateStorage::executeAsyncQuery(const QString &queryString)
52+{
53+ QMutexLocker l(&s_mutex);
54+ QSqlQuery query;
55+
56+ bool ok = query.exec(queryString);
57+ if (!ok) {
58+ qWarning() << "Error executing query" << queryString
59+ << "Driver error:" << query.lastError().driverText()
60+ << "Database error:" << query.lastError().databaseText();
61+ }
62+}
63+
64+QRect WindowStateStorage::getGeometry(const QString &windowId, const QRect &defaultValue)
65+{
66+ QMutexLocker l(&s_mutex);
67+ QString queryString = QString("SELECT * FROM geometry WHERE windowId = '%1';")
68+ .arg(windowId);
69+ QSqlQuery query;
70+
71+ bool ok = query.exec(queryString);
72+ if (!ok) {
73+ qWarning() << "Error retrieving window state for" << windowId
74+ << "Driver error:" << query.lastError().driverText()
75+ << "Database error:" << query.lastError().databaseText();
76+ return defaultValue;
77+ }
78+ if (!query.first()) {
79+ return defaultValue;
80+ }
81+ return QRect(query.value("x").toInt(), query.value("y").toInt(), query.value("width").toInt(), query.value("height").toInt());
82+}
83+
84+void WindowStateStorage::initdb()
85+{
86+ m_db.open();
87+ if (!m_db.open()) {
88+ qWarning() << "Error opening state database:" << m_db.lastError().driverText() << m_db.lastError().databaseText();
89+ return;
90+ }
91+
92+ if (!m_db.tables().contains("geometry")) {
93+ QSqlQuery query;
94+ query.exec("CREATE TABLE geometry(windowId TEXT UNIQUE, x INTEGER, y INTEGER, width INTEGER, height INTEGER);");
95+ }
96+}
plugins/Utils/windowstatestorage.hnew file+38
@@ -0,0 +1,38 @@
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 <QObject>
18+#include <QSqlDatabase>
19+#include <QMutex>
20+
21+class WindowStateStorage: public QObject
22+{
23+ Q_OBJECT
24+public:
25+ WindowStateStorage(QObject *parent = 0);
26+
27+ Q_INVOKABLE void saveGeometry(const QString &windowId, const QRect &rect);
28+ Q_INVOKABLE QRect getGeometry(const QString &windowId, const QRect &defaultValue);
29+
30+private:
31+ void initdb();
32+
33+ static void executeAsyncQuery(const QString &queryString);
34+ static QMutex s_mutex;
35+
36+ // NB: This is accessed from threads. Make sure to mutex it.
37+ QSqlDatabase m_db;
38+};
po/gd.po+45−40
@@ -8,7 +8,7 @@ msgstr ""
88"Project-Id-Version: unity8\n"
99"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
1010"POT-Creation-Date: 2015-01-22 11:09+0100\n"
11−"PO-Revision-Date: 2014-11-09 08:18+0000\n"
11+"PO-Revision-Date: 2015-02-11 15:01+0000\n"
1212"Last-Translator: GunChleoc <Unknown>\n"
1313"Language-Team: Fòram na Gàidhlig\n"
1414"MIME-Version: 1.0\n"
@@ -16,8 +16,8 @@ msgstr ""
1616"Content-Transfer-Encoding: 8bit\n"
1717"Plural-Forms: nplurals=4; plural=(n==1 || n==11) ? 0 : (n==2 || n==12) ? 1 : "
1818"(n > 2 && n < 20) ? 2 : 3;\n"
19−"X-Launchpad-Export-Date: 2015-01-28 06:30+0000\n"
20−"X-Generator: Launchpad (build 17306)\n"
19+"X-Launchpad-Export-Date: 2015-02-13 06:54+0000\n"
20+"X-Generator: Launchpad (build 17336)\n"
2121"Language: gd\n"
2222
2323#: plugins/LightDM/Greeter.cpp:130
@@ -241,11 +241,11 @@ msgstr "Leig às airson ath-nuadhachadh…"
241241
242242#: qml/Dash/ScopesList.qml:66
243243msgid "Manage"
244−msgstr ""
244+msgstr "Stiùirich"
245245
246246#: qml/Dash/ScopesList.qml:112
247247msgid "Home"
248−msgstr ""
248+msgstr "Dhachaigh"
249249
250250#: qml/Dash/ScopesList.qml:113
251251msgid "Also installed"
@@ -297,7 +297,7 @@ msgstr "Dh'fhàillig leis, thoir gnogag gus feuchainn a-rithist"
297297
298298#: qml/Panel/Indicators/MessageMenuItemFactory.qml:149
299299msgid "Reply"
300−msgstr ""
300+msgstr "Freagair"
301301
302302#: qml/Panel/Indicators/ModemInfoItem.qml:105
303303msgid "Unlock SIM"
@@ -369,58 +369,58 @@ msgstr ""
369369
370370#: qml/Wizard/Page.qml:89
371371msgid "Back"
372−msgstr ""
372+msgstr "Air ais"
373373
374374#: qml/Wizard/Pages/10-welcome.qml:27
375375msgid "Hi!"
376−msgstr ""
376+msgstr "Shin thu!"
377377
378378#: qml/Wizard/Pages/10-welcome.qml:44
379379msgid "Welcome to your Ubuntu phone."
380−msgstr ""
380+msgstr "Fàilte dhan fhòn Ubuntu agad."
381381
382382#: qml/Wizard/Pages/10-welcome.qml:52
383383msgid "Let’s get started."
384−msgstr ""
384+msgstr "Tòisicheamaid."
385385
386386#: qml/Wizard/Pages/10-welcome.qml:88 qml/Wizard/Pages/30-passwd-type.qml:128
387387#: qml/Wizard/Pages/40-wifi.qml:197 qml/Wizard/Pages/50-location.qml:131
388388#: qml/Wizard/Pages/60-reporting.qml:50 qml/Wizard/Pages/passwd-confirm.qml:82
389389#: qml/Wizard/Pages/passwd-set.qml:90
390390msgid "Continue"
391−msgstr ""
391+msgstr "Lean air adhart"
392392
393393#: qml/Wizard/Pages/20-sim.qml:25
394394msgid "Add a SIM card and restart your device"
395−msgstr ""
395+msgstr "Cuir cairt SIM ris 's tòisich an uidheam agad às ùr"
396396
397397#: qml/Wizard/Pages/20-sim.qml:55
398398msgid "Without it, you won’t be able to make calls or use text messaging."
399−msgstr ""
399+msgstr "Chan urrainn dhut gairm no teachdaireachd a chur às a h-aonais."
400400
401401#: qml/Wizard/Pages/20-sim.qml:69 qml/Wizard/Pages/40-wifi.qml:197
402402msgid "Skip"
403−msgstr ""
403+msgstr "Leum thairis air"
404404
405405#: qml/Wizard/Pages/30-passwd-type.qml:39
406406msgid "Lock security"
407−msgstr ""
407+msgstr "Glas tèarainteachd"
408408
409409#: qml/Wizard/Pages/30-passwd-type.qml:74
410410msgid "Please select how you’d like to unlock your phone."
411−msgstr ""
411+msgstr "Tagh an dòigh sa tha thu airson a' ghlas a thoirt far an fhòn agad."
412412
413413#: qml/Wizard/Pages/30-passwd-type.qml:97
414414msgid "Swipe"
415−msgstr ""
415+msgstr "Grad-shlaighd"
416416
417417#: qml/Wizard/Pages/30-passwd-type.qml:98
418418msgid "No security"
419−msgstr ""
419+msgstr "Gun tèarainteachd"
420420
421421#: qml/Wizard/Pages/30-passwd-type.qml:100
422422msgid "Passcode"
423−msgstr ""
423+msgstr "Còd-faire"
424424
425425#: qml/Wizard/Pages/30-passwd-type.qml:101
426426msgid "4 digits only"
@@ -428,31 +428,31 @@ msgstr ""
428428
429429#: qml/Wizard/Pages/30-passwd-type.qml:103
430430msgid "Passphrase"
431−msgstr ""
431+msgstr "Abairt-fhaire"
432432
433433#: qml/Wizard/Pages/30-passwd-type.qml:104
434434msgid "Numbers and letters"
435−msgstr ""
435+msgstr "Àireamhan 's litrichean"
436436
437437#: qml/Wizard/Pages/40-wifi.qml:28
438438msgid "Connect to Wi‑Fi"
439−msgstr ""
439+msgstr "Ceangail ri Wi-Fi"
440440
441441#: qml/Wizard/Pages/40-wifi.qml:147
442442msgid "Available networks…"
443−msgstr ""
443+msgstr "Lìonraidhean ri làimh…"
444444
445445#: qml/Wizard/Pages/40-wifi.qml:148
446446msgid "No available networks."
447−msgstr ""
447+msgstr "Chan eil lìonra ri làimh."
448448
449449#: qml/Wizard/Pages/50-location.qml:27
450450msgid "Location"
451−msgstr ""
451+msgstr "Àite"
452452
453453#: qml/Wizard/Pages/50-location.qml:62
454454msgid "Let the phone detect your location:"
455−msgstr ""
455+msgstr "Faodaidh am fòn aithneachadh far a bheil thu:"
456456
457457#: qml/Wizard/Pages/50-location.qml:69
458458msgid "Using GPS only (less accurate)"
@@ -471,7 +471,7 @@ msgstr ""
471471
472472#: qml/Wizard/Pages/50-location.qml:112
473473msgid "Not at all"
474−msgstr ""
474+msgstr "Chan fhaod idir"
475475
476476#: qml/Wizard/Pages/50-location.qml:124
477477msgid "You can change your mind later in <b>System Settings</b>."
@@ -479,70 +479,75 @@ msgstr ""
479479
480480#: qml/Wizard/Pages/60-reporting.qml:24
481481msgid "Improving your experience"
482−msgstr ""
482+msgstr "A' cur piseach air a' ghnàth-eòlas agad"
483483
484484#: qml/Wizard/Pages/60-reporting.qml:36
485485msgid ""
486486"Your phone is set up to automatically report errors to Canonical and its "
487487"partners, the makers of the operating system."
488488msgstr ""
489+"Chaidh am fòn agad a shuidheachadh gus innse mu mhearachdan gu fèin-"
490+"obrachail do Chanonical 's a chom-pàirtichean a rinn an siostam-obrachaidh "
491+"seo."
489492
490493#: qml/Wizard/Pages/60-reporting.qml:43
491494msgid ""
492495"This can be disabled in <b>System Settings</b> under <b>Security &amp; "
493496"Privacy</b>"
494497msgstr ""
498+"Gabhaidh seo cur à comas ann an <b>Roghainnean an t-siostaim</b> fo "
499+"<b>Thèarainteachd ⁊ prìobhaideachd</b>"
495500
496501#: qml/Wizard/Pages/80-finished.qml:24
497502msgid "All done"
498−msgstr ""
503+msgstr "Coileanta"
499504
500505#: qml/Wizard/Pages/80-finished.qml:39
501506msgid "Nice work!"
502−msgstr ""
507+msgstr "Shin thu!"
503508
504509#: qml/Wizard/Pages/80-finished.qml:46
505510msgid "Your phone is now ready to use."
506−msgstr ""
511+msgstr "Gabhaidh am fòn agad cleachdadh a-nis."
507512
508513#: qml/Wizard/Pages/80-finished.qml:53
509514msgid "Finish"
510−msgstr ""
515+msgstr "Crìochnaich"
511516
512517#: qml/Wizard/Pages/here-terms.qml:27
513518msgid "Terms & Conditions"
514−msgstr ""
519+msgstr "Teirmichean ⁊ cumhaichean"
515520
516521#: qml/Wizard/Pages/passwd-confirm.qml:49
517522msgid "Confirm passphrase"
518−msgstr ""
523+msgstr "Dearbhaich an abairt-fhaire"
519524
520525#: qml/Wizard/Pages/passwd-confirm.qml:50
521526msgid "Confirm passcode"
522−msgstr ""
527+msgstr "Dearbhaich an còd-faire"
523528
524529#: qml/Wizard/Pages/passwd-confirm.qml:53
525530#: qml/Wizard/Pages/passwd-confirm.qml:54
526531msgid "Please try again."
527−msgstr ""
532+msgstr "Feuch ris a-rithist."
528533
529534#: qml/Wizard/Pages/passwd-set.qml:61
530535msgid "Choose your passcode"
531−msgstr ""
536+msgstr "Tagh an còd-faire agad"
532537
533538#: qml/Wizard/Pages/passwd-set.qml:68
534539msgid "Passphrase must be 4 characters long"
535−msgstr ""
540+msgstr "Feumaidh an abairt-fhaire a bhith 4 caractaran a dh'fhaid"
536541
537542#: qml/Wizard/StackButton.qml:39
538543#, qt-format
539544msgid "〈 %1"
540−msgstr ""
545+msgstr "〈 %1"
541546
542547#: qml/Wizard/StackButton.qml:42
543548#, qt-format
544549msgid "%1 〉"
545−msgstr ""
550+msgstr "%1 〉"
546551
547552#~ msgid "Confirm"
548553#~ msgstr "Dearbhaich"
po/gl.po+7−7
@@ -8,15 +8,15 @@ msgstr ""
88"Project-Id-Version: unity\n"
99"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
1010"POT-Creation-Date: 2015-01-22 11:09+0100\n"
11−"PO-Revision-Date: 2015-01-28 10:25+0000\n"
11+"PO-Revision-Date: 2015-02-11 10:36+0000\n"
1212"Last-Translator: Marcos Lans <Unknown>\n"
1313"Language-Team: Galician <gl@li.org>\n"
1414"MIME-Version: 1.0\n"
1515"Content-Type: text/plain; charset=UTF-8\n"
1616"Content-Transfer-Encoding: 8bit\n"
1717"Plural-Forms: nplurals=2; plural=n != 1;\n"
18−"X-Launchpad-Export-Date: 2015-01-29 06:00+0000\n"
19−"X-Generator: Launchpad (build 17306)\n"
18+"X-Launchpad-Export-Date: 2015-02-13 06:54+0000\n"
19+"X-Generator: Launchpad (build 17336)\n"
2020
2121#: plugins/LightDM/Greeter.cpp:130
2222msgid "Password: "
@@ -56,7 +56,7 @@ msgstr "Saír da sesión"
5656
5757#: qml/Components/Dialogs.qml:87
5858msgid "Are you sure you want to log out?"
59−msgstr "Está seguro de querer pechar a sesión?"
59+msgstr "Confirma que desexa pechar a sesión?"
6060
6161#: qml/Components/Dialogs.qml:89 qml/Components/Dialogs.qml:111
6262#: qml/Components/Dialogs.qml:134
@@ -74,7 +74,7 @@ msgstr "Apagar"
7474
7575#: qml/Components/Dialogs.qml:109
7676msgid "Are you sure you want to shut down?"
77−msgstr "Está seguro de querer apagar?"
77+msgstr "Confirma que desexa apagar?"
7878
7979#: qml/Components/Dialogs.qml:131
8080msgid "Reboot"
@@ -82,7 +82,7 @@ msgstr "Reiniciar"
8282
8383#: qml/Components/Dialogs.qml:132
8484msgid "Are you sure you want to reboot?"
85−msgstr "Está seguro de querer reiniciar?"
85+msgstr "Confirma que desexa reiniciar?"
8686
8787#: qml/Components/Dialogs.qml:154
8888msgid "Power"
@@ -92,7 +92,7 @@ msgstr "Enerxía"
9292msgid ""
9393"Are you sure you would like\n"
9494"to power off?"
95−msgstr "Está seguro de querer apagar?"
95+msgstr "Confirma que desexa apagar?"
9696
9797#: qml/Components/Dialogs.qml:157
9898msgid "Power off"
po/nl.po+4−4
@@ -8,15 +8,15 @@ msgstr ""
88"Project-Id-Version: unity\n"
99"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
1010"POT-Creation-Date: 2015-01-22 11:09+0100\n"
11−"PO-Revision-Date: 2015-01-17 11:13+0000\n"
11+"PO-Revision-Date: 2015-02-12 20:12+0000\n"
1212"Last-Translator: rob <linuxned@gmail.com>\n"
1313"Language-Team: Dutch <nl@li.org>\n"
1414"MIME-Version: 1.0\n"
1515"Content-Type: text/plain; charset=UTF-8\n"
1616"Content-Transfer-Encoding: 8bit\n"
1717"Plural-Forms: nplurals=2; plural=n != 1;\n"
18−"X-Launchpad-Export-Date: 2015-01-28 06:30+0000\n"
19−"X-Generator: Launchpad (build 17306)\n"
18+"X-Launchpad-Export-Date: 2015-02-13 06:54+0000\n"
19+"X-Generator: Launchpad (build 17336)\n"
2020
2121#: plugins/LightDM/Greeter.cpp:130
2222msgid "Password: "
@@ -287,7 +287,7 @@ msgstr "Mislukt, tik om het opnieuw te proberen"
287287
288288#: qml/Panel/Indicators/MessageMenuItemFactory.qml:149
289289msgid "Reply"
290−msgstr ""
290+msgstr "Beantwoorden"
291291
292292#: qml/Panel/Indicators/ModemInfoItem.qml:105
293293msgid "Unlock SIM"
qml/CMakeLists.txt+3−2
@@ -9,10 +9,11 @@ set(QML_DIRS
99 Dash
1010 graphics
1111 Greeter
12− Panel
1312 Launcher
14− Stages
1513 Notifications
14+ Panel
15+ Stages
16+ Tutorial
1617 Wizard
1718 )
1819
qml/Components/EdgeDemo.qmldeleted−254
@@ -1,254 +0,0 @@
1−/*
2− * Copyright (C) 2013 Canonical, Ltd.
3− *
4− * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU General Public License as published by
6− * the Free Software Foundation; version 3.
7− *
8− * This program is distributed in the hope that it will be useful,
9− * but WITHOUT ANY WARRANTY; without even the implied warranty of
10− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU General Public License for more details.
12− *
13− * You should have received a copy of the GNU General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− */
16−
17−import AccountsService 0.1
18−import LightDM 0.1 as LightDM
19−import QtQuick 2.0
20−import Ubuntu.Components 1.1
21−
22−Item {
23− id: demo
24−
25− property Item greeter
26− property Item launcher
27− property Item panel
28− property Item stages
29−
30− property bool launcherEnabled: true
31− property bool stagesEnabled: true
32− property bool panelEnabled: true
33− property bool panelContentEnabled: true
34− property bool running: !launcherEnabled || !stagesEnabled || !panelEnabled || !panelContentEnabled
35−
36− property bool paused: false
37−
38− onPausedChanged: {
39− if (d.rightEdgeDemo) d.rightEdgeDemo.paused = paused
40− if (d.topEdgeDemo) d.topEdgeDemo.paused = paused
41− if (d.bottomEdgeDemo) d.bottomEdgeDemo.paused = paused
42− if (d.leftEdgeDemo) d.leftEdgeDemo.paused = paused
43− if (d.finalEdgeDemo) d.finalEdgeDemo.paused = paused
44− }
45−
46− function hideEdgeDemoInShell() {
47− AccountsService.demoEdges = false;
48− stopDemo();
49− }
50−
51− function hideEdgeDemoInGreeter() {
52− // TODO: AccountsService.demoEdges = false as lightdm user
53− }
54−
55− function hideEdgeDemos() {
56− hideEdgeDemoInGreeter();
57− hideEdgeDemoInShell();
58− }
59−
60− function stopDemo() {
61− launcherEnabled = true
62− stagesEnabled = true
63− panelEnabled = true
64− panelContentEnabled = true
65−
66− // Use a tiny delay for these destroy() calls because if a lot is
67− // happening at once (like creating and being destroyed in same event
68− // loop, as might happen when answering a call while demo is open),
69− // the destroy() call will be ignored.
70− if (d.rightEdgeDemo) d.rightEdgeDemo.destroy(1);
71− if (d.topEdgeDemo) d.topEdgeDemo.destroy(1);
72− if (d.bottomEdgeDemo) d.bottomEdgeDemo.destroy(1);
73− if (d.leftEdgeDemo) d.leftEdgeDemo.destroy(1);
74− if (d.finalEdgeDemo) d.finalEdgeDemo.destroy(1);
75− }
76−
77− function startDemo() {
78− if (!d.overlay) {
79− d.overlay = Qt.createComponent("EdgeDemoOverlay.qml")
80− }
81−
82− launcherEnabled = false;
83− stagesEnabled = false;
84− panelEnabled = false;
85− panelContentEnabled = false;
86−
87− // Begin with either greeter or top, depending on which is visible
88− if (greeter && greeter.shown) {
89− startRightEdgeDemo()
90− } else {
91− startTopEdgeDemo()
92− }
93− }
94−
95− QtObject {
96− id: d
97− property Component overlay
98− property QtObject rightEdgeDemo
99− property QtObject topEdgeDemo
100− property QtObject bottomEdgeDemo
101− property QtObject leftEdgeDemo
102− property QtObject finalEdgeDemo
103− property bool showEdgeDemo: AccountsService.demoEdges
104− property bool showEdgeDemoInGreeter: AccountsService.demoEdges // TODO: AccountsService.demoEdges as lightdm user
105−
106− function restartDemo() {
107− stopDemo()
108− if (d.showEdgeDemo) {
109− startDemo()
110− }
111− }
112−
113− onShowEdgeDemoChanged: restartDemo()
114− }
115−
116− Connections {
117− target: i18n
118− onLanguageChanged: d.restartDemo()
119− }
120−
121− function startRightEdgeDemo() {
122− if (demo.greeter) {
123− d.rightEdgeDemo = d.overlay.createObject(demo.greeter, {
124− "edge": "right",
125− "title": i18n.tr("Right edge"),
126− "text": i18n.tr("Try swiping from the right edge to unlock the phone"),
127− "anchors.fill": demo.greeter,
128− });
129− }
130− if (d.rightEdgeDemo) {
131− d.rightEdgeDemo.onSkip.connect(demo.hideEdgeDemos)
132− } else {
133− stopDemo();
134− }
135− }
136−
137− Connections {
138− target: demo.greeter
139−
140− function hide() {
141− if (d.rightEdgeDemo && d.rightEdgeDemo.available) {
142− d.rightEdgeDemo.hide()
143− hideEdgeDemoInGreeter()
144− startTopEdgeDemo()
145− }
146− }
147−
148− onUnlocked: hide()
149− onShownChanged: if (!greeter.shown) hide()
150− }
151−
152− function startTopEdgeDemo() {
153− demo.panelEnabled = true;
154− if (demo.stages) {
155− d.topEdgeDemo = d.overlay.createObject(demo.panel, {
156− "edge": "top",
157− "title": i18n.tr("Top edge"),
158− "text": i18n.tr("Try swiping from the top edge to access the indicators"),
159− "anchors.fill": demo.panel,
160− });
161− }
162− if (d.topEdgeDemo) {
163− d.topEdgeDemo.onSkip.connect(demo.hideEdgeDemoInShell)
164− } else {
165− stopDemo();
166− }
167− }
168−
169− Connections {
170− target: demo.panel.indicators
171− onFullyOpenedChanged: {
172− if (d.topEdgeDemo && d.topEdgeDemo.available && demo.panel.indicators.fullyOpened) {
173− d.topEdgeDemo.hideNow()
174− startBottomEdgeDemo()
175− }
176− }
177− }
178−
179− function startBottomEdgeDemo() {
180− if (demo.panel.indicators) {
181− d.bottomEdgeDemo = d.overlay.createObject(demo.panel.indicators, {
182− "edge": "bottom",
183− "title": i18n.tr("Close"),
184− "text": i18n.tr("Swipe up again to close the settings screen"),
185− "anchors.fill": demo.panel.indicators,
186− });
187− }
188− if (d.bottomEdgeDemo) {
189− d.bottomEdgeDemo.onSkip.connect(demo.hideEdgeDemoInShell)
190− } else {
191− stopDemo();
192− }
193− }
194−
195− Connections {
196− target: demo.panel.indicators
197− onPartiallyOpenedChanged: {
198− if (d.bottomEdgeDemo &&
199− d.bottomEdgeDemo.available &&
200− !demo.panel.indicators.partiallyOpened &&
201− !demo.panel.indicators.fullyOpened) {
202− d.bottomEdgeDemo.hideNow()
203− startLeftEdgeDemo()
204− }
205− }
206− }
207−
208− function startLeftEdgeDemo() {
209− demo.panelEnabled = false;
210− demo.launcherEnabled = true;
211− if (demo.stages) {
212− d.leftEdgeDemo = d.overlay.createObject(demo.stages, {
213− "edge": "left",
214− "title": i18n.tr("Left edge"),
215− "text": i18n.tr("Swipe from the left to reveal the launcher for quick access to apps"),
216− "anchors.fill": demo.stages,
217− });
218− }
219− if (d.leftEdgeDemo) {
220− d.leftEdgeDemo.onSkip.connect(demo.hideEdgeDemoInShell)
221− } else {
222− stopDemo();
223− }
224− }
225−
226− Connections {
227− target: demo.launcher
228− onStateChanged: {
229− if (d.leftEdgeDemo && d.leftEdgeDemo.available && launcher.state == "visible") {
230− d.leftEdgeDemo.hide()
231− launcher.hide()
232− startFinalEdgeDemo()
233− }
234− }
235− }
236−
237− function startFinalEdgeDemo() {
238− demo.launcherEnabled = false;
239− if (demo.stages) {
240− d.finalEdgeDemo = d.overlay.createObject(demo.stages, {
241− "edge": "none",
242− "title": i18n.tr("Well done"),
243− "text": i18n.tr("You have now mastered the edge gestures and can start using the phone<br><br>Tap on the screen to start"),
244− "anchors.fill": demo.stages,
245− "showSkip": false,
246− });
247− }
248− if (d.finalEdgeDemo) {
249− d.finalEdgeDemo.onSkip.connect(demo.hideEdgeDemoInShell)
250− } else {
251− stopDemo();
252− }
253− }
254−}
qml/Components/EdgeDemoOverlay.qmldeleted−273
@@ -1,273 +0,0 @@
1−/*
2− * Copyright (C) 2013 Canonical, Ltd.
3− *
4− * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU General Public License as published by
6− * the Free Software Foundation; version 3.
7− *
8− * This program is distributed in the hope that it will be useful,
9− * but WITHOUT ANY WARRANTY; without even the implied warranty of
10− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU General Public License for more details.
12− *
13− * You should have received a copy of the GNU General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− */
16−
17−import Powerd 0.1
18−import QtQuick 2.0
19−import QtGraphicalEffects 1.0
20−import Unity.Application 0.1
21−import Ubuntu.Components 0.1
22−
23−Showable {
24− id: overlay
25−
26− /*
27− * Valid values are "left", "right", "top", "bottom", or "none".
28− */
29− property string edge: "top"
30−
31− /*
32− * This is the header displayed, like "Right edge".
33− */
34− property alias title: titleLabel.text
35−
36− /*
37− * This is the block of text displayed below the header.
38− */
39− property alias text: textLabel.text
40−
41− /*
42− * This is the text for the skip button.
43− */
44− property alias skipText: skipLabel.text
45−
46− /*
47− * This is the visible status of the skip button.
48− */
49− property alias showSkip: skipLabel.visible
50−
51− /*
52− * Whether this demo is running currently.
53− */
54− readonly property bool active: available && visible
55−
56− /*
57− * Whether animations are paused.
58− */
59− property alias paused: wholeAnimation.paused
60−
61− /*
62− * Whether animations are running.
63− */
64− readonly property alias running: wholeAnimation.running
65−
66− signal skip()
67−
68− function doSkip() {
69− d.skipOnHide = true;
70− hide();
71− }
72−
73− function hideNow() {
74− overlay.visible = false;
75− overlay.available = false;
76− if (d.skipOnHide) {
77− overlay.skip();
78− }
79− }
80−
81− showAnimation: StandardAnimation {
82− property: "opacity"
83− to: 1
84− onRunningChanged: if (running) overlay.visible = true
85− }
86− hideAnimation: StandardAnimation {
87− property: "opacity"
88− to: 0
89− duration: UbuntuAnimation.BriskDuration
90− onRunningChanged: if (!running) overlay.hideNow()
91− }
92−
93− QtObject {
94− id: d
95− property bool skipOnHide: false
96− property int edgeMargin: units.gu(4)
97− }
98−
99− Rectangle {
100− objectName: "backgroundShade"
101−
102− anchors.fill: parent
103− color: "black"
104− opacity: 0.8
105− visible: overlay.active
106−
107− MouseArea {
108− objectName: "backgroundShadeMouseArea"
109−
110− anchors.fill: parent
111− enabled: overlay.edge == "none" && overlay.opacity == 1.0
112− onClicked: overlay.doSkip()
113− }
114− }
115−
116− Item {
117− id: hintGroup
118− x: 0
119− y: 0
120− width: parent.width
121− height: parent.height
122− visible: overlay.active
123−
124− Column {
125− id: labelGroup
126− spacing: units.gu(3)
127−
128− anchors {
129− margins: d.edgeMargin
130− left: parent.left
131− top: overlay.edge == "bottom" ? undefined : parent.top
132− bottom: overlay.edge == "bottom" ? parent.bottom : undefined
133− }
134−
135− Label {
136− id: titleLabel
137− fontSize: "x-large"
138− width: units.gu(25)
139− wrapMode: Text.WordWrap
140− }
141−
142− Label {
143− id: textLabel
144− width: units.gu(25)
145− wrapMode: Text.WordWrap
146− }
147−
148− Label {
149− id: skipLabel
150− objectName: "skipLabel"
151− text: i18n.tr("Skip intro")
152− color: UbuntuColors.orange
153− fontSize: "small"
154−
155− Icon {
156− anchors.left: parent.right
157− anchors.verticalCenter: parent.verticalCenter
158− height: units.dp(12)
159− width: units.dp(12)
160− name: "chevron"
161− color: UbuntuColors.orange
162− }
163−
164− MouseArea {
165− // Make clickable area bigger than just the link because
166− // otherwise, the edge demo will feel hard to dismiss.
167− anchors.fill: parent
168− anchors.margins: -units.gu(5)
169− onClicked: overlay.doSkip()
170− }
171− }
172− }
173−
174− LinearGradient {
175− id: edgeHint
176− property int size: 1
177− cached: false
178− visible: overlay.edge != "none"
179− gradient: Gradient {
180− GradientStop {
181− position: 0.0
182− color: Qt.hsla(16.0/360.0, 0.83, 0.47, 0.4) // UbuntuColors.orange, but transparent
183− }
184− GradientStop {
185− position: 1.0
186− color: "transparent"
187− }
188− }
189− anchors.fill: parent
190− start: {
191− if (overlay.edge == "right") {
192− return Qt.point(width, 0);
193− } else if (overlay.edge == "left") {
194− return Qt.point(0, 0);
195− } else if (overlay.edge == "top") {
196− return Qt.point(0, 0);
197− } else {
198− return Qt.point(0, height);
199− }
200− }
201− end: {
202− if (overlay.edge == "right") {
203− return Qt.point(width - size, 0);
204− } else if (overlay.edge == "left") {
205− return Qt.point(size, 0);
206− } else if (overlay.edge == "top") {
207− return Qt.point(0, size);
208− } else {
209− return Qt.point(0, height - size);
210− }
211− }
212− }
213− }
214−
215− SequentialAnimation {
216− id: wholeAnimation
217− objectName: "wholeAnimation"
218− running: overlay.active
219−
220− ParallelAnimation {
221− id: fadeInAnimation
222−
223− StandardAnimation {
224− target: labelGroup
225− property: {
226− if (overlay.edge == "right" || overlay.edge == "left") {
227− return "anchors.leftMargin";
228− } else if (overlay.edge == "bottom") {
229− return "anchors.bottomMargin";
230− } else {
231− return "anchors.topMargin";
232− }
233− }
234− from: {
235− if (overlay.edge == "right") {
236− return d.edgeMargin + units.gu(3)
237− } else {
238− return d.edgeMargin - units.gu(3)
239− }
240− }
241− to: d.edgeMargin
242− duration: overlay.edge == "none" ? 0 : UbuntuAnimation.SleepyDuration
243− }
244− StandardAnimation {
245− target: overlay
246− property: "opacity"
247− from: 0.0
248− to: 1.0
249− duration: UbuntuAnimation.SleepyDuration
250− }
251− }
252−
253− SequentialAnimation {
254− id: hintAnimation
255− loops: Animation.Infinite
256− property string prop: (overlay.edge == "left" || overlay.edge == "right") ? "x" : "y"
257− property double endVal: units.dp(5) * ((overlay.edge == "left" || overlay.edge == "top") ? 1 : -1)
258− property double maxGlow: units.dp(20)
259− property int duration: overlay.edge == "none" ? 0 : UbuntuAnimation.SleepyDuration
260−
261− ParallelAnimation {
262− StandardAnimation { target: hintGroup; property: hintAnimation.prop; from: 0; to: hintAnimation.endVal; duration: hintAnimation.duration }
263− StandardAnimation { target: edgeHint; property: "size"; from: 1; to: hintAnimation.maxGlow; duration: hintAnimation.duration }
264− }
265−
266− // Undo the above
267− ParallelAnimation {
268− StandardAnimation { target: hintGroup; property: hintAnimation.prop; from: hintAnimation.endVal; to: 0; duration: hintAnimation.duration }
269− StandardAnimation { target: edgeHint; property: "size"; from: hintAnimation.maxGlow; to: 1; duration: hintAnimation.duration }
270− }
271− }
272− }
273−}
qml/Dash/GenericScopeView.qml+1
@@ -608,6 +608,7 @@ FocusScope {
608608 onBackClicked: scopeView.backClicked()
609609 onSettingsClicked: subPageLoader.openSubPage("settings")
610610 onFavoriteClicked: scopeView.scope.favorite = !scopeView.scope.favorite
611+ onSearchTextFieldFocused: scopeView.showHeader()
611612 }
612613 }
613614 }
qml/Dash/PageHeader.qml+2
@@ -51,6 +51,7 @@ Item {
5151 signal storeClicked()
5252 signal settingsClicked()
5353 signal favoriteClicked()
54+ signal searchTextFieldFocused()
5455
5556 onScopeStyleChanged: refreshLogo()
5657 onSearchQueryChanged: {
@@ -228,6 +229,7 @@ Item {
228229
229230 onActiveFocusChanged: {
230231 if (activeFocus) {
232+ root.searchTextFieldFocused();
231233 root.openSearchHistory();
232234 }
233235 }
qml/Greeter/GreeterContent.qml+1
@@ -121,6 +121,7 @@ Item {
121121 objectName: "infographics"
122122 height: narrowMode ? parent.height : 0.75 * parent.height
123123 model: greeterContentLoader.infographicModel
124+ clip: true // clip large data bubbles
124125
125126 property string selectedUser
126127 property string infographicUser: AccountsService.statsWelcomeScreen ? selectedUser : ""
qml/Launcher/Launcher.qml+13−7
@@ -26,6 +26,7 @@ Item {
2626 property bool autohideEnabled: false
2727 property bool available: true // can be used to disable all interactions
2828 property alias inverted: panel.inverted
29+ property bool shadeBackground: true // can be used to disable background shade when launcher is visible
2930
3031 property int panelWidth: units.gu(8)
3132 property int dragAreaWidth: units.gu(1)
@@ -33,6 +34,10 @@ Item {
3334 property real progress: dragArea.dragging && dragArea.touchX > panelWidth ?
3435 (width * (dragArea.touchX-panelWidth) / (width - panelWidth)) : 0
3536
37+ readonly property bool dragging: dragArea.dragging
38+ readonly property real dragDistance: dragArea.dragging ? dragArea.touchX : 0
39+ readonly property real visibleWidth: panel.width + panel.x
40+
3641 readonly property bool shown: panel.x > -panel.width
3742
3843 // emitted when an application is selected
@@ -154,7 +159,7 @@ Item {
154159
155160 MouseArea {
156161 id: launcherDragArea
157− enabled: root.state == "visible"
162+ enabled: root.available && root.state == "visible"
158163 anchors.fill: panel
159164 anchors.rightMargin: -units.gu(2)
160165 drag {
@@ -180,7 +185,7 @@ Item {
180185 right: parent.right
181186 bottom: parent.bottom
182187 }
183− enabled: root.state == "visible"
188+ enabled: root.shadeBackground && root.state == "visible"
184189 onPressed: {
185190 root.state = ""
186191 }
@@ -190,7 +195,7 @@ Item {
190195 id: backgroundShade
191196 anchors.fill: parent
192197 color: "black"
193− opacity: root.state == "visible" ? 0.6 : 0
198+ opacity: root.shadeBackground && root.state == "visible" ? 0.6 : 0
194199
195200 Behavior on opacity { NumberAnimation { duration: UbuntuAnimation.BriskDuration } }
196201 }
@@ -198,14 +203,14 @@ Item {
198203 LauncherPanel {
199204 id: panel
200205 objectName: "launcherPanel"
201− enabled: root.available
206+ enabled: root.available && root.state == "visible"
202207 width: root.panelWidth
203208 anchors {
204209 top: parent.top
205210 bottom: parent.bottom
206211 }
207212 x: -width
208− visible: x > -width || dragArea.status === DirectionalDragArea.Undecided
213+ visible: root.x > 0 || x > -width || dragArea.status === DirectionalDragArea.Undecided
209214 model: LauncherModel
210215
211216 property bool animate: true
@@ -247,6 +252,7 @@ Item {
247252 direction: Direction.Rightwards
248253
249254 enabled: root.available
255+ x: -root.x // so if launcher is adjusted relative to screen, we stay put (like tutorial does when teasing)
250256 width: root.dragAreaWidth
251257 height: root.height
252258
@@ -260,7 +266,7 @@ Item {
260266 // would appear right next to the user's finger out of nowhere.
261267 // Instead, we make the panel go towards the user's finger in several
262268 // steps. ie., in an animated way.
263− var targetPanelX = Math.min(0, touchX - panel.width)
269+ var targetPanelX = Math.min(0, touchX - panel.width) - root.x
264270 var delta = targetPanelX - panel.x
265271 // the trick is not to go all the way (1.0) as it would cause a sudden jump
266272 panel.x += 0.4 * delta
@@ -292,7 +298,7 @@ Item {
292298 name: "visible"
293299 PropertyChanges {
294300 target: panel
295− x: 0
301+ x: -root.x // so we never go past panelWidth, even when teased by tutorial
296302 }
297303 },
298304 State {
qml/Launcher/LauncherPanel.qml+7−5
@@ -149,14 +149,14 @@ Rectangle {
149149 id: snapToBottomAnimation
150150 target: launcherListView
151151 property: "contentY"
152− to: launcherListView.originY
152+ to: launcherListView.originY + launcherListView.topMargin
153153 }
154154
155155 UbuntuNumberAnimation {
156156 id: snapToTopAnimation
157157 target: launcherListView
158158 property: "contentY"
159− to: launcherListView.contentHeight - launcherListView.height + launcherListView.originY
159+ to: launcherListView.contentHeight - launcherListView.height + launcherListView.originY - launcherListView.topMargin
160160 }
161161
162162 displaced: Transition {
@@ -521,12 +521,12 @@ Rectangle {
521521 left: parent.left
522522 leftMargin: (quickList.item.width - units.gu(1)) / 2 - width / 2
523523 verticalCenter: parent.verticalCenter
524− verticalCenterOffset: (parent.height / 2 + units.dp(3)) * (quickList.offset > 0 ? 1 : -1)
524+ verticalCenterOffset: (parent.height / 2 + units.dp(3)) * (quickList.offset > 0 ? 1 : -1) * (root.inverted ? 1 : -1)
525525 }
526526 height: units.gu(1)
527527 width: units.gu(2)
528528 source: "graphics/quicklist_tooltip.png"
529− rotation: quickList.offset > 0 ? 0 : 180
529+ rotation: (quickList.offset > 0 ? 0 : 180) + (root.inverted ? 0 : 180)
530530 }
531531
532532 InverseMouseArea {
@@ -543,7 +543,9 @@ Rectangle {
543543 id: quickList
544544 objectName: "quickList"
545545 color: "#f5f5f5"
546− width: units.gu(30)
546+ // Because we're setting left/right anchors depending on orientation, it will break the
547+ // width setting after rotating twice. This makes sure we also re-apply width on rotation
548+ width: root.inverted ? units.gu(30) : units.gu(30)
547549 height: quickListColumn.height
548550 visible: quickListShape.visible
549551 anchors {
qml/Notifications/Notification.qml+36−6
@@ -40,6 +40,7 @@ Item {
4040 property bool fullscreen: false
4141 property int maxHeight
4242 property int margins
43+ readonly property bool draggable: (type === Notification.SnapDecision && state === "contracted") || type === Notification.Interactive || type === Notification.Ephemeral
4344 readonly property bool darkOnBright: panel.indicators.shown || type === Notification.SnapDecision
4445 readonly property color red: "#fc4949"
4546 readonly property color green: "#3fb24f"
@@ -52,7 +53,7 @@ Item {
5253 implicitHeight: type !== Notification.PlaceHolder ? (fullscreen ? maxHeight : outterColumn.height - shapedBack.anchors.topMargin + contentSpacing * 2) : 0
5354
5455 color: (type === Notification.Confirmation && notificationList.useModal && !greeter.shown) || darkOnBright ? sdLightGrey : Qt.rgba(0.132, 0.117, 0.109, 0.97)
55− opacity: 1 // FIXME: 1 because of LP: #1354406 workaround, has to be 0 really
56+ opacity: 1 - (x / notification.width) // FIXME: non-zero initially because of LP: #1354406 workaround, we want this to start at 0 upon creation eventually
5657
5758 state: {
5859 var result = "";
@@ -80,18 +81,28 @@ Item {
8081 id: sound
8182 objectName: "sound"
8283 audioRole: MediaPlayer.alert
83− source: hints["suppress-sound"] != "true" && hints["sound-file"] != undefined ? hints["sound-file"] : ""
84+ source: hints["suppress-sound"] !== "true" && hints["sound-file"] !== undefined ? hints["sound-file"] : ""
8485 }
8586
8687 // FIXME: using onCompleted because of LP: #1354406 workaround, has to be onOpacityChanged really
8788 Component.onCompleted: {
88− if (opacity == 1.0 && hints["suppress-sound"] != "true" && sound.source != "") {
89+ if (opacity == 1.0 && hints["suppress-sound"] !== "true" && sound.source !== "") {
8990 sound.play();
9091 }
9192 }
9293
94+ Behavior on x {
95+ id: normalXBehavior
96+
97+ enabled: draggable
98+ UbuntuNumberAnimation {
99+ duration: UbuntuAnimation.FastDuration
100+ easing.type: Easing.OutBounce
101+ }
102+ }
103+
93104 onHintsChanged: {
94− if (type === Notification.Confirmation && opacity == 1.0 && hints["suppress-sound"] != "true" && sound.source != "") {
105+ if (type === Notification.Confirmation && opacity == 1.0 && hints["suppress-sound"] !== "true" && sound.source !== "") {
95106 sound.play();
96107 }
97108 }
@@ -146,6 +157,12 @@ Item {
146157 opacity: parent.opacity
147158 }
148159
160+ onXChanged: {
161+ if (draggable && notification.x > 0.75 * notification.width) {
162+ notification.notification.close()
163+ }
164+ }
165+
149166 Item {
150167 id: contents
151168 anchors.fill: fullscreen ? nonShapedBack : shapedBack
@@ -169,7 +186,7 @@ Item {
169186 actions: paths.actions
170187 menuObjectPath: paths.menuObjectPath
171188 onNameOwnerChanged: {
172− if (lastNameOwner != "" && nameOwner == "" && notification.notification != undefined) {
189+ if (lastNameOwner !== "" && nameOwner === "" && notification.notification !== undefined) {
173190 notification.notification.close()
174191 }
175192 lastNameOwner = nameOwner
@@ -181,6 +198,12 @@ Item {
181198
182199 anchors.fill: parent
183200 objectName: "interactiveArea"
201+
202+ drag.target: draggable ? notification : undefined
203+ drag.axis: Drag.XAxis
204+ drag.minimumX: 0
205+ drag.maximumX: notification.width
206+
184207 onClicked: {
185208 if (notification.type == Notification.Interactive) {
186209 notification.notification.invokeAction(actionRepeater.itemAt(0).actionId)
@@ -188,6 +211,13 @@ Item {
188211 notificationList.currentIndex = index;
189212 }
190213 }
214+ onReleased: {
215+ if (notification.x < notification.width / 2) {
216+ notification.x = 0
217+ } else {
218+ notification.x = notification.width
219+ }
220+ }
191221 }
192222
193223 Column {
@@ -447,7 +477,7 @@ Item {
447477 right: parent.right
448478 margins: contentSpacing
449479 }
450− visible: notification.type == Notification.SnapDecision && actionRepeater.count > 0 && !oneOverTwoCase.visible
480+ visible: notification.type === Notification.SnapDecision && actionRepeater.count > 0 && !oneOverTwoCase.visible
451481 spacing: units.gu(2)
452482 layoutDirection: Qt.RightToLeft
453483
qml/Notifications/Notifications.qml+2−6
@@ -41,15 +41,11 @@ ListView {
4141 property bool topmostIsFullscreen: false
4242 spacing: topmostIsFullscreen ? 0 : units.gu(.5)
4343
44− // FIXME: This doesn't make any sense and results in a binding loop
45− currentIndex: (currentIndex < 1 && count > 1) ? 1 : -1
44+ currentIndex: count > 1 ? 1 : -1
4645
4746 delegate: Notification {
4847 objectName: "notification" + index
49− anchors {
50− left: parent.left
51− right: parent.right
52− }
48+ width: parent.width
5349 type: model.type
5450 hints: model.hints
5551 iconSource: model.icon
qml/Panel/Panel.qml+5−3
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013-2014 Canonical, Ltd.
2+ * Copyright (C) 2013-2015 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -90,6 +90,9 @@ Item {
9090 right: indicators.left
9191 }
9292 saturation: 1 - indicators.unitProgress
93+
94+ // Don't let input event pass trough
95+ MouseArea { anchors.fill: parent }
9396 }
9497
9598 Image {
@@ -110,8 +113,7 @@ Item {
110113 right: indicators.left
111114 }
112115 height: indicators.minimizedPanelHeight
113− enabled: callHint.visible
114− onClicked: callHint.showLiveCall()
116+ onClicked: { if (callHint.visible) { callHint.showLiveCall(); } }
115117 }
116118
117119 IndicatorsMenu {
qml/Shell.qml+49−24
@@ -36,6 +36,7 @@ import "Panel"
3636import "Components"
3737import "Notifications"
3838import "Stages"
39+import "Tutorial"
3940import "Wizard"
4041import Unity.Notifications 1.0 as NotificationBackend
4142import Unity.Session 0.1
@@ -62,7 +63,7 @@ Item {
6263
6364 readonly property bool locked: LightDM.Greeter.active && !LightDM.Greeter.authenticated && !forcedUnlock
6465 readonly property alias hasLockedApp: greeter.hasLockedApp
65− readonly property bool forcedUnlock: edgeDemo.running
66+ readonly property bool forcedUnlock: tutorial.running
6667 onForcedUnlockChanged: if (forcedUnlock) lockscreen.hide()
6768
6869 property bool sideStageEnabled: shell.width >= units.gu(100)
@@ -161,7 +162,7 @@ Item {
161162
162163 ScreenGrabber {
163164 id: screenGrabber
164− z: edgeDemo.z + 10
165+ z: dialogs.z + 10
165166 enabled: Powerd.status === Powerd.On
166167 }
167168
@@ -252,14 +253,16 @@ Item {
252253 }
253254
254255 onApplicationAdded: {
255− if (greeter.shown && appId != "unity8-dash") {
256− greeter.startUnlock()
256+ if (appId != "unity8-dash") {
257+ if (greeter.shown) {
258+ greeter.startUnlock();
259+ }
257260
258261 // If this happens on first boot, we may be in edge
259262 // tutorial or wizard while receiving a call. But a call
260263 // is more important than wizard so just bail out of those.
261− if (edgeDemo.running) {
262− edgeDemo.hideEdgeDemos();
264+ if (tutorial.running) {
265+ tutorial.finish();
263266 wizard.hide();
264267 }
265268 }
@@ -287,6 +290,15 @@ Item {
287290 source: usageModeSettings.usageMode === "Windowed" ? "Stages/DesktopStage.qml"
288291 : tabletMode ? "Stages/TabletStage.qml" : "Stages/PhoneStage.qml"
289292
293+ property bool interactive: tutorial.spreadEnabled
294+ && !greeter.shown
295+ && !lockscreen.shown
296+ && panel.indicators.fullyClosed
297+ && launcher.progress == 0
298+ && !notifications.useModal
299+
300+ onInteractiveChanged: { if (interactive) { focus = true; } }
301+
290302 Binding {
291303 target: applicationsDisplayLoader.item
292304 property: "objectName"
@@ -306,12 +318,12 @@ Item {
306318 Binding {
307319 target: applicationsDisplayLoader.item
308320 property: "interactive"
309− value: edgeDemo.stagesEnabled && !greeter.shown && !lockscreen.shown && panel.indicators.fullyClosed && launcher.progress == 0 && !notifications.useModal
321+ value: applicationsDisplayLoader.interactive
310322 }
311323 Binding {
312324 target: applicationsDisplayLoader.item
313325 property: "spreadEnabled"
314− value: edgeDemo.stagesEnabled && !greeter.hasLockedApp
326+ value: tutorial.spreadEnabled && !greeter.hasLockedApp
315327 }
316328 Binding {
317329 target: applicationsDisplayLoader.item
@@ -644,9 +656,13 @@ Item {
644656 LauncherModel.setUser(user);
645657 }
646658
647− onTapped: launcher.tease()
659+ onTapped: {
660+ if (!tutorial.running) {
661+ launcher.tease();
662+ }
663+ }
648664 onDraggingChanged: {
649− if (dragging) {
665+ if (dragging && !tutorial.running) {
650666 launcher.tease();
651667 }
652668 }
@@ -684,7 +700,7 @@ Item {
684700
685701 onStatusChanged: {
686702 if (Powerd.status === Powerd.Off && reason !== Powerd.Proximity &&
687− !callManager.hasCalls && !edgeDemo.running) {
703+ !callManager.hasCalls && !tutorial.running) {
688704 // We don't want to simply call greeter.showNow() here, because
689705 // that will take too long. Qt will delay button event
690706 // handling until the greeter is done loading and may think the
@@ -700,7 +716,7 @@ Item {
700716 }
701717
702718 function showHome() {
703− if (edgeDemo.running) {
719+ if (tutorial.running) {
704720 return
705721 }
706722
@@ -742,15 +758,17 @@ Item {
742758 anchors.fill: parent //because this draws indicator menus
743759 indicators {
744760 hides: [launcher]
745− available: edgeDemo.panelEnabled && (!shell.locked || AccountsService.enableIndicatorsWhileLocked) && !greeter.hasLockedApp
746− contentEnabled: edgeDemo.panelContentEnabled
761+ available: tutorial.panelEnabled && (!shell.locked || AccountsService.enableIndicatorsWhileLocked) && !greeter.hasLockedApp
762+ contentEnabled: tutorial.panelContentEnabled
747763 width: parent.width > units.gu(60) ? units.gu(40) : parent.width
748764
749765 minimizedPanelHeight: units.gu(3)
750766 expandedPanelHeight: units.gu(7)
751767
752768 indicatorsModel: Indicators.IndicatorsModel {
753− Component.onCompleted: load(indicatorProfile);
769+ // TODO: This should be sourced by device type (e.g. "desktop", "tablet", "phone"...)
770+ profile: indicatorProfile
771+ Component.onCompleted: load()
754772 }
755773 }
756774 callHint {
@@ -776,8 +794,9 @@ Item {
776794 anchors.bottom: parent.bottom
777795 width: parent.width
778796 dragAreaWidth: shell.edgeSize
779− available: edgeDemo.launcherEnabled && (!shell.locked || AccountsService.enableLauncherWhileLocked) && !greeter.hasLockedApp
797+ available: tutorial.launcherEnabled && (!shell.locked || AccountsService.enableLauncherWhileLocked) && !greeter.hasLockedApp
780798 inverted: usageModeSettings.usageMode === "Staged"
799+ shadeBackground: !tutorial.running
781800
782801 onShowDashHome: showHome()
783802 onDash: showDash()
@@ -790,7 +809,7 @@ Item {
790809 if (greeter.hasLockedApp) {
791810 greeter.startUnlock()
792811 }
793− if (!edgeDemo.running)
812+ if (!tutorial.running)
794813 shell.activateApplication(appId)
795814 }
796815 onShownChanged: {
@@ -863,15 +882,21 @@ Item {
863882 }
864883 }
865884
866− EdgeDemo {
867− id: edgeDemo
868− objectName: "edgeDemo"
869− z: dialogs.z + 10
870− paused: Powerd.status === Powerd.Off || wizard.active // Saves power
871− greeter: greeter
885+ Tutorial {
886+ id: tutorial
887+ objectName: "tutorial"
888+ active: AccountsService.demoEdges
889+ paused: LightDM.Greeter.active
872890 launcher: launcher
873891 panel: panel
874892 stages: stages
893+ overlay: overlay
894+ edgeSize: shell.edgeSize
895+
896+ onFinished: {
897+ AccountsService.demoEdges = false;
898+ active = false; // for immediate response / if AS is having problems
899+ }
875900 }
876901
877902 Connections {
@@ -881,7 +906,7 @@ Item {
881906
882907 Rectangle {
883908 id: shutdownFadeOutRectangle
884− z: edgeDemo.z + 10
909+ z: screenGrabber.z + 10
885910 enabled: false
886911 visible: false
887912 color: "black"
qml/Stages/ApplicationWindow.qml+17−2
@@ -18,7 +18,7 @@ import QtQuick 2.0
1818import Ubuntu.Components 1.1
1919import Unity.Application 0.1
2020
21−Item {
21+FocusScope {
2222 id: root
2323
2424 // to be read from outside
@@ -43,6 +43,7 @@ Item {
4343 readonly property color splashColor: root.application ? root.application.splashColor : "#00000000"
4444 readonly property color splashColorHeader: root.application ? root.application.splashColorHeader : "#00000000"
4545 readonly property color splashColorFooter: root.application ? root.application.splashColorFooter : "#00000000"
46+ readonly property url defaultScreenshot: root.application ? root.application.defaultScreenshot : ""
4647
4748 // Whether the Application had a surface before but lost it.
4849 property bool hadSurface: sessionContainer.surfaceContainer.hadSurface
@@ -72,7 +73,7 @@ Item {
7273 Image {
7374 id: screenshotImage
7475 objectName: "screenshotImage"
75− source: ""
76+ source: d.defaultScreenshot
7677 anchors.fill: parent
7778 antialiasing: !root.interactive
7879
@@ -120,9 +121,12 @@ Item {
120121 d.surfaceInitialized = false;
121122 }
122123 }
124+
125+ focus: true
123126 }
124127
125128 StateGroup {
129+ id: stateGroup
126130 objectName: "applicationWindowStateGroup"
127131 states: [
128132 State {
@@ -215,6 +219,17 @@ Item {
215219 PropertyAction { target: screenshotImage; property: "source"; value: "" }
216220 }
217221 },
222+ Transition {
223+ from: "splashScreen"; to: "screenshot"
224+ SequentialAnimation {
225+ PropertyAction { target: screenshotImage
226+ property: "visible"; value: true }
227+ UbuntuNumberAnimation { target: screenshotImage; property: "opacity";
228+ from: 0.0; to: 1.0
229+ duration: UbuntuAnimation.BriskDuration }
230+ PropertyAction { target: splashLoader; property: "active"; value: false }
231+ }
232+ },
218233 Transition {
219234 from: "surface"; to: "void"
220235 SequentialAnimation {
qml/Stages/DesktopStage.qml+2−2
@@ -38,8 +38,6 @@ Item {
3838 Connections {
3939 target: ApplicationManager
4040 onApplicationAdded: {
41− // Initial placement to avoid having the window decoration behind the panel
42− appRepeater.itemAt(ApplicationManager.count-1).y = units.gu(3)
4341 ApplicationManager.requestFocusApplication(ApplicationManager.get(ApplicationManager.count-1).appId)
4442 }
4543
@@ -91,6 +89,7 @@ Item {
9189 delegate: Item {
9290 id: appDelegate
9391 z: ApplicationManager.count - index
92+ y: units.gu(3)
9493 width: units.gu(60)
9594 height: units.gu(50)
9695
@@ -121,6 +120,7 @@ Item {
121120 minWidth: appDelegate.minWidth
122121 minHeight: appDelegate.minHeight
123122 resizeHandleWidth: units.gu(0.5)
123+ windowId: model.appId // FIXME: Change this to point to windowId once we have such a thing
124124
125125 onPressed: ApplicationManager.requestFocusApplication(model.appId)
126126 }
qml/Stages/PhoneStage.qml+64−34
@@ -32,6 +32,21 @@ Rectangle {
3232 property bool spreadEnabled: true // If false, animations and right edge will be disabled
3333 property real inverseProgress: 0 // This is the progress for left edge drags, in pixels.
3434 property int orientation: Qt.PortraitOrientation
35+ property QtObject applicationManager: ApplicationManager
36+ property bool focusFirstApp: true // If false, focused app will appear on right edge like other apps
37+ property bool altTabEnabled: true
38+ property real startScale: 1.1
39+ property real endScale: 0.7
40+
41+ // How far left the stage has been dragged
42+ readonly property real dragProgress: spreadRepeater.count > 0 ? -spreadRepeater.itemAt(0).xTranslate : 0
43+
44+ readonly property alias dragging: spreadDragArea.dragging
45+
46+ // Only used by the tutorial right now, when it is teasing the right edge
47+ property real dragAreaOverlap
48+
49+ signal opened()
3550
3651 color: "#111111"
3752
@@ -52,30 +67,30 @@ Rectangle {
5267 if (inverseProgress == 0 && priv.oldInverseProgress > 0) {
5368 // left edge drag released. Minimum distance is given by design.
5469 if (priv.oldInverseProgress > units.gu(22)) {
55− ApplicationManager.requestFocusApplication("unity8-dash");
70+ applicationManager.requestFocusApplication("unity8-dash");
5671 }
5772 }
5873 priv.oldInverseProgress = inverseProgress;
5974 }
6075
6176 Connections {
62− target: ApplicationManager
77+ target: applicationManager
6378
6479 onFocusRequested: {
6580 if (spreadView.phase > 0) {
6681 spreadView.snapTo(priv.indexOf(appId));
6782 } else {
68− ApplicationManager.focusApplication(appId);
83+ applicationManager.focusApplication(appId);
6984 }
7085 }
7186
7287 onApplicationAdded: {
7388 if (spreadView.phase == 2) {
74− spreadView.snapTo(ApplicationManager.count - 1);
89+ spreadView.snapTo(applicationManager.count - 1);
7590 } else {
7691 spreadView.phase = 0;
7792 spreadView.contentX = -spreadView.shift;
78− ApplicationManager.focusApplication(appId);
93+ applicationManager.focusApplication(appId);
7994 }
8095 }
8196
@@ -90,9 +105,9 @@ Rectangle {
90105 }
91106
92107 function focusTopMostApp() {
93− if (ApplicationManager.count > 0) {
94− var topmostApp = ApplicationManager.get(0);
95− ApplicationManager.focusApplication(topmostApp.appId);
108+ if (applicationManager.count > 0) {
109+ var topmostApp = applicationManager.get(0);
110+ applicationManager.focusApplication(topmostApp.appId);
96111 }
97112 }
98113 }
@@ -100,18 +115,21 @@ Rectangle {
100115 QtObject {
101116 id: priv
102117
103− property string focusedAppId: ApplicationManager.focusedApplicationId
104− property var focusedApplication: ApplicationManager.findApplication(focusedAppId)
118+ readonly property int firstSpreadIndex: root.focusFirstApp ? 1 : 0
119+ property string focusedAppId: applicationManager.focusedApplicationId
120+ property var focusedApplication: applicationManager.findApplication(focusedAppId)
105121 property var focusedAppDelegate: null
106122
107123 property real oldInverseProgress: 0
108− property bool animateX: true
124+ property bool animateX: false
109125
110126 onFocusedAppIdChanged: focusedAppDelegate = spreadRepeater.itemAt(0);
111127
128+ onFocusedAppDelegateChanged: focusedAppDelegate.focus = true;
129+
112130 function indexOf(appId) {
113− for (var i = 0; i < ApplicationManager.count; i++) {
114− if (ApplicationManager.get(i).appId == appId) {
131+ for (var i = 0; i < applicationManager.count; i++) {
132+ if (applicationManager.get(i).appId == appId) {
115133 return i;
116134 }
117135 }
@@ -131,7 +149,7 @@ Rectangle {
131149
132150 // This indicates when the spreadView is active. That means, all the animations
133151 // are activated and tiles need to line up for the spread.
134− readonly property bool active: shiftedContentX > 0 || spreadDragArea.status === DirectionalDragArea.Recognized
152+ readonly property bool active: shiftedContentX > 0 || spreadDragArea.status === DirectionalDragArea.Recognized || !root.focusFirstApp
135153
136154 // The flickable needs to fill the screen in order to get touch events all over.
137155 // However, we don't want to the user to be able to scroll back all the way. For
@@ -197,10 +215,17 @@ Rectangle {
197215 // Add 1 pixel to make sure we definitely hit positionMarker4 even with rounding errors of the animation.
198216 snapAnimation.targetContentX = width * positionMarker4 + 1 - shift;
199217 snapAnimation.start();
218+ root.opened();
200219 }
201220 }
202221 function snapTo(index) {
203− if (ApplicationManager.count <= index) {
222+ if (!root.altTabEnabled) {
223+ // Reset to start instead
224+ snapAnimation.targetContentX = -shift;
225+ snapAnimation.start();
226+ return;
227+ }
228+ if (applicationManager.count <= index) {
204229 // In case we're trying to snap to some non existing app, lets snap back to the first one
205230 index = 0;
206231 }
@@ -215,10 +240,12 @@ Rectangle {
215240 snapAnimation.start();
216241 }
217242
218− // In case the ApplicationManager already holds an app when starting up we're missing animations
243+ // In case the applicationManager already holds an app when starting up we're missing animations
219244 // Make sure we end up in the same state
220245 Component.onCompleted: {
221246 spreadView.contentX = -spreadView.shift
247+ priv.animateX = true;
248+ snapAnimation.complete();
222249 }
223250
224251 SequentialAnimation {
@@ -235,7 +262,7 @@ Rectangle {
235262 ScriptAction {
236263 script: {
237264 if (spreadView.selectedIndex >= 0) {
238− ApplicationManager.focusApplication(ApplicationManager.get(spreadView.selectedIndex).appId);
265+ applicationManager.focusApplication(applicationManager.get(spreadView.selectedIndex).appId);
239266
240267 spreadView.selectedIndex = -1;
241268 spreadView.phase = 0;
@@ -250,7 +277,7 @@ Rectangle {
250277 // This width controls how much the spread can be flicked left/right. It's composed of:
251278 // tileDistance * app count (with a minimum of 3 apps, in order to also allow moving 1 and 2 apps a bit)
252279 // + some constant value (still scales with the screen width) which looks good and somewhat fills the screen
253− width: Math.max(3, ApplicationManager.count) * spreadView.tileDistance + (spreadView.width - spreadView.tileDistance) * 1.5
280+ width: Math.max(3, applicationManager.count) * spreadView.tileDistance + (spreadView.width - spreadView.tileDistance) * 1.5
254281 height: parent.height
255282 Behavior on width {
256283 enabled: spreadView.closingIndex >= 0
@@ -265,20 +292,22 @@ Rectangle {
265292 x: spreadView.contentX
266293
267294 onClicked: {
268− spreadView.snapTo(0);
295+ if (root.altTabEnabled) {
296+ spreadView.snapTo(0);
297+ }
269298 }
270299
271300 Repeater {
272301 id: spreadRepeater
273302 objectName: "spreadRepeater"
274− model: ApplicationManager
303+ model: applicationManager
275304 delegate: TransformedSpreadDelegate {
276305 id: appDelegate
277306 objectName: "appDelegate" + index
278307 startAngle: 45
279308 endAngle: 5
280− startScale: 1.1
281− endScale: 0.7
309+ startScale: root.startScale
310+ endScale: root.endScale
282311 startDistance: spreadView.tileDistance
283312 endDistance: units.gu(.5)
284313 width: spreadView.width
@@ -286,11 +315,12 @@ Rectangle {
286315 selected: spreadView.selectedIndex == index
287316 otherSelected: spreadView.selectedIndex >= 0 && !selected
288317 interactive: !spreadView.interactive && spreadView.phase === 0
289− && spreadView.shiftedContentX === 0 && root.interactive && index === 0
290− swipeToCloseEnabled: spreadView.interactive && !snapAnimation.running
318+ && spreadView.shiftedContentX === 0 && root.interactive && isFocused
319+ swipeToCloseEnabled: spreadView.interactive && root.interactive && !snapAnimation.running
291320 maximizedAppTopMargin: root.maximizedAppTopMargin
292321 dropShadow: spreadView.active ||
293322 (priv.focusedAppDelegate && priv.focusedAppDelegate.x !== 0)
323+ focusFirstApp: root.focusFirstApp
294324
295325 readonly property bool isDash: model.appId == "unity8-dash"
296326
@@ -298,7 +328,7 @@ Rectangle {
298328
299329 x: {
300330 // focused app is always positioned at 0 except when following left edge drag
301− if (index == 0) {
331+ if (isFocused) {
302332 if (!isDash && root.inverseProgress > 0 && spreadView.phase === 0) {
303333 return root.inverseProgress;
304334 }
@@ -309,10 +339,10 @@ Rectangle {
309339 }
310340
311341 // Otherwise line up for the spread
312− return spreadView.width + (index - 1) * spreadView.tileDistance;
342+ return spreadView.width + spreadIndex * spreadView.tileDistance;
313343 }
314344
315− application: ApplicationManager.get(index)
345+ application: applicationManager.get(index)
316346 closeable: !isDash
317347
318348 property real behavioredIndex: index
@@ -350,7 +380,7 @@ Rectangle {
350380 progress: {
351381 var tileProgress = (spreadView.shiftedContentX - behavioredIndex * spreadView.tileDistance) / spreadView.width;
352382 // Tile 1 needs to move directly from the beginning...
353− if (behavioredIndex == 1 && spreadView.phase < 2) {
383+ if (root.focusFirstApp && behavioredIndex == 1 && spreadView.phase < 2) {
354384 tileProgress += spreadView.tileDistance / spreadView.width;
355385 }
356386 // Limiting progress to ~0 and 1.7 to avoid binding calculations when tiles are not
@@ -365,7 +395,7 @@ Rectangle {
365395
366396 // This mostly is the same as progress, just adds the snapping to phase 1 for tiles 0 and 1
367397 animatedProgress: {
368− if (spreadView.phase == 0 && index < 2) {
398+ if (spreadView.phase == 0 && index <= priv.firstSpreadIndex) {
369399 if (progress < spreadView.positionMarker1) {
370400 return progress;
371401 } else if (progress < spreadView.positionMarker1 + 0.05){
@@ -390,11 +420,11 @@ Rectangle {
390420 }
391421
392422 onClicked: {
393− if (spreadView.phase == 2) {
394− if (ApplicationManager.focusedApplicationId == ApplicationManager.get(index).appId) {
423+ if (root.altTabEnabled && spreadView.phase == 2) {
424+ if (applicationManager.focusedApplicationId == applicationManager.get(index).appId) {
395425 spreadView.snapTo(index);
396426 } else {
397− ApplicationManager.requestFocusApplication(ApplicationManager.get(index).appId);
427+ applicationManager.requestFocusApplication(applicationManager.get(index).appId);
398428 }
399429 }
400430 }
@@ -409,7 +439,7 @@ Rectangle {
409439
410440 onClosed: {
411441 spreadView.closingIndex = index;
412− ApplicationManager.stopApplication(ApplicationManager.get(index).appId);
442+ applicationManager.stopApplication(applicationManager.get(index).appId);
413443 }
414444 }
415445 }
@@ -422,7 +452,7 @@ Rectangle {
422452 direction: Direction.Leftwards
423453 enabled: (spreadView.phase != 2 && root.spreadEnabled) || dragging
424454
425− anchors { top: parent.top; right: parent.right; bottom: parent.bottom }
455+ anchors { top: parent.top; right: parent.right; bottom: parent.bottom; rightMargin: -root.dragAreaOverlap }
426456 width: root.dragAreaWidth
427457
428458 property var gesturePoints: new Array()
qml/Stages/SessionContainer.qml+22−7
@@ -17,7 +17,7 @@
1717import QtQuick 2.0
1818import "Animations"
1919
20−Item {
20+FocusScope {
2121 id: root
2222 objectName: "sessionContainer"
2323 property QtObject session
@@ -34,8 +34,8 @@ Item {
3434 orientation: root.orientation
3535 }
3636
37−
3837 Repeater {
38+ id: childSessionsRepeater
3939 model: root.childSessions
4040
4141 delegate: Loader {
@@ -45,9 +45,17 @@ Item {
4545 // Only way to do recursive qml items.
4646 source: Qt.resolvedUrl("SessionContainer.qml")
4747
48+ z: index
49+
50+ // Since a Loader is a FocusScope, propagate its focus to the loaded Item
51+ Binding {
52+ target: item; when: item
53+ property: "focus"; value: focus
54+ }
55+
4856 Binding {
4957 target: item; when: item
50− property: "interactive"; value: root.interactive
58+ property: "interactive"; value: index == (childSessionsRepeater.count - 1) && root.interactive
5159 }
5260
5361 Binding {
@@ -69,10 +77,6 @@ Item {
6977 target: item; when: item
7078 property: "orientation"; value: root.orientation
7179 }
72−
73− Component.onDestruction: {
74− root.session.surface.forceActiveFocus();
75− }
7680 }
7781 }
7882
@@ -138,5 +142,16 @@ Item {
138142 QtObject {
139143 id: d
140144 property var animations: []
145+
146+ property var focusedChild: {
147+ if (childSessionsRepeater.count == 0) {
148+ return _surfaceContainer;
149+ } else {
150+ return childSessionsRepeater.itemAt(childSessionsRepeater.count - 1);
151+ }
152+ }
153+ onFocusedChildChanged: {
154+ focusedChild.focus = true;
155+ }
141156 }
142157}
qml/Stages/SpreadDelegate.qml+2−1
@@ -21,7 +21,7 @@ import QtQuick 2.0
2121import Ubuntu.Components 1.1
2222import "../Components"
2323
24−Item {
24+FocusScope {
2525 id: root
2626
2727 // to be read from outside
@@ -65,6 +65,7 @@ Item {
6565 ApplicationWindow {
6666 id: appWindow
6767 objectName: application ? "appWindow_" + application.appId : "appWindow_null"
68+ focus: true
6869 anchors {
6970 fill: parent
7071 topMargin: appWindow.fullscreen ? 0 : maximizedAppTopMargin
qml/Stages/SurfaceContainer.qml+11−25
@@ -18,7 +18,7 @@ import QtQuick 2.0
1818import Ubuntu.Components 1.1
1919import Ubuntu.Gestures 0.1 // For TouchGate
2020
21−Item {
21+FocusScope {
2222 id: root
2323 objectName: "surfaceContainer"
2424 property Item surface: null
@@ -26,10 +26,15 @@ Item {
2626 property int orientation
2727 property bool interactive
2828
29+ signal surfacePressed()
30+
2931 onSurfaceChanged: {
3032 if (surface) {
33+ // Set the surface focus *after* it is added to the scene to
34+ // ensure an update to the scene's active focus.
35+ surface.focus = false;
3136 surface.parent = root;
32− d.forceSurfaceActiveFocusIfReady();
37+ surface.focus = true;
3338 } else {
3439 hadSurface = true;
3540 }
@@ -38,7 +43,6 @@ Item {
3843 Binding { target: surface; property: "orientation"; value: root.orientation }
3944 Binding { target: surface; property: "z"; value: 1 }
4045 Binding { target: surface; property: "enabled"; value: root.interactive; when: surface }
41− Binding { target: surface; property: "focus"; value: root.interactive; when: surface }
4246 Binding { target: surface; property: "antialiasing"; value: !root.interactive; when: surface }
4347
4448 TouchGate {
@@ -46,28 +50,10 @@ Item {
4650 anchors.fill: root
4751 enabled: root.surface ? root.surface.enabled : false
4852 z: 2
49− }
50−
51− Connections {
52− target: root.surface
53− // FIXME: I would rather not need to do this, but currently it doesn't get
54− // active focus without it and I don't know why.
55− // Possibly because if an item get focus=true before it has a parent, once
56− // it gets a parent QQuickWindow won't check its focus and update its activeFocus
57− // accordingly. Unlike when you focus=true after the item already has a parent.
58− onFocusChanged: d.forceSurfaceActiveFocusIfReady();
59− onParentChanged: d.forceSurfaceActiveFocusIfReady();
60− onEnabledChanged: d.forceSurfaceActiveFocusIfReady();
61− }
62−
63− QtObject {
64− id: d
65− function forceSurfaceActiveFocusIfReady() {
66− if (root.surface !== null &&
67− root.surface.focus &&
68− root.surface.parent === root &&
69− root.surface.enabled) {
70− root.surface.forceActiveFocus();
53+ onPressed: {
54+ root.focus = true;
55+ if (root.interactive) {
56+ root.forceActiveFocus();
7157 }
7258 }
7359 }
qml/Stages/TabletStage.qml+14
@@ -76,6 +76,20 @@ Rectangle {
7676
7777 appId0 = ApplicationManager.count >= 1 ? ApplicationManager.get(0).appId : "";
7878 appId1 = ApplicationManager.count > 1 ? ApplicationManager.get(1).appId : "";
79+
80+ // Update the QML focus accordingly
81+ updateSpreadDelegateFocus();
82+ }
83+
84+ function updateSpreadDelegateFocus() {
85+ if (priv.focusedAppId) {
86+ var focusedAppIndex = priv.indexOf(priv.focusedAppId);
87+ if (focusedAppIndex !== -1) {
88+ spreadRepeater.itemAt(focusedAppIndex).focus = true;
89+ } else {
90+ console.warn("TabletStage: Failed to find the SpreadDelegate for appID=" + priv.focusedAppId);
91+ }
92+ }
7993 }
8094
8195 function indexOf(appId) {
qml/Stages/TransformedSpreadDelegate.qml+27−21
@@ -46,6 +46,18 @@ SpreadDelegate {
4646 property real startDistance: units.gu(5)
4747 property real endDistance: units.gu(.5)
4848
49+ // Whether first app is displayed entirely vs just on the spread edge
50+ property bool focusFirstApp: true
51+
52+ // Whether this delegate is displayed to the user outside of the spread
53+ readonly property bool isFocused: focusFirstApp && index === 0
54+
55+ // Adjusted index for any spread animation calculations
56+ readonly property int spreadIndex: index - (focusFirstApp ? 1 : 0)
57+
58+ // How far left this delegate has been translated
59+ readonly property alias xTranslate: priv.xTranslate
60+
4961 onSelectedChanged: {
5062 if (selected) {
5163 priv.snapshot();
@@ -63,8 +75,8 @@ SpreadDelegate {
6375 Connections {
6476 target: spreadView
6577 onPhaseChanged: {
66− if (spreadView.phase == 1) {
67− if (index == 0) {
78+ if (root.focusFirstApp && spreadView.phase == 1) {
79+ if (index === 0) {
6880 priv.phase2startTranslate = priv.easingAnimation(0, spreadView.positionMarker4, 0, -spreadView.width, spreadView.positionMarker4) + spreadView.width;
6981 priv.phase2startAngle = priv.easingAnimation(0, spreadView.positionMarker4, root.startAngle, root.endAngle, spreadView.positionMarker4);
7082 priv.phase2startScale = priv.easingAnimation(0, spreadView.positionMarker4, root.startScale, root.endScale, spreadView.positionMarker4);
@@ -134,7 +146,7 @@ SpreadDelegate {
134146 }
135147
136148 if (otherSelected) {
137− if (spreadView.phase < 2 && index == 0) {
149+ if (spreadView.phase < 2 && root.isFocused) {
138150 return linearAnimation(selectedProgress, 0, selectedXTranslate,
139151 selectedXTranslate - spreadView.tileDistance, root.progress);
140152 }
@@ -142,8 +154,7 @@ SpreadDelegate {
142154 return selectedXTranslate;
143155 }
144156
145− switch (index) {
146− case 0:
157+ if (root.focusFirstApp && index === 0) {
147158 if (spreadView.phase == 0) {
148159 return Math.min(0, linearAnimation(0, spreadView.positionMarker2,
149160 0, -spreadView.width * .25, root.animatedProgress));
@@ -153,11 +164,10 @@ SpreadDelegate {
153164 } else if (!priv.isSelected){ // phase 2
154165 // Apply the same animation as with the rest but add spreadView.width to align it with the others.
155166 return -easingCurve.value * spreadView.width + spreadView.width;
156− } else if (priv.isSelected) {
167+ } else {
157168 return linearAnimation(selectedProgress, 0, selectedXTranslate, 0, root.progress);
158169 }
159−
160− case 1:
170+ } else if (root.focusFirstApp && index === 1) {
161171 if (spreadView.phase == 0 && !priv.isSelected) {
162172 return linearAnimation(0, spreadView.positionMarker2,
163173 0, -spreadView.width * spreadView.snapPosition, root.animatedProgress);
@@ -170,13 +180,13 @@ SpreadDelegate {
170180
171181 if (priv.isSelected) {
172182 // Distance to left edge
173− var targetTranslate = -spreadView.width - ((index - 1) * root.startDistance);
183+ var targetTranslate = -spreadView.width - (root.spreadIndex * root.startDistance);
174184 return linearAnimation(selectedProgress, 0,
175185 selectedXTranslate, targetTranslate, root.progress);
176186 }
177187
178188 // Fix it at the right edge...
179− var rightEdgeOffset = -((index - 1) * root.startDistance);
189+ var rightEdgeOffset = -(root.spreadIndex * root.startDistance);
180190 // ...and use our easing to move them to the left. Stop a bit earlier for each tile
181191 var animatedEndDistance = linearAnimation(0, 2, root.endDistance, 0, root.progress);
182192 return -easingCurve.value * spreadView.width + (index * animatedEndDistance) + rightEdgeOffset;
@@ -194,8 +204,7 @@ SpreadDelegate {
194204 if (priv.isSelected) {
195205 return linearAnimation(selectedProgress, 0, selectedAngle, 0, root.progress);
196206 }
197− switch (index) {
198− case 0:
207+ if (root.focusFirstApp && index === 0) {
199208 if (spreadView.phase == 0) {
200209 return Math.max(0, linearAnimation(0, spreadView.positionMarker2,
201210 0, root.tile0SnapAngle, root.animatedProgress));
@@ -203,7 +212,7 @@ SpreadDelegate {
203212 return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4,
204213 root.tile0SnapAngle, phase2startAngle, root.progress);
205214 }
206− case 1:
215+ } else if (root.focusFirstApp && index === 1) {
207216 if (spreadView.phase == 0) {
208217 return linearAnimation(0, spreadView.positionMarker2, root.startAngle,
209218 root.startAngle * (1-spreadView.snapPosition), root.animatedProgress);
@@ -227,15 +236,14 @@ SpreadDelegate {
227236 return linearAnimation(selectedProgress, 0, selectedScale, 1, root.progress);
228237 }
229238
230− switch (index) {
231− case 0:
239+ if (root.focusFirstApp && index === 0) {
232240 if (spreadView.phase == 0) {
233241 return 1;
234242 } else if (spreadView.phase == 1) {
235243 return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4,
236244 1, phase2startScale, root.progress);
237245 }
238− case 1:
246+ } else if (root.focusFirstApp && index === 1) {
239247 if (spreadView.phase == 0) {
240248 var targetScale = tile1StartScale - ((tile1StartScale - 1) * spreadView.snapPosition);
241249 return linearAnimation(0, spreadView.positionMarker2,
@@ -254,7 +262,7 @@ SpreadDelegate {
254262 return linearAnimation (selectedProgress, Math.max(0, selectedProgress - .5),
255263 selectedOpacity, 0, root.progress);
256264 }
257− if (index == 0) {
265+ if (root.isFocused) {
258266 switch (spreadView.phase) {
259267 case 0:
260268 return linearAnimation(0, spreadView.positionMarker2, 1, .7, root.animatedProgress);
@@ -272,16 +280,14 @@ SpreadDelegate {
272280 return linearAnimation(selectedProgress, 0, selectedTopMarginProgress, 0, root.progress);
273281 }
274282
275− switch (index) {
276− case 0:
283+ if (root.focusFirstApp && index === 0) {
277284 if (spreadView.phase == 0) {
278285 return 0;
279286 } else if (spreadView.phase == 1) {
280287 return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4,
281288 0, priv.phase2startTopMarginProgress, root.progress);
282289 }
283− break;
284− case 1:
290+ } else if (root.focusFirstApp && index === 1) {
285291 if (spreadView.phase == 0) {
286292 return 0;
287293 } else if (spreadView.phase == 1) {
qml/Stages/TransformedTabletSpreadDelegate.qml+1−1
@@ -366,7 +366,7 @@ SpreadDelegate {
366366 Scale {
367367 origin { x: 0; y: (spreadView.height * priv.scale) + maximizedAppTopMargin * 3 }
368368 xScale: 1
369− yScale: isFullscreen ? 1 - priv.topMarginProgress * maximizedAppTopMargin / spreadView.height : 1
369+ yScale: fullscreen ? 1 - priv.topMarginProgress * maximizedAppTopMargin / spreadView.height : 1
370370 },
371371 Translate {
372372 x: priv.xTranslate
qml/Stages/WindowMoveResizeArea.qml+33−24
@@ -18,6 +18,7 @@
1818
1919import QtQuick 2.3
2020import Ubuntu.Components 1.1
21+import Utils 0.1
2122
2223MouseArea {
2324 id: root
@@ -26,7 +27,8 @@ MouseArea {
2627
2728 // The target item managed by this. Must be a parent or a sibling
2829 // The area will anchor to it and manage move and resize events
29− property var target: null
30+ property Item target: null
31+ property string windowId: ""
3032 property int resizeHandleWidth: 0
3133 property int minWidth: 0
3234 property int minHeight: 0
@@ -37,8 +39,6 @@ MouseArea {
3739 readonly property int windowHeight: root.height - resizeHandleWidth * 2
3840
3941 property var startPoint
40− property int startWidth
41− property int startHeight
4242
4343 property bool resizeTop: false
4444 property bool resizeBottom: false
@@ -47,10 +47,18 @@ MouseArea {
4747
4848 }
4949
50+ Component.onCompleted: {
51+ var windowState = WindowStateStorage.getGeometry(root.windowId, Qt.rect(target.x, target.y, target.width, target.height))
52+ if (windowState !== undefined) {
53+ target.x = windowState.x
54+ target.y = windowState.y
55+ target.width = windowState.width
56+ target.height = windowState.height
57+ }
58+ }
59+
5060 onPressed: {
51− priv.startPoint = mapToItem(null, Qt.point(mouse.x, mouse.y)).x;
52− priv.startWidth = root.width;
53− priv.startHeight = root.height;
61+ priv.startPoint = Qt.point(mouse.x, mouse.y);
5462 priv.resizeTop = mouseY < root.resizeHandleWidth;
5563 priv.resizeBottom = mouseY > (root.height - root.resizeHandleWidth);
5664 priv.resizeLeft = mouseX < root.resizeHandleWidth;
@@ -58,32 +66,28 @@ MouseArea {
5866 }
5967
6068 onPositionChanged: {
61− var currentPoint = mapToItem(null, Qt.point(mouse.x, mouse.y)).x;
69+ var currentPoint = Qt.point(mouse.x, mouse.y);
6270 var mouseDiff = Qt.point(currentPoint.x - priv.startPoint.x, currentPoint.y - priv.startPoint.y);
6371 var moveDiff = Qt.point(0, 0);
6472 var sizeDiff = Qt.point(0, 0);
73+ var maxSizeDiff = Qt.point(root.minWidth - root.target.width, root.minHeight - root.target.height)
74+
6575 if (priv.resizeTop || priv.resizeBottom || priv.resizeLeft || priv.resizeRight) {
6676 if (priv.resizeTop) {
67− moveDiff.y += mouseDiff.y;
68− sizeDiff.y += -mouseDiff.y;
69− } else if (priv.resizeBottom) {
70− sizeDiff.y += mouse.y - priv.startPoint.y;
71− priv.startPoint.y = mouse.y
77+ sizeDiff.y = Math.max(maxSizeDiff.y, -currentPoint.y + priv.startPoint.y)
78+ moveDiff.y = -sizeDiff.y
7279 }
73− if (priv.resizeLeft) {
74− moveDiff.x += mouseDiff.x;
75− sizeDiff.x += -mouseDiff.x;
76− } else if (priv.resizeRight) {
77− sizeDiff.x += mouse.x - priv.startPoint.x;
78− priv.startPoint.x = mouse.x
80+ if (priv.resizeBottom) {
81+ sizeDiff.y = Math.max(maxSizeDiff.y, currentPoint.y - priv.startPoint.y)
82+ priv.startPoint.y += sizeDiff.y
7983 }
80− if (priv.windowWidth + sizeDiff.x < root.minWidth) {
81− sizeDiff.x = root.minWidth - priv.windowWidth;
82− moveDiff.x = moveDiff.x == 0 ? 0 : -sizeDiff.x;
84+ if (priv.resizeLeft) {
85+ sizeDiff.x = Math.max(maxSizeDiff.x, -currentPoint.x + priv.startPoint.x)
86+ moveDiff.x = -sizeDiff.x
8387 }
84− if (priv.windowHeight + sizeDiff.y < root.minHeight) {
85− sizeDiff.y = root.minHeight - priv.windowHeight;
86− moveDiff.y = moveDiff.y == 0 ? 0 : -sizeDiff.y;
88+ if (priv.resizeRight) {
89+ sizeDiff.x = Math.max(maxSizeDiff.x, currentPoint.x - priv.startPoint.x)
90+ priv.startPoint.x += sizeDiff.x
8791 }
8892
8993 target.x += moveDiff.x;
@@ -94,5 +98,10 @@ MouseArea {
9498 target.x += mouseDiff.x;
9599 target.y += mouseDiff.y;
96100 }
101+
102+ }
103+
104+ Component.onDestruction: {
105+ WindowStateStorage.saveGeometry(root.windowId, Qt.rect(target.x, target.y, target.width, target.height))
97106 }
98107}
qml/Tutorial/Arrow.qmlnew file+56
@@ -0,0 +1,56 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.3
18+import Ubuntu.Components 1.1
19+
20+Item {
21+ id: root
22+
23+ property alias color: circle.color
24+
25+ // Will make whole arrow darker
26+ property real darkenBy: 0
27+
28+ property alias chevronOpacity: chevron.opacity
29+
30+ ////
31+
32+ Rectangle {
33+ id: circle
34+ anchors.fill: parent
35+ radius: width / 2
36+ }
37+
38+ Image {
39+ id: chevron
40+ anchors.centerIn: parent
41+ source: Qt.resolvedUrl("graphics/chevron.png")
42+ fillMode: Image.PreserveAspectFit
43+ sourceSize.width: 152
44+ sourceSize.height: 152
45+ width: parent.width / 2
46+ height: parent.height / 2
47+ }
48+
49+ Rectangle {
50+ id: darkCircle
51+ anchors.fill: parent
52+ radius: width / 2
53+ color: "black"
54+ opacity: root.darkenBy
55+ }
56+}
qml/Tutorial/Slider.qmlnew file+123
@@ -0,0 +1,123 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.3
18+import Ubuntu.Components 1.1
19+
20+Item {
21+ id: root
22+
23+ // Whether this slider is short or long
24+ property bool shortSwipe
25+
26+ // How far the user has slid
27+ property real offset
28+
29+ // Set to true when slider is being used
30+ property bool active
31+
32+ // How far in percentage terms
33+ readonly property real percent: d.slideOffset / target.x
34+
35+ QtObject {
36+ id: d
37+ readonly property color trayColor: "#424141"
38+ readonly property real margin: units.gu(0.5)
39+ readonly property real arrowSize: root.height - margin * 2
40+ readonly property real dotSize: units.dp(1)
41+ readonly property real slideOffset: MathUtils.clamp(root.offset - offscreenOffset, -offscreenOffset, target.x)
42+ readonly property real offscreenOffset: units.gu(2)
43+ }
44+
45+ implicitWidth: shortSwipe ? units.gu(15) : units.gu(27.5)
46+ implicitHeight: units.gu(6.5)
47+
48+ Rectangle {
49+ color: d.trayColor
50+ anchors.fill: parent
51+ anchors.rightMargin: clipBox.width - 1
52+ }
53+
54+ // We want to have a circular border around the target. But we can't just
55+ // do a radius on two of a rectangle's corners. So we clip a full circle.
56+ Item {
57+ id: clipBox
58+
59+ clip: true
60+ anchors.top: parent.top
61+ anchors.bottom: parent.bottom
62+ anchors.right: parent.right
63+ width: parent.height / 2
64+
65+ Rectangle {
66+ color: d.trayColor
67+ anchors.top: parent.top
68+ anchors.bottom: parent.bottom
69+ anchors.right: parent.right
70+ width: parent.width * 2
71+ radius: parent.width
72+ }
73+ }
74+
75+ Arrow {
76+ id: target
77+ width: d.arrowSize
78+ height: d.arrowSize
79+ color: "#73000000"
80+ chevronOpacity: 0.52
81+ anchors.right: parent.right
82+ anchors.rightMargin: d.margin
83+ anchors.verticalCenter: parent.verticalCenter
84+ }
85+
86+ Row {
87+ anchors.left: handle.horizontalCenter
88+ anchors.right: target.horizontalCenter
89+ anchors.verticalCenter: parent.verticalCenter
90+
91+ layoutDirection: Qt.RightToLeft
92+ spacing: d.dotSize * 2
93+
94+ Repeater {
95+ model: parent.width / (parent.spacing + d.dotSize)
96+ Rectangle {
97+ anchors.verticalCenter: parent ? parent.verticalCenter : undefined
98+ height: d.dotSize
99+ width: height
100+ radius: width
101+ color: "white"
102+ opacity: 0.2
103+ }
104+ }
105+ }
106+
107+ Arrow {
108+ id: handle
109+ width: d.arrowSize
110+ height: d.arrowSize
111+ color: UbuntuColors.orange
112+ darkenBy: root.active ? 0.5 : 0
113+ anchors.left: parent.left
114+ // We use a Translate transform rather than anchors.leftMargin because
115+ // the latter has weird performance problems on the TutorialRight page.
116+ transform: [
117+ Translate {
118+ x: d.slideOffset
119+ }
120+ ]
121+ anchors.verticalCenter: parent.verticalCenter
122+ }
123+}
qml/Tutorial/Tick.qmlnew file+29
@@ -0,0 +1,29 @@
1+/*
2+ * Copyright (C) 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 General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.3
18+import Ubuntu.Components 1.1
19+
20+MouseArea {
21+ implicitHeight: tick.height
22+ implicitWidth: tick.width
23+ Image {
24+ id: tick
25+ source: Qt.resolvedUrl("graphics/tick.png")
26+ height: units.gu(6.5)
27+ width: units.gu(6.5)
28+ }
29+}
qml/Tutorial/Tutorial.qmlnew file+92
@@ -0,0 +1,92 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.3
18+import Ubuntu.Components 1.1
19+
20+Item {
21+ id: root
22+
23+ property alias active: loader.active
24+ property bool paused
25+ property real edgeSize
26+
27+ property Item launcher
28+ property Item panel
29+ property Item stages
30+ property Item overlay
31+
32+ readonly property bool launcherEnabled: loader.item ? loader.item.launcherEnabled : true
33+ readonly property bool spreadEnabled: loader.item ? loader.item.spreadEnabled : true
34+ readonly property bool panelEnabled: loader.item ? loader.item.panelEnabled : true
35+ readonly property bool panelContentEnabled: loader.item ? loader.item.panelContentEnabled : true
36+ readonly property bool running: loader.item ? loader.item.running : false
37+
38+ function finish() {
39+ if (loader.item) {
40+ loader.item.finish();
41+ }
42+ }
43+
44+ signal finished()
45+
46+ Loader {
47+ id: loader
48+ anchors.fill: parent
49+ source: "TutorialContent.qml"
50+
51+ Binding {
52+ target: loader.item
53+ property: "paused"
54+ value: root.paused
55+ }
56+
57+ Binding {
58+ target: loader.item
59+ property: "edgeSize"
60+ value: root.edgeSize
61+ }
62+
63+ Binding {
64+ target: loader.item
65+ property: "launcher"
66+ value: root.launcher
67+ }
68+
69+ Binding {
70+ target: loader.item
71+ property: "panel"
72+ value: root.panel
73+ }
74+
75+ Binding {
76+ target: loader.item
77+ property: "stages"
78+ value: root.stages
79+ }
80+
81+ Binding {
82+ target: loader.item
83+ property: "overlay"
84+ value: root.overlay
85+ }
86+
87+ Connections {
88+ target: loader.item
89+ onFinished: root.finished()
90+ }
91+ }
92+}
qml/Tutorial/TutorialBottom.qmlnew file+104
@@ -0,0 +1,104 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.3
18+import Ubuntu.Components 1.1
19+import Ubuntu.Gestures 0.1
20+import "../Components"
21+import "." as LocalComponents
22+
23+TutorialPage {
24+ id: root
25+
26+ property alias edgeSize: dragArea.height
27+
28+ title: i18n.tr("Open special menus")
29+ text: i18n.tr("Swipe up from the bottom edge.")
30+ fullTextWidth: true
31+
32+ SequentialAnimation {
33+ id: teaseAnimation
34+ paused: running && root.paused
35+ running: !dragArea.useTouchY && slider.dragOffset === 0
36+ loops: Animation.Infinite
37+
38+ UbuntuNumberAnimation {
39+ target: slider
40+ property: "teaseOffset"
41+ to: units.gu(1)
42+ duration: UbuntuAnimation.SleepyDuration
43+ }
44+ UbuntuNumberAnimation {
45+ target: slider
46+ property: "teaseOffset"
47+ to: 0
48+ duration: UbuntuAnimation.SleepyDuration
49+ }
50+ }
51+
52+ foreground {
53+ children: [
54+ LocalComponents.Slider {
55+ id: slider
56+ anchors {
57+ bottom: parent.bottom
58+ bottomMargin: width / 2 - height / 2
59+ horizontalCenter: parent.horizontalCenter
60+ }
61+ rotation: -90
62+ offset: teaseOffset + dragOffset
63+ active: dragArea.dragging
64+
65+ property real teaseOffset
66+ property real dragOffset: dragArea.useTouchY ? -dragArea.touchY : 0
67+
68+ Behavior on dragOffset {
69+ id: offsetAnimation
70+ UbuntuNumberAnimation {}
71+ }
72+ }
73+ ]
74+ }
75+
76+ EdgeDragArea {
77+ id: dragArea
78+ direction: Direction.Upwards
79+ anchors {
80+ bottom: parent.bottom
81+ left: parent.left
82+ right: parent.right
83+ }
84+
85+ property bool useTouchY
86+
87+ onDraggingChanged: {
88+ if (!dragging) {
89+ if (slider.percent >= 0.85) {
90+ root.hide();
91+ } else if (slider.percent >= 0.15) {
92+ root.showError();
93+ }
94+ }
95+
96+ // We use a separate vars here rather than just directly looking at
97+ // 'dragging' because we want to preserve our 'slider.offset'
98+ // value during the above percent check. Now that we made it,
99+ // we can have 'slider.offset' go back to zero.
100+ offsetAnimation.enabled = !dragging;
101+ useTouchY = dragging;
102+ }
103+ }
104+}
qml/Tutorial/TutorialBottomFinish.qmlnew file+41
@@ -0,0 +1,41 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.3
18+import Ubuntu.Components 1.1
19+import "." as LocalComponents
20+
21+TutorialPage {
22+ id: root
23+
24+ title: i18n.tr("This action does different things for different apps")
25+ text: i18n.tr("Tap here to finish.")
26+ fullTextWidth: true
27+
28+ foreground {
29+ children: [
30+ LocalComponents.Tick {
31+ objectName: "tick"
32+ anchors {
33+ horizontalCenter: parent.horizontalCenter
34+ top: parent.top
35+ topMargin: root.textBottom + units.gu(3)
36+ }
37+ onClicked: root.hide()
38+ }
39+ ]
40+ }
41+}
qml/Tutorial/TutorialContent.qmlnew file+124
@@ -0,0 +1,124 @@
1+/*
2+ * Copyright (C) 2013,2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.3
18+import Ubuntu.Components 1.1
19+
20+Item {
21+ id: root
22+
23+ property Item launcher
24+ property Item panel
25+ property Item stages
26+ property Item overlay
27+
28+ readonly property bool launcherEnabled: !running ||
29+ (!paused && tutorialLeft.shown)
30+ readonly property bool spreadEnabled: !running
31+ readonly property bool panelEnabled: !running
32+ readonly property bool panelContentEnabled: !running
33+ readonly property alias running: d.running
34+
35+ property bool paused: false
36+ property real edgeSize
37+
38+ signal finished()
39+
40+ function finish() {
41+ d.stop();
42+ finished();
43+ }
44+
45+ ////
46+
47+ Component.onCompleted: {
48+ d.start();
49+ }
50+
51+ QtObject {
52+ id: d
53+
54+ property bool running
55+
56+ function stop() {
57+ running = false;
58+ }
59+
60+ function start() {
61+ running = true;
62+ tutorialLeft.show();
63+ }
64+ }
65+
66+ TutorialLeft {
67+ id: tutorialLeft
68+ objectName: "tutorialLeft"
69+ parent: root.stages
70+ anchors.fill: parent
71+ launcher: root.launcher
72+ paused: !shown || root.paused
73+
74+ onFinished: tutorialLeftFinish.show()
75+ }
76+
77+ TutorialLeftFinish {
78+ id: tutorialLeftFinish
79+ objectName: "tutorialLeftFinish"
80+ parent: root.stages
81+ anchors.fill: parent
82+ textXOffset: root.launcher.panelWidth
83+ paused: !shown || root.paused
84+
85+ onFinished: {
86+ root.launcher.hide();
87+ tutorialRight.show();
88+ }
89+ }
90+
91+ TutorialRight {
92+ id: tutorialRight
93+ objectName: "tutorialRight"
94+ parent: root.stages
95+ anchors.fill: parent
96+ edgeSize: root.edgeSize
97+ panel: root.panel
98+ paused: !shown || root.paused
99+
100+ onFinished: tutorialBottom.show()
101+ }
102+
103+ TutorialBottom {
104+ id: tutorialBottom
105+ objectName: "tutorialBottom"
106+ parent: root.stages
107+ anchors.fill: parent
108+ edgeSize: root.edgeSize
109+ paused: !shown || root.paused
110+
111+ onFinished: tutorialBottomFinish.show()
112+ }
113+
114+ TutorialBottomFinish {
115+ id: tutorialBottomFinish
116+ objectName: "tutorialBottomFinish"
117+ parent: root.stages
118+ anchors.fill: parent
119+ backgroundFadesOut: true
120+ paused: !shown || root.paused
121+
122+ onFinished: root.finish()
123+ }
124+}
qml/Tutorial/TutorialLeft.qmlnew file+91
@@ -0,0 +1,91 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.3
18+import Ubuntu.Components 1.1
19+import "." as LocalComponents
20+
21+TutorialPage {
22+ id: root
23+
24+ property var launcher
25+
26+ title: i18n.tr("Open the launcher")
27+ text: i18n.tr("Short swipe from the left edge.")
28+
29+ textXOffset: root.launcher.x + root.launcher.visibleWidth
30+
31+ Connections {
32+ target: root.launcher
33+
34+ onStateChanged: {
35+ if (root.launcher.state === "visible") {
36+ finishTimer.start();
37+ }
38+ }
39+
40+ onDash: {
41+ finishTimer.stop();
42+ root.showError();
43+ root.launcher.hide();
44+ }
45+ }
46+
47+ SequentialAnimation {
48+ id: teaseAnimation
49+ paused: running && root.paused
50+ running: !slider.active && root.launcher.visibleWidth === 0 && root.shown
51+ loops: Animation.Infinite
52+
53+ UbuntuNumberAnimation {
54+ target: root.launcher
55+ property: "x"
56+ to: units.gu(2)
57+ duration: UbuntuAnimation.SleepyDuration
58+ }
59+ UbuntuNumberAnimation {
60+ target: root.launcher
61+ property: "x"
62+ to: 0
63+ duration: UbuntuAnimation.SleepyDuration
64+ }
65+ }
66+
67+ Timer {
68+ id: finishTimer
69+ interval: 1
70+ onTriggered: {
71+ root.hide();
72+ root.launcher.x = 0; // make sure to reset launcher before we go
73+ }
74+ }
75+
76+ foreground {
77+ children: [
78+ LocalComponents.Slider {
79+ id: slider
80+ anchors {
81+ left: parent.left
82+ top: parent.top
83+ topMargin: root.textBottom + units.gu(3)
84+ }
85+ offset: root.launcher.x + root.launcher.visibleWidth + root.launcher.progress
86+ active: root.launcher.dragging
87+ shortSwipe: true
88+ }
89+ ]
90+ }
91+}
qml/Tutorial/TutorialLeftFinish.qmlnew file+41
@@ -0,0 +1,41 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.3
18+import Ubuntu.Components 1.1
19+import "." as LocalComponents
20+
21+TutorialPage {
22+ id: root
23+
24+ title: i18n.tr("These are the shortcuts to favorite apps")
25+ text: i18n.tr("Tap here to continue.")
26+ fullTextWidth: true
27+
28+ foreground {
29+ children: [
30+ LocalComponents.Tick {
31+ objectName: "tick"
32+ anchors {
33+ horizontalCenter: parent.horizontalCenter
34+ top: parent.top
35+ topMargin: root.textBottom + units.gu(3)
36+ }
37+ onClicked: root.hide()
38+ }
39+ ]
40+ }
41+}
qml/Tutorial/TutorialPage.qmlnew file+245
@@ -0,0 +1,245 @@
1+/*
2+ * Copyright (C) 2013,2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.3
18+import Ubuntu.Components 1.1
19+import "../Components"
20+
21+Showable {
22+ id: root
23+
24+ // This is the header displayed, like "Right edge"
25+ property alias title: titleLabel.text
26+
27+ // This is the block of text displayed below the header
28+ property alias text: textLabel.text
29+
30+ // Whether animations are paused
31+ property bool paused
32+
33+ // Whether to give the text the full width that the title has
34+ property bool fullTextWidth
35+
36+ // Whether whole page (background + foreground) or just the foreground fades in
37+ property bool backgroundFadesIn: false
38+
39+ // Whether whole page (background + foreground) or just the foreground fades out
40+ property bool backgroundFadesOut: false
41+
42+ // The foreground Item, add children to it that you want to fade in
43+ property alias foreground: foregroundExtra
44+
45+ // The text label bottom, so you can position elements relative to it
46+ readonly property real textBottom: Math.max(textLabel.y + textLabel.height, errorTextLabel.y + errorTextLabel.height)
47+
48+ // The MouseArea that eats events (so you can adjust size as you will)
49+ property alias mouseArea: mouseArea
50+
51+ // X/Y offsets for text
52+ property real textXOffset: 0
53+ property real textYOffset: 0
54+
55+ // Foreground text opacity
56+ property real textOpacity: 1
57+
58+ signal finished()
59+
60+ function showError() {
61+ errorTimer.start();
62+ }
63+
64+ ////
65+
66+ visible: false
67+ shown: false
68+
69+ property real _foregroundHideOpacity
70+
71+ showAnimation: StandardAnimation {
72+ property: root.backgroundFadesIn ? "opacity" : "_foregroundHideOpacity"
73+ from: 0
74+ to: 1
75+ duration: root.backgroundFadesIn ? UbuntuAnimation.SleepyDuration : UbuntuAnimation.BriskDuration
76+ onStarted: root.visible = true
77+ }
78+
79+ hideAnimation: StandardAnimation {
80+ property: root.backgroundFadesOut ? "opacity" : "_foregroundHideOpacity"
81+ to: 0
82+ duration: UbuntuAnimation.BriskDuration
83+ onStopped: {
84+ root.visible = false;
85+ root.finished();
86+ }
87+ }
88+
89+ QtObject {
90+ id: d
91+
92+ readonly property real sideMargin: units.gu(5.5)
93+ readonly property real verticalOffset: -units.gu(9)
94+ readonly property real textXOffset: Math.max(0, root.textXOffset - sideMargin + units.gu(2))
95+
96+ property real fadeInOffset: {
97+ if (showAnimation.running) {
98+ var opacity = root[root.showAnimation.property]
99+ return (1 - opacity) * units.gu(3);
100+ } else {
101+ return 0;
102+ }
103+ }
104+ }
105+
106+ Timer {
107+ id: errorTimer
108+ interval: 3500
109+ }
110+
111+ MouseArea { // eat any errant presses
112+ id: mouseArea
113+ anchors.fill: parent
114+ }
115+
116+ Rectangle {
117+ anchors.fill: parent
118+ color: "black"
119+ opacity: 0.82
120+ }
121+
122+ Item {
123+ id: foreground
124+ anchors.fill: parent
125+ opacity: root._foregroundHideOpacity
126+
127+ Item {
128+ anchors.fill: parent
129+ opacity: root.textOpacity
130+
131+ Label {
132+ id: titleLabel
133+ anchors {
134+ top: parent.verticalCenter
135+ topMargin: d.verticalOffset + root.textYOffset
136+ left: parent.left
137+ leftMargin: d.sideMargin + d.textXOffset
138+ }
139+ width: parent.width - d.sideMargin * 2
140+ horizontalAlignment: Text.AlignLeft
141+ wrapMode: Text.Wrap
142+ font.weight: Font.Light
143+ font.pixelSize: units.gu(3.5)
144+ }
145+
146+ Label {
147+ id: textLabel
148+ anchors {
149+ top: titleLabel.bottom
150+ topMargin: units.gu(2)
151+ left: parent.left
152+ leftMargin: d.sideMargin + d.textXOffset
153+ }
154+ width: (parent.width - d.sideMargin * 2) * (fullTextWidth ? 1 : 0.66)
155+ horizontalAlignment: Text.AlignLeft
156+ wrapMode: Text.Wrap
157+ font.weight: Font.Light
158+ font.pixelSize: units.gu(2.5)
159+ }
160+
161+ // We use two separate labels like this rather than just changing
162+ // the text of the above labels because we want to know where to place
163+ // sliders (via root.textBottom) without having that place change
164+ // as the text changes length.
165+ Label {
166+ id: errorTitleLabel
167+ objectName: "errorTitleLabel"
168+ anchors {
169+ top: titleLabel.top
170+ left: titleLabel.left
171+ }
172+ width: titleLabel.width
173+ horizontalAlignment: titleLabel.horizontalAlignment
174+ wrapMode: titleLabel.wrapMode
175+ font.weight: titleLabel.font.weight
176+ font.pixelSize: titleLabel.font.pixelSize
177+ opacity: 0
178+ text: i18n.tr("You almost got it!")
179+ }
180+
181+ Label {
182+ id: errorTextLabel
183+ objectName: "errorTextLabel"
184+ anchors {
185+ top: errorTitleLabel.bottom
186+ topMargin: textLabel.anchors.topMargin
187+ left: textLabel.left
188+ }
189+ width: textLabel.width
190+ horizontalAlignment: textLabel.horizontalAlignment
191+ wrapMode: textLabel.wrapMode
192+ font.weight: textLabel.font.weight
193+ font.pixelSize: textLabel.font.pixelSize
194+ opacity: 0
195+ text: i18n.tr("Try again.")
196+ }
197+ }
198+
199+ // A place for subclasses to add extra widgets
200+ Item {
201+ id: foregroundExtra
202+ anchors.fill: parent
203+ }
204+ }
205+
206+ states: State {
207+ name: "errorState"
208+ when: errorTimer.running
209+ PropertyChanges { target: titleLabel; opacity: 0 }
210+ PropertyChanges { target: textLabel; opacity: 0 }
211+ PropertyChanges { target: errorTitleLabel; opacity: 1 }
212+ PropertyChanges { target: errorTextLabel; opacity: 1 }
213+ }
214+
215+ transitions: Transition {
216+ to: "errorState"
217+ reversible: true
218+ SequentialAnimation {
219+ ParallelAnimation {
220+ StandardAnimation {
221+ target: titleLabel
222+ property: "opacity"
223+ duration: UbuntuAnimation.BriskDuration
224+ }
225+ StandardAnimation {
226+ target: textLabel
227+ property: "opacity"
228+ duration: UbuntuAnimation.BriskDuration
229+ }
230+ }
231+ ParallelAnimation {
232+ StandardAnimation {
233+ target: errorTitleLabel
234+ property: "opacity"
235+ duration: UbuntuAnimation.BriskDuration
236+ }
237+ StandardAnimation {
238+ target: errorTextLabel
239+ property: "opacity"
240+ duration: UbuntuAnimation.BriskDuration
241+ }
242+ }
243+ }
244+ }
245+}
qml/Tutorial/TutorialRight.qmlnew file+223
@@ -0,0 +1,223 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.3
18+import Ubuntu.Components 1.1
19+import Ubuntu.Gestures 0.1
20+import Unity.Application 0.1
21+import "../Components"
22+import "../Stages"
23+import "." as LocalComponents
24+
25+TutorialPage {
26+ id: root
27+
28+ property var panel
29+ property alias edgeSize: stage.dragAreaWidth
30+
31+ title: i18n.tr("To view open apps")
32+ text: i18n.tr("Long swipe from the right edge.")
33+
34+ textOpacity: 1 - slider.percent
35+
36+ SequentialAnimation {
37+ id: teaseAnimation
38+ paused: running && root.paused
39+ running: !stage.dragging && stage.dragProgress === 0
40+ loops: Animation.Infinite
41+
42+ UbuntuNumberAnimation {
43+ target: stage
44+ property: "x"
45+ to: -stage.dragAreaOverlap
46+ duration: UbuntuAnimation.SleepyDuration
47+ }
48+ UbuntuNumberAnimation {
49+ target: stage
50+ property: "x"
51+ to: 0
52+ duration: UbuntuAnimation.SleepyDuration
53+ }
54+ }
55+
56+ foreground {
57+ children: [
58+ LocalComponents.Slider {
59+ id: slider
60+ anchors {
61+ right: parent.right
62+ top: parent.top
63+ topMargin: root.textBottom + units.gu(3)
64+ }
65+ rotation: 180
66+ offset: stage.dragProgress - stage.x
67+ active: stage.dragging
68+ },
69+
70+ // Just assume PhoneStage for now. The tablet version of the right-edge
71+ // tutorial is still being spec'd by the design team.
72+ PhoneStage {
73+ id: stage
74+ objectName: "stage"
75+ anchors.top: parent.top
76+ width: parent.width
77+ height: parent.height
78+ applicationManager: fakeAppManager
79+ color: "transparent"
80+ interactive: false
81+ altTabEnabled: false
82+ focusFirstApp: false
83+ startScale: 0.8
84+ endScale: 0.6
85+ dragAreaOverlap: units.gu(2)
86+
87+ onOpened: {
88+ overlay.show();
89+ root.textOpacity = 0;
90+ slider.visible = false;
91+ }
92+
93+ onDraggingChanged: {
94+ if (!dragging) {
95+ if (!overlay.shown) {
96+ root.showError();
97+ }
98+ teaseAnimation.complete();
99+ }
100+ }
101+ },
102+
103+ Showable {
104+ id: overlay
105+ objectName: "overlay"
106+ anchors.fill: parent
107+
108+ opacity: 0
109+ shown: false
110+ showAnimation: UbuntuNumberAnimation { property: "opacity"; to: 1 }
111+
112+ Label {
113+ anchors.top: parent.top
114+ anchors.topMargin: root.panel.panelHeight + units.gu(2)
115+ anchors.left: parent.left
116+ anchors.leftMargin: units.gu(2)
117+ anchors.right: parent.right
118+ anchors.rightMargin: units.gu(2)
119+ wrapMode: Text.Wrap
120+ horizontalAlignment: Text.AlignHCenter
121+ fontSize: "large"
122+ text: i18n.tr("View all your running tasks.")
123+ }
124+
125+ LocalComponents.Tick {
126+ objectName: "tick"
127+ anchors.bottom: bottomOverlayText.top
128+ anchors.bottomMargin: units.gu(1)
129+ anchors.horizontalCenter: bottomOverlayText.horizontalCenter
130+ onClicked: root.hide()
131+ }
132+
133+ Label {
134+ id: bottomOverlayText
135+ anchors.bottom: parent.bottom
136+ anchors.bottomMargin: units.gu(2)
137+ anchors.left: parent.left
138+ anchors.leftMargin: units.gu(2)
139+ anchors.right: parent.right
140+ anchors.rightMargin: units.gu(2)
141+ wrapMode: Text.Wrap
142+ horizontalAlignment: Text.AlignHCenter
143+ fontSize: "small"
144+ text: i18n.tr("Tap here to continue.")
145+ }
146+ }
147+ ]
148+ }
149+
150+ ListModel {
151+ id: fakeAppManager
152+
153+ readonly property string focusedApplicationId: "facebook"
154+
155+ function focusApplication(appId) {}
156+ function requestFocusApplication(appId) {}
157+ function findApplication(appId) {return null;}
158+
159+ signal applicationAdded(string appId)
160+ signal applicationRemoved(string appId)
161+ signal focusRequested(string appId)
162+
163+ ListElement {
164+ appId: "facebook"
165+ fullscreen: false
166+ name: ""
167+ icon: ""
168+ state: ApplicationInfoInterface.Stopped
169+ splashTitle: ""
170+ splashImage: ""
171+ splashShowHeader: false
172+ splashColor: "transparent"
173+ splashColorHeader: "transparent"
174+ splashColorFooter: "transparent"
175+ defaultScreenshot: "../Tutorial/graphics/facebook.png"
176+ }
177+
178+ ListElement {
179+ appId: "camera"
180+ fullscreen: false
181+ name: ""
182+ icon: ""
183+ state: ApplicationInfoInterface.Stopped
184+ splashTitle: ""
185+ splashImage: ""
186+ splashShowHeader: false
187+ splashColor: "transparent"
188+ splashColorHeader: "transparent"
189+ splashColorFooter: "transparent"
190+ defaultScreenshot: "../Tutorial/graphics/camera.png"
191+ }
192+
193+ ListElement {
194+ appId: "gallery"
195+ fullscreen: false
196+ name: ""
197+ icon: ""
198+ state: ApplicationInfoInterface.Stopped
199+ splashTitle: ""
200+ splashImage: ""
201+ splashShowHeader: false
202+ splashColor: "transparent"
203+ splashColorHeader: "transparent"
204+ splashColorFooter: "transparent"
205+ defaultScreenshot: "../Tutorial/graphics/gallery.png"
206+ }
207+
208+ ListElement {
209+ appId: "dialer"
210+ fullscreen: false
211+ name: ""
212+ icon: ""
213+ state: ApplicationInfoInterface.Stopped
214+ splashTitle: ""
215+ splashImage: ""
216+ splashShowHeader: false
217+ splashColor: "transparent"
218+ splashColorHeader: "transparent"
219+ splashColorFooter: "transparent"
220+ defaultScreenshot: "../Tutorial/graphics/dialer.png"
221+ }
222+ }
223+}
qml/Tutorial/graphics/camera.pngnew file

Binary file not shown.

qml/Tutorial/graphics/chevron.pngnew file

Binary file not shown.

qml/Tutorial/graphics/dialer.pngnew file

Binary file not shown.

qml/Tutorial/graphics/facebook.pngnew file

Binary file not shown.

qml/Tutorial/graphics/gallery.pngnew file

Binary file not shown.

qml/Tutorial/graphics/tick.pngnew file

Binary file not shown.

tests/autopilot/unity8/shell/emulators/edges_demo.pydeleted−140
@@ -1,140 +0,0 @@
1−# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2−#
3−# Unity Autopilot Test Suite
4−# Copyright (C) 2014 Canonical
5−#
6−# This program is free software: you can redistribute it and/or modify
7−# it under the terms of the GNU General Public License as published by
8−# the Free Software Foundation, either version 3 of the License, or
9−# (at your option) any later version.
10−#
11−# This program is distributed in the hope that it will be useful,
12−# but WITHOUT ANY WARRANTY; without even the implied warranty of
13−# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14−# GNU General Public License for more details.
15−#
16−# You should have received a copy of the GNU General Public License
17−# along with this program. If not, see <http://www.gnu.org/licenses/>.
18−#
19−
20−import logging
21−import time
22−
23−import ubuntuuitoolkit
24−
25−import autopilot
26−from autopilot import introspection
27−
28−
29−logger = logging.getLogger(__name__)
30−
31−
32−class RightEdgeDemoOverlay(
33− ubuntuuitoolkit.UbuntuUIToolkitCustomProxyObjectBase):
34−
35− @classmethod
36− def validate_dbus_object(cls, path, state):
37− name = introspection.get_classname_from_path(path)
38− if name == b'EdgeDemoOverlay':
39− if state['edge'][1] == 'right':
40− return True
41− return False
42−
43− @autopilot.logging.log_action(logger.info)
44− def swipe(self):
45− """Swipe to the left to complete this demo step."""
46− x, y, width, height = self.globalRect
47− start_x = x + width
48− stop_x = x
49− start_y = stop_y = y + height // 2
50− self.pointing_device.drag(start_x, start_y, stop_x, stop_y)
51− return self.get_root_instance().wait_select_single(
52− edge='top', active=True)
53−
54−
55−class TopEdgeDemoOverlay(
56− ubuntuuitoolkit.UbuntuUIToolkitCustomProxyObjectBase):
57−
58− @classmethod
59− def validate_dbus_object(cls, path, state):
60− name = introspection.get_classname_from_path(path)
61− if name == b'EdgeDemoOverlay':
62− if state['edge'][1] == 'top':
63− return True
64− return False
65−
66− @autopilot.logging.log_action(logger.info)
67− def swipe(self):
68− """Swipe to the bottom to complete this demo step."""
69− x, y, width, height = self.globalRect
70− start_x = stop_x = x + width // 2
71− start_y = y
72− stop_y = y + height
73− self.pointing_device.drag(start_x, start_y, stop_x, stop_y)
74− return self.get_root_instance().wait_select_single(
75− edge='bottom', active=True)
76−
77−
78−class BottomEdgeDemoOverlay(
79− ubuntuuitoolkit.UbuntuUIToolkitCustomProxyObjectBase):
80−
81− @classmethod
82− def validate_dbus_object(cls, path, state):
83− name = introspection.get_classname_from_path(path)
84− if name == b'EdgeDemoOverlay':
85− if state['edge'][1] == 'bottom':
86− return True
87− return False
88−
89− @autopilot.logging.log_action(logger.info)
90− def swipe(self):
91− """Swipe to the top to complete this demo step."""
92− x, y, width, height = self.globalRect
93− start_x = stop_x = x + width // 2
94− start_y = y + height
95− stop_y = y
96− self.pointing_device.drag(start_x, start_y, stop_x, stop_y)
97− return self.get_root_instance().wait_select_single(
98− edge='left', active=True)
99−
100−
101−class LeftEdgeDemoOverlay(
102− ubuntuuitoolkit.UbuntuUIToolkitCustomProxyObjectBase):
103−
104− @classmethod
105− def validate_dbus_object(cls, path, state):
106− name = introspection.get_classname_from_path(path)
107− if name == b'EdgeDemoOverlay':
108− if state['edge'][1] == 'left':
109− return True
110− return False
111−
112− @autopilot.logging.log_action(logger.info)
113− def swipe(self):
114− """Swipe to the right to complete this demo step."""
115− x, y, width, height = self.globalRect
116− start_x = x
117− stop_x = x + width
118− start_y = stop_y = y + height // 2
119− self.pointing_device.drag(start_x, start_y, stop_x, stop_y)
120− return self.get_root_instance().wait_select_single(
121− edge='none', active=True)
122−
123−
124−class FinalEdgeDemoOverlay(
125− ubuntuuitoolkit.UbuntuUIToolkitCustomProxyObjectBase):
126−
127− @classmethod
128− def validate_dbus_object(cls, path, state):
129− name = introspection.get_classname_from_path(path)
130− if name == b'EdgeDemoOverlay':
131− if state['edge'][1] == 'none':
132− return True
133− return False
134−
135− @autopilot.logging.log_action(logger.info)
136− def tap_to_start(self):
137− """Tap to finish the demo and start using the Ubuntu Touch."""
138− time.sleep(1)
139− self.pointing_device.click_object(self)
140− self.shown.wait_for(False)
tests/autopilot/unity8/shell/emulators/tutorial.pynew file+74
@@ -0,0 +1,74 @@
1+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2+#
3+# Unity Autopilot Test Suite
4+# Copyright (C) 2014 Canonical
5+#
6+# This program is free software: you can redistribute it and/or modify
7+# it under the terms of the GNU General Public License as published by
8+# the Free Software Foundation, either version 3 of the License, or
9+# (at your option) any later version.
10+#
11+# This program is distributed in the hope that it will be useful,
12+# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+# GNU General Public License for more details.
15+#
16+# You should have received a copy of the GNU General Public License
17+# along with this program. If not, see <http://www.gnu.org/licenses/>.
18+#
19+
20+import logging
21+import time
22+
23+import ubuntuuitoolkit
24+
25+import autopilot
26+from autopilot import introspection
27+
28+
29+logger = logging.getLogger(__name__)
30+
31+
32+class TutorialPage(
33+ ubuntuuitoolkit.UbuntuUIToolkitCustomProxyObjectBase):
34+
35+ @classmethod
36+ def validate_dbus_object(cls, path, state):
37+ name = introspection.get_classname_from_path(path)
38+ return name in (b'TutorialPage', b'TutorialLeft',
39+ b'TutorialLeftFinish', b'TutorialRight',
40+ b'TutorialBottom', b'TutorialBottomFinish')
41+
42+ @autopilot.logging.log_action(logger.info)
43+ def short_swipe_right(self):
44+ self.shown.wait_for(True)
45+ x, y, width, height = self.globalRect
46+ start_x = x
47+ stop_x = x + width // 3
48+ start_y = stop_y = y + height // 2
49+ self.pointing_device.drag(start_x, start_y, stop_x, stop_y)
50+
51+ @autopilot.logging.log_action(logger.info)
52+ def swipe_left(self):
53+ self.shown.wait_for(True)
54+ x, y, width, height = self.globalRect
55+ start_x = width
56+ stop_x = x
57+ start_y = stop_y = y + height // 2
58+ self.pointing_device.drag(start_x, start_y, stop_x, stop_y)
59+
60+ @autopilot.logging.log_action(logger.info)
61+ def swipe_up(self):
62+ self.shown.wait_for(True)
63+ x, y, width, height = self.globalRect
64+ start_y = height
65+ stop_y = y
66+ start_x = stop_x = x + width // 2
67+ self.pointing_device.drag(start_x, start_y, stop_x, stop_y)
68+
69+ @autopilot.logging.log_action(logger.info)
70+ def tap(self):
71+ """Tap the tick button to complete this step."""
72+ self.shown.wait_for(True)
73+ button = self.wait_select_single(objectName="tick")
74+ self.pointing_device.click_object(button)
tests/autopilot/unity8/shell/fixture_setup.py+6−6
@@ -54,7 +54,7 @@ class FakeScopes(fixtures.Fixture):
5454 return ld_library_path
5555
5656
57−class EdgesDemo(fixtures.Fixture):
57+class Tutorial(fixtures.Fixture):
5858
5959 def __init__(self, enable):
6060 super().__init__()
@@ -62,12 +62,12 @@ class EdgesDemo(fixtures.Fixture):
6262
6363 def setUp(self):
6464 super().setUp()
65− original_state = self._is_edges_demo_enabled()
65+ original_state = self._is_tutorial_enabled()
6666 if self.enable != original_state:
67− self.addCleanup(self._set_edges_demo, original_state)
68− self._set_edges_demo(self.enable)
67+ self.addCleanup(self._set_tutorial, original_state)
68+ self._set_tutorial(self.enable)
6969
70− def _is_edges_demo_enabled(self):
70+ def _is_tutorial_enabled(self):
7171 command = [
7272 'dbus-send', '--system', '--print-reply',
7373 '--dest=org.freedesktop.Accounts',
@@ -79,7 +79,7 @@ class EdgesDemo(fixtures.Fixture):
7979 output = subprocess.check_output(command, universal_newlines=True)
8080 return True if output.count('true') else False
8181
82− def _set_edges_demo(self, value):
82+ def _set_tutorial(self, value):
8383 value_string = 'true' if value else 'false'
8484 command = [
8585 'dbus-send', '--system', '--print-reply',
tests/autopilot/unity8/shell/tests/test_edges_demo.pydeleted−51
@@ -1,51 +0,0 @@
1−# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2−#
3−# Unity Autopilot Test Suite
4−# Copyright (C) 2014 Canonical
5−#
6−# This program is free software: you can redistribute it and/or modify
7−# it under the terms of the GNU General Public License as published by
8−# the Free Software Foundation, either version 3 of the License, or
9−# (at your option) any later version.
10−#
11−# This program is distributed in the hope that it will be useful,
12−# but WITHOUT ANY WARRANTY; without even the implied warranty of
13−# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14−# GNU General Public License for more details.
15−#
16−# You should have received a copy of the GNU General Public License
17−# along with this program. If not, see <http://www.gnu.org/licenses/>.
18−#
19−
20−from autopilot.matchers import Eventually
21−from testtools.matchers import Equals
22−
23−from unity8.shell import (
24− fixture_setup,
25− tests
26−)
27−# unused import to load the edge emulators custom proxy objects.
28−from unity8.shell.emulators import edges_demo # NOQA
29−
30−
31−class EdgesDemoTestCase(tests.UnityTestCase):
32−
33− def setUp(self):
34− super(EdgesDemoTestCase, self).setUp()
35− self._qml_mock_enabled = False
36− self._data_dirs_mock_enabled = False
37−
38− self.useFixture(fixture_setup.EdgesDemo(True))
39− self.unity = self.launch_unity()
40−
41− def test_complete_edge_demo(self):
42− edge_demo = self.unity.select_single('EdgeDemo')
43− self.assertThat(edge_demo.running, Eventually(Equals(True)))
44− right_edge_overlay = self.unity.wait_select_single(
45− edge='right', active=True)
46− top_edge_overlay = right_edge_overlay.swipe()
47− bottom_edge_overlay = top_edge_overlay.swipe()
48− left_edge_overlay = bottom_edge_overlay.swipe()
49− final_overlay = left_edge_overlay.swipe()
50− final_overlay.tap_to_start()
51− self.assertThat(edge_demo.running, Eventually(Equals(False)))
tests/autopilot/unity8/shell/tests/test_tutorial.pynew file+57
@@ -0,0 +1,57 @@
1+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2+#
3+# Unity Autopilot Test Suite
4+# Copyright (C) 2014 Canonical
5+#
6+# This program is free software: you can redistribute it and/or modify
7+# it under the terms of the GNU General Public License as published by
8+# the Free Software Foundation, either version 3 of the License, or
9+# (at your option) any later version.
10+#
11+# This program is distributed in the hope that it will be useful,
12+# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+# GNU General Public License for more details.
15+#
16+# You should have received a copy of the GNU General Public License
17+# along with this program. If not, see <http://www.gnu.org/licenses/>.
18+#
19+
20+from autopilot.matchers import Eventually
21+from testtools.matchers import Equals
22+
23+from unity8.shell import (
24+ fixture_setup,
25+ tests
26+)
27+# unused import to load the tutorial emulators custom proxy objects.
28+from unity8.shell.emulators import tutorial # NOQA
29+
30+
31+class TutorialTestCase(tests.UnityTestCase):
32+
33+ def setUp(self):
34+ super(TutorialTestCase, self).setUp()
35+ self._qml_mock_enabled = False
36+ self._data_dirs_mock_enabled = False
37+
38+ self.useFixture(fixture_setup.Tutorial(True))
39+ self.unity = self.launch_unity()
40+
41+ def test_complete_tutorial(self):
42+ greeter = self.main_window.get_greeter()
43+ tutorial = self.unity.select_single('Tutorial')
44+ self.assertThat(tutorial.running, Eventually(Equals(True)))
45+ greeter.swipe()
46+ page = self.unity.wait_select_single(objectName='tutorialLeft')
47+ page.short_swipe_right()
48+ page = self.unity.wait_select_single(objectName='tutorialLeftFinish')
49+ page.tap()
50+ page = self.unity.wait_select_single(objectName='tutorialRight')
51+ page.swipe_left()
52+ page.tap()
53+ page = self.unity.wait_select_single(objectName='tutorialBottom')
54+ page.swipe_up()
55+ page = self.unity.wait_select_single(objectName='tutorialBottomFinish')
56+ page.tap()
57+ self.assertThat(tutorial.running, Eventually(Equals(False)))
tests/mocks/Unity/Application/MirSurfaceItem.qml+16−1
@@ -44,9 +44,24 @@ Rectangle {
4444 }
4545
4646 Text {
47+ text: surfaceText.text
48+ color: "black"
49+ font: surfaceText.font
50+ fontSizeMode: Text.Fit
51+ minimumPixelSize: 10
52+ verticalAlignment: Text.AlignVCenter
53+ x: surfaceText.x
54+ y: surfaceText.y
55+ width: surfaceText.width
56+ height: surfaceText.height
57+
58+ transform: Translate { x: -2; y: -2 }
59+ }
60+ Text {
61+ id: surfaceText
4762 anchors.fill: parent
4863 text: "SURFACE"
49− color: "yellow"
64+ color: root.parent && root.parent.activeFocus ? "yellow" : "blue"
5065 font.bold: true
5166 fontSizeMode: Text.Fit
5267 minimumPixelSize: 10; font.pixelSize: 200
tests/mocks/Unity/Indicators/fakeindicatorsmodel.cpp+13−1
@@ -21,7 +21,8 @@
2121#include "indicators.h"
2222
2323FakeIndicatorsModel::FakeIndicatorsModel(QObject *parent)
24− : QAbstractListModel(parent)
24+ : QAbstractListModel(parent),
25+ m_profile("phone")
2526{
2627 QObject::connect(this, SIGNAL(rowsInserted(const QModelIndex &, int, int)), this, SIGNAL(countChanged()));
2728 QObject::connect(this, SIGNAL(rowsRemoved(const QModelIndex &, int, int)), this, SIGNAL(countChanged()));
@@ -38,6 +39,17 @@ int FakeIndicatorsModel::count() const
3839 return rowCount();
3940}
4041
42+QString FakeIndicatorsModel::profile() const
43+{
44+ return m_profile;
45+}
46+
47+void FakeIndicatorsModel::setProfile(const QString& profile)
48+{
49+ m_profile = profile;
50+ Q_EMIT profileChanged();
51+}
52+
4153void FakeIndicatorsModel::load(const QString&)
4254{
4355}
tests/mocks/Unity/Indicators/fakeindicatorsmodel.h+6
@@ -27,7 +27,9 @@ class FakeIndicatorsModel : public QAbstractListModel
2727 Q_OBJECT
2828 Q_ENUMS(Roles)
2929 Q_PROPERTY(int count READ count NOTIFY countChanged)
30+ Q_PROPERTY(QString profile READ profile WRITE setProfile NOTIFY profileChanged)
3031 Q_PROPERTY(QVariant modelData READ modelData WRITE setModelData NOTIFY modelDataChanged)
32+
3133public:
3234
3335 FakeIndicatorsModel(QObject *parent=0);
@@ -42,6 +44,8 @@ public:
4244
4345 Q_INVOKABLE QVariant data(int row, int role) const;
4446
47+ QString profile() const;
48+ void setProfile(const QString& profile);
4549 void setModelData(const QVariant& data);
4650 QVariant modelData() const { return m_modelData; }
4751
@@ -54,11 +58,13 @@ public:
5458
5559Q_SIGNALS:
5660 void countChanged();
61+ void profileChanged();
5762 void modelDataChanged();
5863
5964private:
6065 int count() const;
6166
67+ QString m_profile;
6268 QVariant m_modelData;
6369};
6470
tests/mocks/Unity/Launcher/CMakeLists.txt+1−1
@@ -1,4 +1,4 @@
1−pkg_check_modules(LAUNCHER_API REQUIRED unity-shell-launcher=5)
1+pkg_check_modules(LAUNCHER_API REQUIRED unity-shell-launcher=6)
22
33include_directories(
44 ${CMAKE_CURRENT_SOURCE_DIR}
tests/mocks/Unity/Launcher/MockLauncherModel.cpp+10
@@ -209,6 +209,16 @@ void MockLauncherModel::setApplicationManager(unity::shell::application::Applica
209209 Q_UNUSED(applicationManager)
210210}
211211
212+bool MockLauncherModel::onlyPinned() const
213+{
214+ return false;
215+}
216+
217+void MockLauncherModel::setOnlyPinned(bool onlyPinned)
218+{
219+ Q_UNUSED(onlyPinned)
220+}
221+
212222void MockLauncherModel::emitHint()
213223{
214224 Q_EMIT hint();
tests/mocks/Unity/Launcher/MockLauncherModel.h+3
@@ -49,6 +49,9 @@ public:
4949 unity::shell::application::ApplicationManagerInterface *applicationManager() const;
5050 void setApplicationManager(unity::shell::application::ApplicationManagerInterface *applicationManager);
5151
52+ bool onlyPinned() const override;
53+ void setOnlyPinned(bool onlyPinned) override;
54+
5255 // For testing
5356 Q_INVOKABLE void emitHint();
5457
tests/mocks/Unity/Notifications/CMakeLists.txt+2−1
@@ -4,7 +4,8 @@ include_directories(
44
55set(MockNotificationsPlugin_SOURCES
66 plugin.cpp
7− MockNotificationTypes.cpp
7+ MockNotification.cpp
8+ MockNotificationModel.cpp
89 MockActionModel.cpp
910)
1011
tests/mocks/Unity/Notifications/MockActionModel.cpp+6−4
@@ -1,5 +1,5 @@
11/*
2− * Copyright 2014 Canonical Ltd.
2+ * Copyright 2015 Canonical Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU Lesser General Public License as published by
@@ -25,8 +25,6 @@ struct ActionModelPrivate {
2525};
2626
2727ActionModel::ActionModel(QObject *parent) : QStringListModel(parent), p(new ActionModelPrivate) {
28− insertAction("ok_id", "Ok");
29− insertAction("cancel_id", "Cancel");
3028}
3129
3230ActionModel::~ActionModel() {
@@ -66,7 +64,11 @@ Q_INVOKABLE QVariant ActionModel::data(int row, int role) const
6664 return data(index(row, 0), role);
6765}
6866
69−void ActionModel::insertAction(const QString &id, const QString &label) {
67+void ActionModel::append(const QString &id, const QString &label) {
7068 p->ids.push_back(id);
7169 p->labels.push_back(label);
7270}
71+
72+int ActionModel::getCount() const {
73+ return p->labels.size();
74+}
tests/mocks/Unity/Notifications/MockActionModel.h+4−2
@@ -1,5 +1,5 @@
11/*
2− * Copyright 2014 Canonical Ltd.
2+ * Copyright 2015 Canonical Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU Lesser General Public License as published by
@@ -26,6 +26,7 @@ struct ActionModelPrivate;
2626
2727class ActionModel : public QStringListModel {
2828 Q_OBJECT
29+ Q_PROPERTY(int count READ getCount)
2930
3031public:
3132 ActionModel(QObject *parent=nullptr);
@@ -42,7 +43,8 @@ public:
4243 };
4344 Q_INVOKABLE QVariant data(int row, int role) const;
4445
45− void insertAction(const QString &id, const QString &label);
46+ Q_INVOKABLE void append(const QString &id, const QString &label);
47+ int getCount() const;
4648
4749private:
4850 QScopedPointer<ActionModelPrivate> p;
tests/mocks/Unity/Notifications/MockNotification.cppnew file+178
@@ -0,0 +1,178 @@
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+ * Authors:
17+ * Mirco Mueller <mirco.mueller@canonical.com>
18+ */
19+
20+#include "MockNotification.h"
21+
22+#include <QDebug>
23+
24+struct MockNotificationPrivate {
25+ int id;
26+ QString summary;
27+ QString body;
28+ int value;
29+ MockNotification::Type type;
30+ QString icon;
31+ QString secondaryIcon;
32+ QStringList actions;
33+ ActionModel* actionsModel;
34+ QVariantMap hints;
35+};
36+
37+MockNotification::MockNotification(QObject *parent) : QObject(parent), p(new MockNotificationPrivate()) {
38+ p->actionsModel = new ActionModel();
39+}
40+
41+MockNotification::~MockNotification() {
42+}
43+
44+QString MockNotification::getSummary() const {
45+ return p->summary;
46+}
47+
48+void MockNotification::setSummary(const QString &summary) {
49+ if(p->summary != summary) {
50+ p->summary = summary;
51+ Q_EMIT summaryChanged(p->summary);
52+ Q_EMIT dataChanged(p->id);
53+ }
54+}
55+
56+QString MockNotification::getBody() const {
57+ return p->body;
58+}
59+
60+void MockNotification::setBody(const QString &body) {
61+ if(p->body != body) {
62+ p->body = body;
63+ Q_EMIT bodyChanged(p->body);
64+ Q_EMIT dataChanged(p->id);
65+ }
66+}
67+
68+int MockNotification::getID() const {
69+ return p->id;
70+}
71+
72+void MockNotification::setID(const int id) {
73+ p->id = id;
74+}
75+
76+int MockNotification::getValue() const {
77+ return p->value;
78+}
79+
80+void MockNotification::setValue(int value) {
81+ if(p->value != value) {
82+ p->value = value;
83+ Q_EMIT valueChanged(p->value);
84+ Q_EMIT dataChanged(p->id);
85+ }
86+}
87+
88+QString MockNotification::getIcon() const {
89+ return p->icon;
90+}
91+
92+void MockNotification::setIcon(const QString &icon) {
93+ if (icon.startsWith(" ") || icon.size() == 0) {
94+ p->icon = nullptr;
95+ }
96+ else {
97+ p->icon = icon;
98+
99+ if (icon.indexOf("/") == -1) {
100+ p->icon.prepend("image://theme/");
101+ }
102+ }
103+
104+ Q_EMIT iconChanged(p->icon);
105+ Q_EMIT dataChanged(p->id);
106+}
107+
108+QString MockNotification::getSecondaryIcon() const {
109+ return p->secondaryIcon;
110+}
111+
112+void MockNotification::setSecondaryIcon(const QString &secondaryIcon) {
113+ if (secondaryIcon.startsWith(" ") || secondaryIcon.size() == 0) {
114+ p->secondaryIcon = nullptr;
115+ }
116+ else {
117+ p->secondaryIcon = secondaryIcon;
118+
119+ if (secondaryIcon.indexOf("/") == -1) {
120+ p->secondaryIcon.prepend("image://theme/");
121+ }
122+ }
123+
124+ Q_EMIT secondaryIconChanged(p->secondaryIcon);
125+ Q_EMIT dataChanged(p->id);
126+}
127+
128+MockNotification::Type MockNotification::getType() const {
129+ return p->type;
130+}
131+
132+void MockNotification::setType(Type type) {
133+ if(p->type != type) {
134+ p->type = type;
135+ Q_EMIT typeChanged(p->type);
136+ }
137+}
138+
139+ActionModel* MockNotification::getActions() const {
140+ return p->actionsModel;
141+}
142+
143+void MockNotification::setActions(const QStringList &actions) {
144+ if(p->actions != actions) {
145+ p->actions = actions;
146+ Q_EMIT actionsChanged(p->actions);
147+
148+ for (int i = 0; i < p->actions.size(); i += 2) {
149+ p->actionsModel->append(p->actions[i], p->actions[i+1]);
150+ }
151+ }
152+}
153+
154+QVariantMap MockNotification::getHints() const {
155+ return p->hints;
156+}
157+
158+void MockNotification::setHints(const QVariantMap& hints) {
159+ if (p->hints != hints) {
160+ p->hints = hints;
161+ Q_EMIT hintsChanged(p->hints);
162+ }
163+}
164+
165+void MockNotification::invokeAction(const QString &action) {
166+ for(int i=0; i<p->actions.size(); i++) {
167+ if(p->actions[i] == action) {
168+ Q_EMIT actionInvoked(action);
169+ qDebug() << "Info: invoked action" << action;
170+ return;
171+ }
172+ }
173+ fprintf(stderr, "Error: tried to invoke action not in actionList.\n");
174+}
175+
176+void MockNotification::close() {
177+ Q_EMIT completed(p->id);
178+}
tests/mocks/Unity/Notifications/MockNotification.hnew file+94
@@ -0,0 +1,94 @@
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+ * Authors:
17+ * Mirco Mueller <mirco.mueller@canonical.com>
18+ */
19+
20+#ifndef MOCK_NOTIFICATION_H
21+#define MOCK_NOTIFICATION_H
22+
23+#include "MockActionModel.h"
24+#include <QObject>
25+#include <QString>
26+#include <QStringList>
27+#include <QScopedPointer>
28+
29+struct MockNotificationPrivate;
30+
31+class MockNotification : public QObject {
32+ Q_OBJECT
33+ Q_ENUMS(Type)
34+ Q_PROPERTY(QString summary READ getSummary WRITE setSummary NOTIFY summaryChanged)
35+ Q_PROPERTY(QString body READ getBody WRITE setBody NOTIFY bodyChanged)
36+ Q_PROPERTY(int nid READ getID WRITE setID NOTIFY idChanged)
37+ Q_PROPERTY(int value READ getValue WRITE setValue NOTIFY valueChanged)
38+ Q_PROPERTY(QString icon READ getIcon WRITE setIcon NOTIFY iconChanged)
39+ Q_PROPERTY(QString secondaryIcon READ getSecondaryIcon WRITE setSecondaryIcon NOTIFY secondaryIconChanged)
40+ Q_PROPERTY(Type type READ getType WRITE setType NOTIFY typeChanged)
41+ Q_PROPERTY(QStringList rawActions WRITE setActions)
42+ Q_PROPERTY(ActionModel* actions READ getActions NOTIFY actionsChanged)
43+ Q_PROPERTY(QVariantMap hints READ getHints WRITE setHints NOTIFY hintsChanged)
44+
45+private:
46+ QScopedPointer<MockNotificationPrivate> p;
47+
48+public:
49+ enum Urgency { Low, Normal, Critical };
50+ enum Type { PlaceHolder, Confirmation, Ephemeral, Interactive, SnapDecision };
51+
52+Q_SIGNALS:
53+ void summaryChanged(const QString &summary);
54+ void bodyChanged(const QString &body);
55+ void idChanged(const int id);
56+ void valueChanged(int value);
57+ void iconChanged(const QString &icon);
58+ void secondaryIconChanged(const QString &secondaryIcon);
59+ void typeChanged(Type type);
60+ void actionsChanged(const QStringList &actions);
61+ void hintsChanged(const QVariantMap& hints);
62+
63+ void dataChanged(int nid);
64+ void completed(int nid);
65+ void actionInvoked(const QString &action);
66+
67+public:
68+ MockNotification(QObject *parent=nullptr);
69+ virtual ~MockNotification();
70+
71+ QString getSummary() const;
72+ void setSummary(const QString &summary);
73+ QString getBody() const;
74+ void setBody(const QString &body);
75+ int getID() const;
76+ void setID(const int id);
77+ int getValue() const;
78+ void setValue(int value);
79+ QString getIcon() const;
80+ void setIcon(const QString &icon);
81+ QString getSecondaryIcon() const;
82+ void setSecondaryIcon(const QString &secondaryIcon);
83+ Type getType() const;
84+ void setType(Type type);
85+ ActionModel* getActions() const;
86+ void setActions(const QStringList &actions);
87+ QVariantMap getHints() const;
88+ void setHints(const QVariantMap& hints);
89+
90+ Q_INVOKABLE void invokeAction(const QString &action);
91+ Q_INVOKABLE void close();
92+};
93+
94+#endif // MOCK_NOTIFICATION_H
tests/mocks/Unity/Notifications/MockNotificationModel.cppnew file+172
@@ -0,0 +1,172 @@
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+ * Authors:
17+ * Mirco Mueller <mirco.mueller@canonical.com>
18+ */
19+
20+#include "MockNotificationModel.h"
21+#include "MockNotification.h"
22+
23+#include <unity/shell/notifications/ModelInterface.h>
24+
25+#include <QTimer>
26+#include <QList>
27+#include <QVector>
28+#include <QMap>
29+#include <QStringListModel>
30+#include <QQmlEngine>
31+
32+using namespace unity::shell::notifications;
33+
34+MockNotificationModel::MockNotificationModel(QObject *parent) : QAbstractListModel(parent) {
35+}
36+
37+MockNotificationModel::~MockNotificationModel() {
38+}
39+
40+int MockNotificationModel::rowCount(const QModelIndex &parent) const {
41+ return m_queue.size();
42+}
43+
44+int MockNotificationModel::getCount() const {
45+ return m_queue.size();
46+}
47+
48+QVariant MockNotificationModel::data(const QModelIndex &index, int role) const {
49+ if (!index.isValid())
50+ return QVariant();
51+
52+ switch(role) {
53+ case ModelInterface::RoleType:
54+ return QVariant(m_queue[index.row()]->getType());
55+
56+ case ModelInterface::RoleId:
57+ return QVariant(m_queue[index.row()]->getID());
58+
59+ case ModelInterface::RoleSummary:
60+ return QVariant(m_queue[index.row()]->getSummary());
61+
62+ case ModelInterface::RoleBody:
63+ return QVariant(m_queue[index.row()]->getBody());
64+
65+ case ModelInterface::RoleValue:
66+ return QVariant(m_queue[index.row()]->getValue());
67+
68+ case ModelInterface::RoleIcon:
69+ return QVariant(m_queue[index.row()]->getIcon());
70+
71+ case ModelInterface::RoleSecondaryIcon:
72+ return QVariant(m_queue[index.row()]->getSecondaryIcon());
73+
74+ case ModelInterface::RoleActions:
75+ return QVariant::fromValue(m_queue[index.row()]->getActions());
76+
77+ case ModelInterface::RoleHints:
78+ return QVariant(m_queue[index.row()]->getHints());
79+
80+ case ModelInterface::RoleNotification:
81+ return QVariant::fromValue(m_queue[index.row()]);
82+
83+ default:
84+ return QVariant();
85+ }
86+}
87+
88+void MockNotificationModel::append(MockNotification* n) {
89+ int location = m_queue.size();
90+ QModelIndex insertionPoint = QModelIndex();
91+ beginInsertRows(insertionPoint, location, location);
92+ m_queue.insert(location, n);
93+ endInsertRows();
94+}
95+
96+MockNotification* MockNotificationModel::getNotification(int id) const {
97+ for(int i=0; i < m_queue.size(); i++) {
98+ if(m_queue[i]->getID() == id) {
99+ return m_queue[i];
100+ }
101+ }
102+
103+ return nullptr;
104+}
105+
106+void MockNotificationModel::remove(const int id) {
107+ for(int i = 0; i < m_queue.size(); i++) {
108+ if(m_queue[i]->getID() == id) {
109+ removeInternal(i);
110+ return;
111+ }
112+ }
113+}
114+
115+void MockNotificationModel::removeSecond() {
116+ if(m_queue.size() < 2)
117+ return;
118+ removeInternal(1);
119+}
120+
121+void MockNotificationModel::removeInternal(int loc) {
122+ QModelIndex deletePoint = QModelIndex();
123+ beginRemoveRows(deletePoint, loc, loc);
124+ m_queue.erase(m_queue.begin() + loc);
125+ endRemoveRows();
126+}
127+
128+MockNotification* MockNotificationModel::getRaw(const int notificationId) const {
129+ for(int i = 0; i < m_queue.size(); i++) {
130+ if(m_queue[i]->getID() == notificationId) {
131+ MockNotification* n = m_queue[i];
132+ return n;
133+ }
134+ }
135+
136+ return nullptr;
137+}
138+
139+int MockNotificationModel::queued() const {
140+ return m_queue.size();
141+}
142+
143+QHash<int, QByteArray> MockNotificationModel::roleNames() const {
144+ QHash<int, QByteArray> roles;
145+
146+ roles.insert(ModelInterface::RoleType, "type");
147+ roles.insert(ModelInterface::RoleUrgency, "urgency");
148+ roles.insert(ModelInterface::RoleId, "id");
149+ roles.insert(ModelInterface::RoleSummary, "summary");
150+ roles.insert(ModelInterface::RoleBody, "body");
151+ roles.insert(ModelInterface::RoleValue, "value");
152+ roles.insert(ModelInterface::RoleIcon, "icon");
153+ roles.insert(ModelInterface::RoleSecondaryIcon, "secondaryIcon");
154+ roles.insert(ModelInterface::RoleActions, "actions");
155+ roles.insert(ModelInterface::RoleHints, "hints");
156+ roles.insert(ModelInterface::RoleNotification, "notification");
157+
158+ return roles;
159+}
160+
161+void MockNotificationModel::onCompleted(int id) {
162+ remove(id);
163+}
164+
165+void MockNotificationModel::onDataChanged(int id) {
166+ for(int i = 0; i < m_queue.size(); i++) {
167+ if(m_queue[i]->getID() == id) {
168+ Q_EMIT dataChanged(index(i, 0), index(i, 0));
169+ break;
170+ }
171+ }
172+}
tests/mocks/Unity/Notifications/MockNotificationModel.hnew file+72
@@ -0,0 +1,72 @@
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+ * Authors:
17+ * Mirco Mueller <mirco.mueller@canonical.com>
18+ */
19+
20+#ifndef MOCK_NOTIFICATION_MODEL_H
21+#define MOCK_NOTIFICATION_MODEL_H
22+
23+#include <QAbstractListModel>
24+#include <QSharedPointer>
25+#include <QScopedPointer>
26+#include "MockNotification.h"
27+
28+class MockNotification;
29+
30+class MockNotificationModel : public QAbstractListModel {
31+ Q_OBJECT
32+ Q_PROPERTY(int count READ getCount)
33+
34+public:
35+ MockNotificationModel(QObject *parent=nullptr);
36+ virtual ~MockNotificationModel();
37+
38+ virtual int rowCount(const QModelIndex &parent) const;
39+ virtual QVariant data(const QModelIndex &index, int role) const;
40+ virtual QHash<int, QByteArray> roleNames() const;
41+
42+ Q_INVOKABLE void append(MockNotification* n);
43+ MockNotification* getNotification(int id) const;
44+
45+ // getRaw() is only meant to be used from QML, since QML cannot handle
46+ // QSharedPointers... on C++-side only use getNotification()
47+ Q_INVOKABLE MockNotification* getRaw(const int notificationId) const;
48+
49+ Q_INVOKABLE int queued() const;
50+ Q_INVOKABLE void remove(const int id);
51+ Q_INVOKABLE void removeSecond();
52+
53+ int getCount() const;
54+
55+Q_SIGNALS:
56+ void actionInvoked(const QString &action);
57+
58+public Q_SLOTS:
59+ void onCompleted(int id);
60+
61+private Q_SLOTS:
62+ void onDataChanged(int id);
63+
64+Q_SIGNALS:
65+ void queueSizeChanged(int newSize);
66+
67+private:
68+ QList<MockNotification*> m_queue;
69+ void removeInternal(int loc);
70+};
71+
72+#endif
tests/mocks/Unity/Notifications/plugin.cpp+5−3
@@ -1,5 +1,5 @@
11/*
2− * Copyright 2014 Canonical Ltd.
2+ * Copyright 2015 Canonical Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU Lesser General Public License as published by
@@ -19,13 +19,15 @@
1919
2020#include "plugin.h"
2121#include "MockActionModel.h"
22−#include "MockNotificationTypes.h"
22+#include "MockNotification.h"
23+#include "MockNotificationModel.h"
2324
2425#include <QtQml/qqml.h>
2526
2627void TestNotificationPlugin::registerTypes(const char* uri)
2728{
2829 // @uri Unity.Notifications
29− qmlRegisterUncreatableType<MockNotification>(uri, 1, 0, "Notification", "Notification objects can only be created by the plugin");
30+ qmlRegisterType<MockNotification>(uri, 1, 0, "Notification");
31+ qmlRegisterType<MockNotificationModel>(uri, 1, 0, "NotificationModel");
3032 qmlRegisterType<ActionModel>(uri, 1, 0, "ActionModel");
3133}
tests/mocks/Unity/Notifications/plugin.h+1−2
@@ -1,5 +1,5 @@
11/*
2− * Copyright 2014 Canonical Ltd.
2+ * Copyright 2015 Canonical Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU Lesser General Public License as published by
@@ -17,7 +17,6 @@
1717 * Mirco Mueller <mirco.mueller@canonical.com>
1818 */
1919
20−
2120#ifndef TESTNOTIFICATION_PLUGIN_H
2221#define TESTNOTIFICATION_PLUGIN_H
2322
tests/mocks/libusermetrics/UserMetrics.cpp+8
@@ -289,6 +289,7 @@ void UserMetricsPrivate::generateFakeData()
289289 new UserMetricsData("<b>52km</b> travelled", first, firstMonth,
290290 ninth, secondMonth, this));
291291 m_fakeData.insert("single", data);
292+ m_fakeData.insert("has-pin", data);
292293 }
293294
294295 {
@@ -304,12 +305,14 @@ void UserMetricsPrivate::generateFakeData()
304305 new UserMetricsData("<b>33</b> messages today", second,
305306 firstMonth, eighth, secondMonth, this));
306307 m_fakeData.insert("single", data);
308+ m_fakeData.insert("has-pin", data);
307309 }
308310
309311 {
310312 QVariantList firstMonth;
311313 while (firstMonth.size() < 17)
312314 firstMonth.push_back(QVariant(rand()));
315+ firstMonth[8] = QVariant(1.0); // oversized 9th day, to test clipping
313316 while (firstMonth.size() < 31)
314317 firstMonth.push_back(QVariant());
315318 QVariantList secondMonth;
@@ -319,6 +322,7 @@ void UserMetricsPrivate::generateFakeData()
319322 new UserMetricsData("<b>19</b> minutes talk time", eighth,
320323 firstMonth, second, secondMonth, this));
321324 m_fakeData.insert("single", data);
325+ m_fakeData.insert("has-pin", data);
322326 // Also use same data for some tablet users
323327 m_fakeData.insert("has-password", data);
324328 m_fakeData.insert("no-password", data);
@@ -386,6 +390,10 @@ void UserMetricsPrivate::nextFakeData()
386390 if (m_dataIndex == m_fakeData.constEnd() || m_dataIndex.key() != m_username)
387391 {
388392 m_dataIndex = m_fakeData.constFind(m_username);
393+ if (m_dataIndex == m_fakeData.constEnd())
394+ {
395+ m_dataIndex = m_fakeData.constFind("");
396+ }
389397 }
390398
391399 loadFakeData();
tests/plugins/CMakeLists.txt+1
@@ -1,4 +1,5 @@
11add_subdirectory(AccountsService)
2+add_subdirectory(Greeter)
23add_subdirectory(LightDM)
34add_subdirectory(Dash)
45add_subdirectory(Ubuntu)
tests/plugins/Greeter/CMakeLists.txtnew file+1
@@ -0,0 +1 @@
1+add_subdirectory(Unity)
tests/plugins/Greeter/Unity/CMakeLists.txtnew file+1
@@ -0,0 +1 @@
1+add_subdirectory(Launcher)
tests/plugins/Greeter/Unity/Launcher/AccountsServiceDBusAdaptor.cppnew file+48
@@ -0,0 +1,48 @@
1+/*
2+ * Copyright (C) 2014-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 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 "AccountsServiceDBusAdaptor.h"
18+#include <QDebug>
19+#include <QDBusArgument>
20+#include <QDBusMessage>
21+#include <QDBusMetaType>
22+
23+AccountsServiceDBusAdaptor::AccountsServiceDBusAdaptor(QObject* parent)
24+ : QObject(parent)
25+{
26+ qDBusRegisterMetaType<QList<QVariantMap>>();
27+}
28+
29+QVariant AccountsServiceDBusAdaptor::getUserProperty(const QString &user, const QString &interface, const QString &property)
30+{
31+ Q_UNUSED(interface)
32+ Q_UNUSED(property) // We only fake one property here (LauncherItems)
33+
34+ QDBusMessage msg;
35+ QVariant v = QVariant::fromValue(mockProperties.value(user));
36+ QDBusVariant dv(v);
37+ QVariant packed = QVariant::fromValue(dv);
38+ msg << packed;
39+
40+ return packed.value<QDBusArgument>().asVariant();
41+}
42+
43+void AccountsServiceDBusAdaptor::simulatePropertyChange(const QString &user, const QString &property, const QVariant &value)
44+{
45+ Q_ASSERT(property == "LauncherItems");
46+ mockProperties[user] = value.value<QList<QVariantMap>>();
47+ Q_EMIT propertiesChanged(user, "com.canonical.unity.AccountsService", QStringList() << property);
48+}
tests/plugins/Greeter/Unity/Launcher/AccountsServiceDBusAdaptor.hnew file+53
@@ -0,0 +1,53 @@
1+/*
2+ * Copyright (C) 2014-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 General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef UNITY_ACCOUNTSSERVICEDBUSADAPTOR_H
18+#define UNITY_ACCOUNTSSERVICEDBUSADAPTOR_H
19+
20+#include <QMap>
21+#include <QObject>
22+#include <QString>
23+#include <QHash>
24+#include <QDBusArgument>
25+
26+extern QHash<QString, QList<QVariantMap>> mockProperties;
27+
28+class AccountsServiceDBusAdaptor: public QObject
29+{
30+ Q_OBJECT
31+
32+public:
33+ explicit AccountsServiceDBusAdaptor(QObject *parent = 0);
34+
35+ Q_INVOKABLE QVariant getUserProperty(const QString &user, const QString &interface, const QString &property);
36+ template <typename T>
37+ inline T getUserProperty(const QString &user, const QString &interface, const QString &property) {
38+ Q_UNUSED(interface)
39+ Q_ASSERT(property == "LauncherItems");
40+ T ret = mockProperties[user];
41+ return ret;
42+ }
43+
44+Q_SIGNALS:
45+ void propertiesChanged(const QString &user, const QString &interface, const QStringList &changed);
46+
47+private:
48+ void simulatePropertyChange(const QString &user, const QString &property, const QVariant &value);
49+
50+ friend class LauncherModelASTest;
51+};
52+
53+#endif
tests/plugins/Greeter/Unity/Launcher/CMakeLists.txtnew file+56
@@ -0,0 +1,56 @@
1+pkg_check_modules(LAUNCHER_API REQUIRED unity-shell-launcher=6)
2+pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=5)
3+
4+include_directories(
5+ ${CMAKE_CURRENT_SOURCE_DIR}
6+ ${CMAKE_CURRENT_BINARY_DIR}
7+ ${CMAKE_SOURCE_DIR}/plugins/Greeter/Unity/Launcher
8+ ${libunity8-private_SOURCE_DIR}
9+ )
10+
11+add_definitions(-DSM_BUSNAME=sessionBus)
12+add_definitions(-DSRCDIR="${CMAKE_CURRENT_SOURCE_DIR}")
13+add_definitions(-DLAUNCHER_TESTING)
14+
15+### LauncherModelASTest
16+set(testModelCommand dbus-test-runner --task ${CMAKE_CURRENT_BINARY_DIR}/launchermodelastestExec
17+ --parameter -o --parameter ${CMAKE_BINARY_DIR}/launchermodeltest.xml,xunitxml
18+ --parameter -o --parameter -,txt)
19+add_test(NAME launchermodelastest COMMAND ${testModelCommand})
20+add_custom_target(launchermodelastest ${testModelCommand})
21+add_executable(launchermodelastestExec
22+ launchermodelastest.cpp
23+ AccountsServiceDBusAdaptor.cpp
24+ ${CMAKE_SOURCE_DIR}/plugins/Greeter/Unity/Launcher/launchermodelas.cpp
25+ ${CMAKE_SOURCE_DIR}/plugins/Greeter/Unity/Launcher/launcheritem.cpp
26+ ${CMAKE_SOURCE_DIR}/plugins/Greeter/Unity/Launcher/quicklistmodel.cpp
27+ ${CMAKE_SOURCE_DIR}/plugins/Greeter/Unity/Launcher/quicklistentry.cpp
28+ ${LAUNCHER_API_INCLUDEDIR}/unity/shell/launcher/LauncherItemInterface.h
29+ ${LAUNCHER_API_INCLUDEDIR}/unity/shell/launcher/LauncherModelInterface.h
30+ ${LAUNCHER_API_INCLUDEDIR}/unity/shell/launcher/QuickListModelInterface.h
31+ ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationManagerInterface.h
32+ ${LAUNCHER_API_INCLUDEDIR}/unity/shell/application/ApplicationInfoInterface.h
33+ )
34+target_link_libraries(launchermodelastestExec
35+ unity8-private
36+ ${GSETTINGS_QT_LDFLAGS}
37+ )
38+qt5_use_modules(launchermodelastestExec Test Core DBus Gui)
39+
40+# copy .desktop files into build directory for shadow builds
41+file(GLOB DESKTOP_FILES *.desktop)
42+
43+foreach(DESKTOP_FILE ${DESKTOP_FILES})
44+ file(COPY "${DESKTOP_FILE}"
45+ DESTINATION ${CMAKE_CURRENT_BINARY_DIR}
46+ )
47+endforeach()
48+
49+# copy .svg files into build directory for shadow builds
50+file(GLOB DESKTOP_FILES *.svg)
51+
52+foreach(DESKTOP_FILE ${DESKTOP_FILES})
53+ file(COPY "${DESKTOP_FILE}"
54+ DESTINATION ${CMAKE_CURRENT_BINARY_DIR}
55+ )
56+endforeach()
tests/plugins/Greeter/Unity/Launcher/launchermodelastest.cppnew file+169
@@ -0,0 +1,169 @@
1+/*
2+ * Copyright 2014-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+// unity-api
18+#include <unity/shell/launcher/LauncherModelInterface.h>
19+#include <unity/shell/application/ApplicationInfoInterface.h>
20+
21+#include "launcheritem.h"
22+#include "launchermodelas.h"
23+#include "AccountsServiceDBusAdaptor.h"
24+
25+#include <QtTest>
26+
27+QHash<QString, QList<QVariantMap>> mockProperties;
28+
29+class LauncherModelASTest : public QObject
30+{
31+ Q_OBJECT
32+
33+private:
34+ LauncherModel *launcherModel;
35+
36+private Q_SLOTS:
37+
38+ void init() {
39+ // Prepare some fake list
40+ QList<QVariantMap> list;
41+ QVariantMap item;
42+ item.insert("id", "appId1");
43+ item.insert("name", "Item 1");
44+ item.insert("icon", "fake.svg");
45+ item.insert("count", 0);
46+ item.insert("countVisible", false);
47+ item.insert("pinned", true);
48+ list.append(item);
49+ item["id"] = "appId2";
50+ item["name"] = "Item 2";
51+ list.append(item);
52+ mockProperties["user1"] = list;
53+ }
54+
55+ bool isInSync(LauncherModel *model, const QList<QVariantMap> &properties) {
56+ QList<QVariantMap> list;
57+ // Strip unpinned if onlyPinned is set
58+ Q_FOREACH (const QVariantMap &map, properties) {
59+ if (!model->onlyPinned() || map.value("pinned").toBool()) {
60+ list << map;
61+ }
62+ }
63+
64+ bool inSync = model->rowCount() == list.count();
65+ for (int i = 0; inSync && i < model->rowCount(); i++) {
66+ inSync &= model->get(i)->appId() == list.at(i).value("id").toString();
67+ inSync &= model->get(i)->name() == list.at(i).value("name").toString();
68+ inSync &= model->get(i)->icon() == list.at(i).value("icon").toString();
69+ inSync &= model->get(i)->count() == list.at(i).value("count").toInt();
70+ inSync &= model->get(i)->countVisible() == list.at(i).value("countVisible").toBool();
71+ }
72+ return inSync;
73+ }
74+
75+ void testLoadASOnStartup() {
76+ // Load up the model
77+ LauncherModel* model = new LauncherModel(this);
78+
79+ // We didn't set a user yet. model should be empty
80+ QCOMPARE(model->rowCount(), 0);
81+
82+ model->setUser("user1");
83+
84+ QCOMPARE(isInSync(model, mockProperties["user1"]), true);
85+ model->deleteLater();
86+ }
87+
88+ void testASChangedUpdatesModel_data() {
89+ QTest::addColumn<QString>("modelUser");
90+ QTest::addColumn<QString>("changedUser");
91+ QTest::addColumn<bool>("inSync");
92+
93+ QTest::newRow("this user changed") << "user1" << "user1" << true;
94+ QTest::newRow("other user changed") << "user1" << "user2" << false;
95+ }
96+
97+ void testASChangedUpdatesModel() {
98+ QFETCH(QString, modelUser);
99+ QFETCH(QString, changedUser);
100+ QFETCH(bool, inSync);
101+
102+ LauncherModel* model = new LauncherModel(this);
103+ model->setUser(modelUser);
104+
105+ int oldCount = mockProperties[modelUser].count();
106+ QCOMPARE(model->rowCount(), oldCount);
107+
108+ QList<QVariantMap> newList;
109+ QVariantMap newEntry;
110+ newEntry.insert("id", "newappId");
111+ newEntry.insert("name", "New app");
112+ newEntry.insert("icon", "some-icon.svg");
113+ newEntry.insert("count", 0);
114+ newEntry.insert("countVisible", false);
115+ newEntry.insert("pinned", true);
116+ newList.append(newEntry);
117+ model->m_accounts->simulatePropertyChange(changedUser, "LauncherItems", QVariant::fromValue(newList));
118+
119+ QCOMPARE(isInSync(model, mockProperties[modelUser]), true);
120+ QCOMPARE(isInSync(model, mockProperties[changedUser]), inSync);
121+
122+ model->deleteLater();
123+ }
124+
125+ void testUpdateCount() {
126+ LauncherModel* model = new LauncherModel(this);
127+ model->setUser("user1");
128+
129+ QCOMPARE(model->get(0)->countVisible(), false);
130+ QCOMPARE(model->get(0)->count(), 0);
131+
132+ QList<QVariantMap> newList = mockProperties["user1"];
133+ QVariantMap entry = newList.at(0);
134+ entry["countVisible"] = true;
135+ entry["count"] = 55;
136+ newList[0] = entry;
137+ model->m_accounts->simulatePropertyChange("user1", "LauncherItems", QVariant::fromValue(newList));
138+
139+ QCOMPARE(isInSync(model, mockProperties["user1"]), true);
140+
141+ model->deleteLater();
142+ }
143+
144+ void testOnlyPinned() {
145+ LauncherModel *model = new LauncherModel(this);
146+ model->setUser("user1");
147+ model->setOnlyPinned(true);
148+
149+ QCOMPARE(isInSync(model, mockProperties["user1"]), true);
150+
151+ // Let's unpin one item
152+ QList<QVariantMap> newList = mockProperties["user1"];
153+ QVariantMap entry = newList.at(0);
154+ entry["pinned"] = false;
155+ newList[0] = entry;
156+ model->m_accounts->simulatePropertyChange("user1", "LauncherItems", QVariant::fromValue(newList));
157+ QCOMPARE(isInSync(model, mockProperties["user1"]), true);
158+
159+ // Now toggle onlyPinned and make sure the model keeps up
160+ model->setOnlyPinned(false);
161+ QCOMPARE(isInSync(model, mockProperties["user1"]), true);
162+
163+ model->setOnlyPinned(true);
164+ QCOMPARE(isInSync(model, mockProperties["user1"]), true);
165+ }
166+};
167+
168+QTEST_GUILESS_MAIN(LauncherModelASTest)
169+#include "launchermodelastest.moc"
tests/plugins/Unity/Indicators/indicatorsmanagertest.cpp+30−5
@@ -50,7 +50,8 @@ private Q_SLOTS:
5050 QVERIFY(!manager.isLoaded());
5151 QCOMPARE(manager.indicators().count(), 0);
5252
53− manager.load("test1");
53+ manager.setProfile("test1");
54+ manager.load();
5455
5556 QVERIFY(manager.isLoaded());
5657 QCOMPARE(manager.indicators().count(), 4);
@@ -67,7 +68,8 @@ private Q_SLOTS:
6768 void testPluginInterfaceProfile1()
6869 {
6970 IndicatorsManager manager;
70− manager.load("test1");
71+ manager.setProfile("test1");
72+ manager.load();
7173
7274 Indicator::Ptr indicator = manager.indicator("indicator-fake1");
7375 QVERIFY(indicator ? true : false);
@@ -90,7 +92,8 @@ private Q_SLOTS:
9092 void testPluginInterfaceProfile2()
9193 {
9294 IndicatorsManager manager;
93− manager.load("test2");
95+ manager.setProfile("test2");
96+ manager.load();
9497
9598 Indicator::Ptr indicator = manager.indicator("indicator-fake1");
9699 QVERIFY(indicator ? true : false);
@@ -107,13 +110,34 @@ private Q_SLOTS:
107110 QCOMPARE(props["menuObjectPath"].toString(), QString("/com/canonical/indicator/fake1/test2"));
108111 }
109112
113+ /*
114+ * Test switching the indicator profile data
115+ */
116+ void testPluginInterfaceProfileSwitch()
117+ {
118+ IndicatorsManager manager;
119+ manager.setProfile("test1");
120+ manager.load();
121+
122+ Indicator::Ptr indicator = manager.indicator("indicator-fake1");
123+ QVERIFY(indicator ? true : false);
124+
125+ QVariantMap props = indicator->indicatorProperties().toMap();
126+ QCOMPARE(props["menuObjectPath"].toString(), QString("/com/canonical/indicator/fake1/test1"));
127+
128+ manager.setProfile("test2");
129+ props = indicator->indicatorProperties().toMap();
130+ QCOMPARE(props["menuObjectPath"].toString(), QString("/com/canonical/indicator/fake1/test2"));
131+ }
132+
110133 /*
111134 * Test if a new plugin object is create for each different plugin
112135 */
113136 void testPluginInstance()
114137 {
115138 IndicatorsManager manager;
116− manager.load("test2");
139+ manager.setProfile("test2");
140+ manager.load();
117141
118142 Indicator::Ptr i0 = manager.indicator("indicator-fake1");
119143 Indicator::Ptr i1 = manager.indicator("indicator-fake1");
@@ -133,7 +157,8 @@ private Q_SLOTS:
133157 void testPluginInitAndShutdown()
134158 {
135159 IndicatorsManager manager;
136− manager.load("test1");
160+ manager.setProfile("test1");
161+ manager.load();
137162
138163 QWeakPointer<Indicator> wp0;
139164 QWeakPointer<Indicator> wp1;
tests/plugins/Unity/Indicators/indicatorsmodeltest.cpp+6−3
@@ -47,10 +47,11 @@ private Q_SLOTS:
4747 void testLoad()
4848 {
4949 IndicatorsModel model;
50+ model.setProfile("test1");
5051
5152 QCOMPARE(model.property("count").toInt(), 0);
5253
53− model.load("test1");
54+ model.load();
5455
5556 QCOMPARE(model.property("count").toInt(), 4);
5657
@@ -67,7 +68,8 @@ private Q_SLOTS:
6768 {
6869 // Priority order. (2, 1, 4, 3)
6970 IndicatorsModel model;
70− model.load("test1");
71+ model.setProfile("test1");
72+ model.load();
7173
7274 // should be in order:
7375 // fake3, fake4, fake1, fake2
@@ -104,7 +106,8 @@ private Q_SLOTS:
104106 {
105107 // Priority order. (2, 1, 4, 3)
106108 IndicatorsModel model;
107− model.load("test2");
109+ model.setProfile("test2");
110+ model.load();
108111
109112 // should be in order:
110113 // fake3, fake4, fake1, fake2
tests/plugins/Unity/Indicators/sharedunitymenumodeltest.cpp+2−2
@@ -61,7 +61,7 @@ private Q_SLOTS:
6161 QCOMPARE(model1->model(), model2->model());
6262 }
6363
64− void testSharedOwnership()
64+ void testSavedData()
6565 {
6666 QSharedPointer<SharedUnityMenuModel> model1(createFullModel("test1"));
6767 QSharedPointer<SharedUnityMenuModel> model2(createFullModel("test1"));
@@ -70,7 +70,7 @@ private Q_SLOTS:
7070 model1.clear();
7171 QCOMPARE(UnityMenuModelCache::singleton()->contains("/com/canonical/test1"), true);
7272 model2.clear();
73− QCOMPARE(UnityMenuModelCache::singleton()->contains("/com/canonical/test1"), false);
73+ QCOMPARE(UnityMenuModelCache::singleton()->contains("/com/canonical/test1"), true);
7474 }
7575
7676 // Tests that changing cached model data does not change the model path of others
tests/plugins/Unity/Launcher/AccountsServiceDBusAdaptor.cppnew file+42
@@ -0,0 +1,42 @@
1+/*
2+ * Copyright (C) 2014-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 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 "AccountsServiceDBusAdaptor.h"
18+#include <QDebug>
19+
20+AccountsServiceDBusAdaptor::AccountsServiceDBusAdaptor(QObject* parent)
21+ : QObject(parent)
22+{
23+}
24+
25+QVariant AccountsServiceDBusAdaptor::getUserProperty(const QString &user, const QString &interface, const QString &property)
26+{
27+ Q_UNUSED(user)
28+ Q_UNUSED(interface)
29+ return m_properties.value(property);
30+}
31+
32+void AccountsServiceDBusAdaptor::setUserProperty(const QString &user, const QString &interface, const QString &property, const QVariant &value)
33+{
34+ Q_UNUSED(user)
35+ Q_UNUSED(interface)
36+ m_properties[property] = value;
37+}
38+
39+void AccountsServiceDBusAdaptor::setUserPropertyAsync(const QString &user, const QString &interface, const QString &property, const QVariant &value)
40+{
41+ setUserProperty(user, interface, property, value);
42+}
tests/plugins/Unity/Launcher/AccountsServiceDBusAdaptor.hnew file+47
@@ -0,0 +1,47 @@
1+/*
2+ * Copyright (C) 2014-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 General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef UNITY_ACCOUNTSSERVICEDBUSADAPTOR_H
18+#define UNITY_ACCOUNTSSERVICEDBUSADAPTOR_H
19+
20+#include <QMap>
21+#include <QObject>
22+#include <QString>
23+#include <QHash>
24+
25+class AccountsServiceDBusAdaptor: public QObject
26+{
27+ Q_OBJECT
28+
29+public:
30+ explicit AccountsServiceDBusAdaptor(QObject *parent = 0);
31+
32+ Q_INVOKABLE QVariant getUserProperty(const QString &user, const QString &interface, const QString &property);
33+ Q_INVOKABLE void setUserProperty(const QString &user, const QString &interface, const QString &property, const QVariant &value);
34+ Q_INVOKABLE void setUserPropertyAsync(const QString &user, const QString &interface, const QString &property, const QVariant &value);
35+
36+Q_SIGNALS:
37+ void propertiesChanged(const QString &user, const QString &interface, const QStringList &changed);
38+ void maybeChanged(const QString &user); // Standard properties might have changed
39+
40+private:
41+ void simulatePropertyChange(const QString &property, const QVariant &value);
42+
43+private:
44+ QHash<QString, QVariant> m_properties;
45+};
46+
47+#endif
tests/plugins/Unity/Launcher/CMakeLists.txt+3−1
@@ -1,5 +1,5 @@
11pkg_check_modules(GSETTINGS_QT REQUIRED gsettings-qt)
2−pkg_check_modules(LAUNCHER_API REQUIRED unity-shell-launcher=5)
2+pkg_check_modules(LAUNCHER_API REQUIRED unity-shell-launcher=6)
33pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=5)
44
55include_directories(
@@ -23,7 +23,9 @@ add_custom_target(launchermodeltest ${testModelCommand})
2323add_executable(launchermodeltestExec
2424 launchermodeltest.cpp
2525 gsettings.cpp
26+ AccountsServiceDBusAdaptor.cpp
2627 ${CMAKE_SOURCE_DIR}/plugins/Unity/Launcher/launchermodel.cpp
28+ ${CMAKE_SOURCE_DIR}/plugins/Unity/Launcher/asadapter.cpp
2729 ${CMAKE_SOURCE_DIR}/plugins/Unity/Launcher/launcheritem.cpp
2830 ${CMAKE_SOURCE_DIR}/plugins/Unity/Launcher/quicklistmodel.cpp
2931 ${CMAKE_SOURCE_DIR}/plugins/Unity/Launcher/dbusinterface.cpp
tests/plugins/Unity/Launcher/launchermodeltest.cpp+65−1
@@ -1,5 +1,5 @@
11/*
2− * Copyright 2013 Canonical Ltd.
2+ * Copyright 2013-2014 Canonical Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU Lesser General Public License as published by
@@ -25,6 +25,8 @@
2525#include "launchermodel.h"
2626#include "dbusinterface.h"
2727#include "gsettings.h"
28+#include "asadapter.h"
29+#include "AccountsServiceDBusAdaptor.h"
2830
2931#include <QtTest>
3032#include <QDBusInterface>
@@ -129,6 +131,11 @@ private:
129131 LauncherModel *launcherModel;
130132 MockAppManager *appManager;
131133
134+ QList<QVariantMap> getASConfig() {
135+ AccountsServiceDBusAdaptor *as = launcherModel->m_asAdapter->m_accounts;
136+ return as->getUserProperty("", "", "LauncherItems").value<QList<QVariantMap>>();
137+ }
138+
132139private Q_SLOTS:
133140
134141 void initTestCase() {
@@ -467,6 +474,63 @@ private Q_SLOTS:
467474 QCOMPARE(launcherModel->get(1)->appId(), QString("abs-icon"));
468475 QCOMPARE(spy.count(), 2);
469476 }
477+
478+ void testAddSyncsToAS() {
479+ // Make sure launcher and AS are in sync when we start the test
480+ QCOMPARE(launcherModel->rowCount(), getASConfig().count());
481+
482+ int oldCount = launcherModel->rowCount();
483+ appManager->addApplication(new MockApp("rel-icon"));
484+ QCOMPARE(launcherModel->rowCount(), oldCount + 1);
485+ QCOMPARE(launcherModel->rowCount(), getASConfig().count());
486+ }
487+
488+ void testRemoveSyncsToAS() {
489+ // Make sure launcher and AS are in sync when we start the test
490+ QCOMPARE(launcherModel->rowCount(), getASConfig().count());
491+
492+ int oldCount = launcherModel->rowCount();
493+ appManager->stopApplication("abs-icon");
494+ QCOMPARE(launcherModel->rowCount(), oldCount - 1);
495+ QCOMPARE(launcherModel->rowCount(), getASConfig().count());
496+ }
497+
498+ void testMoveSyncsToAS() {
499+ // Make sure launcher and AS are in sync when we start the test
500+ QCOMPARE(launcherModel->rowCount(), getASConfig().count());
501+
502+ for (int i = 0; i < launcherModel->rowCount(); i++) {
503+ QString launcherAppId = launcherModel->get(i)->appId();
504+ QString asAppId = getASConfig().at(i).value("id").toString();
505+ QCOMPARE(launcherAppId, asAppId);
506+ }
507+
508+ launcherModel->move(0, 1);
509+
510+ for (int i = 0; i < launcherModel->rowCount(); i++) {
511+ QString launcherAppId = launcherModel->get(i)->appId();
512+ QString asAppId = getASConfig().at(i).value("id").toString();
513+ QCOMPARE(launcherAppId, asAppId);
514+ }
515+ }
516+
517+ void testCountChangeSyncsToAS() {
518+ // Find the index of the abs-icon app
519+ int index = launcherModel->findApplication("abs-icon");
520+
521+ // Make sure it's invisible and 0 at the beginning
522+ QCOMPARE(getASConfig().at(index).value("countVisible").toBool(), false);
523+ QCOMPARE(getASConfig().at(index).value("count").toInt(), 0);
524+
525+ // Change the count of the abs-icon app through D-Bus
526+ QDBusInterface interface("com.canonical.Unity.Launcher", "/com/canonical/Unity/Launcher/abs_2Dicon", "org.freedesktop.DBus.Properties");
527+ interface.call("Set", "com.canonical.Unity.Launcher.Item", "count", QVariant::fromValue(QDBusVariant(55)));
528+ interface.call("Set", "com.canonical.Unity.Launcher.Item", "countVisible", QVariant::fromValue(QDBusVariant(true)));
529+
530+ // Make sure it changed to visible and 55
531+ QCOMPARE(getASConfig().at(index).value("countVisible").toBool(), true);
532+ QCOMPARE(getASConfig().at(index).value("count").toInt(), 55);
533+ }
470534};
471535
472536QTEST_GUILESS_MAIN(LauncherModelTest)
tests/qmltests/CMakeLists.txt+2−1
@@ -25,7 +25,6 @@ add_qml_test(Components Background)
2525add_qml_test(Components Carousel)
2626add_qml_test(Components Dialogs)
2727add_qml_test(Components DraggingArea)
28−add_qml_test(Components EdgeDemoOverlay)
2928add_qml_test(Components LazyImage)
3029add_qml_test(Components Lockscreen)
3130add_qml_test(Components Rating)
@@ -88,6 +87,8 @@ add_qml_test(Stages PhoneStage ENVIRONMENT)
8887add_qml_test(Stages SpreadDelegate ENVIRONMENT)
8988add_qml_test(Stages SurfaceContainer ENVIRONMENT)
9089add_qml_test(Stages SessionContainer ENVIRONMENT)
90+add_qml_test(Stages TabletStage)
9191add_qml_test(Stages WindowMoveResizeArea)
9292add_qml_test(Stages Splash)
93+add_qml_test(Tutorial Tutorial)
9394add_qml_test(Wizard Wizard)
tests/qmltests/Components/tst_EdgeDemoOverlay.qmldeleted−123
@@ -1,123 +0,0 @@
1−/*
2− * Copyright 2013 Canonical Ltd.
3− *
4− * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU General Public License as published by
6− * the Free Software Foundation; version 3.
7− *
8− * This program is distributed in the hope that it will be useful,
9− * but WITHOUT ANY WARRANTY; without even the implied warranty of
10− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU General Public License for more details.
12− *
13− * You should have received a copy of the GNU General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− */
16−
17−import QtQuick 2.0
18−import QtTest 1.0
19−import Unity.Test 0.1 as UT
20−import "../../../qml/Components"
21−
22−Item {
23− id: root
24− width: boxWidth * 3
25− height: boxHeight * 2
26−
27− property int boxWidth: 250
28− property int boxHeight: 250
29−
30− EdgeDemoOverlay {
31− id: top
32− edge: "top"
33− title: "Top"
34− text: "Displayed on top left"
35− anchors.left: parent.left
36− anchors.top: parent.top
37− width: boxWidth
38− height: boxHeight
39− }
40−
41− EdgeDemoOverlay {
42− id: right
43− edge: "right"
44− title: "Right"
45− text: "Displayed on top right"
46− anchors.right: parent.right
47− anchors.top: parent.top
48− width: boxWidth
49− height: boxHeight
50− }
51−
52− EdgeDemoOverlay {
53− id: left
54− edge: "left"
55− title: "Left"
56− text: "Displayed on bottom right"
57− anchors.right: parent.right
58− anchors.bottom: parent.bottom
59− width: boxWidth
60− height: boxHeight
61− available: false
62− }
63−
64− EdgeDemoOverlay {
65− id: bottom
66− edge: "bottom"
67− title: "Bottom"
68− text: "Displayed on bottom left"
69− anchors.left: parent.left
70− anchors.bottom: parent.bottom
71− width: boxWidth
72− height: boxHeight
73− }
74−
75− EdgeDemoOverlay {
76− id: none
77− edge: "none"
78− title: "None"
79− text: "Displayed on top middle"
80− anchors.horizontalCenter: parent.horizontalCenter
81− anchors.top: parent.top
82− width: boxWidth
83− height: boxHeight
84− }
85−
86− SignalSpy {
87− id: signalSpy
88− }
89−
90− UT.UnityTestCase {
91− name: "EdgeDemoOverlay"
92− when: windowShown
93−
94− function test_animations() {
95− compare(right.running, true)
96−
97− compare(left.running, false)
98− left.available = true
99− compare(left.running, true)
100− }
101−
102− function test_skip() {
103− signalSpy.target = bottom
104− signalSpy.signalName = "skip"
105− signalSpy.clear()
106− var bottomSkip = findChild(bottom, "skipLabel")
107− mousePress(bottomSkip, 1, 1)
108− mouseRelease(bottomSkip, 1, 1)
109− signalSpy.wait()
110− compare(bottom.available, false)
111−
112− // Test that the 'none' edge skips anywhere
113− signalSpy.target = none
114− signalSpy.clear()
115− var backgroundShade = findChild(none, "backgroundShadeMouseArea")
116− tryCompare(backgroundShade, "enabled", true)
117− mousePress(backgroundShade, 1, 1)
118− mouseRelease(backgroundShade, 1, 1)
119− signalSpy.wait()
120− compare(none.available, false)
121− }
122− }
123−}
tests/qmltests/Launcher/tst_Launcher.qml+46−19
@@ -15,6 +15,7 @@
1515 */
1616
1717import QtQuick 2.0
18+import QtQuick.Layouts 1.1
1819import QtTest 1.0
1920import Unity.Test 0.1 as UT
2021import Ubuntu.Components 1.1
@@ -74,10 +75,22 @@ Item {
7475 }
7576 }
7677
77− Button {
78+ ColumnLayout {
7879 anchors { bottom: parent.bottom; right: parent.right; margins: units.gu(1) }
79− text: "emit hinting signal"
80− onClicked: LauncherModel.emitHint()
80+ spacing: units.gu(1)
81+ width: units.gu(20)
82+
83+ Button {
84+ text: "emit hinting signal"
85+ onClicked: LauncherModel.emitHint()
86+ Layout.fillWidth: true
87+ }
88+
89+ Button {
90+ text: "rotate"
91+ onClicked: launcherLoader.item.inverted = !launcherLoader.item.inverted
92+ Layout.fillWidth: true
93+ }
8194 }
8295
8396 SignalSpy {
@@ -140,6 +153,18 @@ Item {
140153 tryCompare(panel, "x", -panel.width, 1000);
141154 }
142155
156+ function positionLauncherListAtBeginning() {
157+ var listView = testCase.findChild(launcherLoader.item, "launcherListView");
158+ listView.contentY = -listView.topMargin;
159+ }
160+ function positionLauncherListAtEnd() {
161+ var listView = testCase.findChild(launcherLoader.item, "launcherListView");
162+ if ((listView.contentHeight + listView.topMargin + listView.bottomMargin) > listView.height) {
163+ listView.contentY = listView.topMargin + listView.contentHeight
164+ - listView.height;
165+ }
166+ }
167+
143168 // Drag from the left edge of the screen rightwards and check that the launcher
144169 // appears (as if being dragged by the finger/pointer)
145170 function test_dragLeftEdgeToRevealLauncherAndTapCenterToDismiss() {
@@ -166,22 +191,22 @@ Item {
166191 launcherApplicationSelected("[...]dialer-app.desktop") */
167192 function test_clickingOnAppIconCausesSignalEmission() {
168193 dragLauncherIntoView();
169− launcher.lastSelectedApplication = ""
194+ launcher.lastSelectedApplication = "";
195+ launcher.inverted = false;
170196
171− var listView = findChild(launcher, "launcherListView");
172− listView.positionViewAtEnd();
197+ positionLauncherListAtBeginning();
173198
174− var appIcon = findChild(launcher, "launcherDelegate0")
199+ var appIcon = findChild(launcher, "launcherDelegate0");
175200
176− verify(appIcon != undefined)
201+ verify(appIcon != undefined);
177202
178− mouseClick(appIcon)
203+ mouseClick(appIcon);
179204
180205 tryCompare(launcher, "lastSelectedApplication",
181− "dialer-app")
206+ appIcon.appId);
182207
183208 // Tapping on an application icon also dismisses the launcher
184− waitUntilLauncherDisappears()
209+ waitUntilLauncherDisappears();
185210 }
186211
187212 /* If I click on the dash icon on the launcher
@@ -277,25 +302,26 @@ Item {
277302 function test_clickFlick_data() {
278303 var listView = findChild(launcher, "launcherListView");
279304 return [
280− {tag: "unfolded top", positionViewAtBeginning: false,
305+ {tag: "unfolded top", positionViewAtBeginning: true,
281306 clickY: listView.topMargin + units.gu(2),
282307 expectFlick: false},
283308
284− {tag: "folded top", positionViewAtBeginning: true,
309+ {tag: "folded top", positionViewAtBeginning: false,
285310 clickY: listView.topMargin + units.gu(2),
286311 expectFlick: true},
287312
288− {tag: "unfolded bottom", positionViewAtBeginning: true,
313+ {tag: "unfolded bottom", positionViewAtBeginning: false,
289314 clickY: listView.height - listView.topMargin - units.gu(1),
290315 expectFlick: false},
291316
292− {tag: "folded bottom", positionViewAtBeginning: false,
317+ {tag: "folded bottom", positionViewAtBeginning: true,
293318 clickY: listView.height - listView.topMargin - units.gu(1),
294319 expectFlick: true},
295320 ];
296321 }
297322
298323 function test_clickFlick(data) {
324+ launcher.inverted = false;
299325 launcher.lastSelectedApplication = "";
300326 dragLauncherIntoView();
301327 var listView = findChild(launcher, "launcherListView");
@@ -305,9 +331,9 @@ Item {
305331 // So for stability's sake we just put the listView in the position
306332 // we want to to actually start doing what this tests intends to check.
307333 if (data.positionViewAtBeginning) {
308− listView.positionViewAtBeginning();
334+ positionLauncherListAtBeginning();
309335 } else {
310− listView.positionViewAtEnd();
336+ positionLauncherListAtEnd();
311337 }
312338 tryCompare(listView, "flicking", false);
313339
@@ -442,9 +468,9 @@ Item {
442468 // Position launcher to where we need it
443469 var listView = findChild(launcher, "launcherListView");
444470 if (data.flickTo == "top") {
445− listView.positionViewAtEnd();
471+ positionLauncherListAtBeginning();
446472 } else {
447− listView.positionViewAtBeginning();
473+ positionLauncherListAtEnd();
448474 }
449475
450476 // Doing longpress
@@ -454,6 +480,7 @@ Item {
454480
455481 verify(quickList.y >= units.gu(1));
456482 verify(quickList.y + quickList.height + units.gu(1) <= launcher.height);
483+ compare(quickList.width, units.gu(30));
457484
458485 // Click somewhere in the empty space to dismiss the quicklist
459486 mouseClick(launcher, launcher.width - units.gu(1), units.gu(1));
tests/mocks/Unity/Notifications/MockNotificationTypes.cpp → tests/qmltests/Notifications/Notification.qmlrenamed+11−6
@@ -1,5 +1,5 @@
11/*
2− * Copyright 2014 Canonical Ltd.
2+ * Copyright 2015 Canonical Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU Lesser General Public License as published by
@@ -17,10 +17,15 @@
1717 * Mirco Mueller <mirco.mueller@canonical.com>
1818 */
1919
20−#include "MockNotificationTypes.h"
20+import Unity.Notifications 1.0
2121
22−MockNotification::MockNotification(QObject *parent) : QObject(parent) {
23−}
24−
25−MockNotification::~MockNotification() {
22+Notification {
23+ nid: 0
24+ type: Notification.PlaceHolder
25+ summary: ""
26+ body: ""
27+ icon: ""
28+ secondaryIcon: ""
29+ value: 0
30+ rawActions: []
2631}
tests/qmltests/Notifications/tst_Notifications.qml+271−213
@@ -1,17 +1,20 @@
11/*
2− * Copyright (C) 2013 Canonical, Ltd.
2+ * Copyright 2015 Canonical Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU General Public License as published by
5+ * it under the terms of the GNU Lesser General Public License as published by
66 * the Free Software Foundation; version 3.
77 *
88 * This program is distributed in the hope that it will be useful,
99 * but WITHOUT ANY WARRANTY; without even the implied warranty of
1010 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU General Public License for more details.
11+ * GNU Lesser General Public License for more details.
1212 *
13− * You should have received a copy of the GNU General Public License
13+ * You should have received a copy of the GNU Lesser General Public License
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ *
16+ * Authors:
17+ * Mirco Mueller <mirco.mueller@canonical.com>
1518 */
1619
1720import QtQuick 2.0
@@ -24,146 +27,134 @@ import Unity.Notifications 1.0
2427import QtMultimedia 5.0
2528
2629Item {
30+ id: foobar
31+
2732 width: notificationsRect.width + interactiveControls.width
2833 height: notificationsRect.height
34+ property int index: 0
2935
3036 Row {
3137 id: rootRow
3238
33− Component {
34− id: mockNotification
35−
36− QtObject {
37− function invokeAction(actionId) {
38− mockModel.actionInvoked(actionId)
39− }
40− }
41− }
42−
43− ListModel {
39+ NotificationModel {
4440 id: mockModel
45− dynamicRoles: true
46−
47− signal actionInvoked(string actionId)
48−
49− function getRaw(id) {
50− return mockNotification.createObject(mockModel)
51− }
5241
5342 // add the default/PlaceHolder notification to the model
5443 Component.onCompleted: {
55− var n = {
56− type: Notification.PlaceHolder,
57− hints: {},
58− summary: "",
59− body: "",
60− icon: "",
61− secondaryIcon: "",
62− actions: []
63− }
64−
44+ var component = Qt.createComponent("Notification.qml")
45+ var n = component.createObject("notification", {"nid": index++,
46+ "type": Notification.PlaceHolder,
47+ "hints": {},
48+ "summary": "",
49+ "body": "",
50+ "icon": "",
51+ "secondaryIcon": "",
52+ "rawActions": []})
53+ n.completed.connect(mockModel.onCompleted)
6554 append(n)
6655 }
6756 }
6857
6958 function add2over1SnapDecisionNotification() {
70− var n = {
71− type: Notification.SnapDecision,
72− hints: {"x-canonical-private-affirmative-tint": "true"},
73− summary: "Theatre at Ferria Stadium",
74− body: "at Ferria Stadium in Bilbao, Spain\n07578545317",
75− icon: "",
76− secondaryIcon: "",
77− actions: [{ id: "ok_id", label: "Ok"},
78− { id: "snooze_id", label: "Snooze"},
79− { id: "view_id", label: "View"}]
80− }
81−
59+ var component = Qt.createComponent("Notification.qml")
60+ var n = component.createObject("notification", {"nid": index++,
61+ "type": Notification.SnapDecision,
62+ "hints": {"x-canonical-private-affirmative-tint": "true"},
63+ "summary": "Theatre at Ferria Stadium",
64+ "body": "at Ferria Stadium in Bilbao, Spain\n07578545317",
65+ "icon": "",
66+ "secondaryIcon": "",
67+ "rawActions": ["ok_id", "Ok",
68+ "snooze_id", "Snooze",
69+ "view_id", "View"]})
70+ n.completed.connect(mockModel.onCompleted)
8271 mockModel.append(n)
8372 }
8473
8574 function addEphemeralNotification() {
86− var n = {
87− type: Notification.Ephemeral,
88− summary: "Cole Raby",
89− body: "I did not expect it to be that late.",
90− icon: "../graphics/avatars/amanda.png",
91− secondaryIcon: "../graphics/applicationIcons/facebook.png",
92− actions: []
93− }
94−
75+ var component = Qt.createComponent("Notification.qml")
76+ var n = component.createObject("notification", {"nid": index++,
77+ "type": Notification.Ephemeral,
78+ "hints": {},
79+ "summary": "Cole Raby",
80+ "body": "I did not expect it to be that late.",
81+ "icon": "../graphics/avatars/amanda.png",
82+ "secondaryIcon": "../graphics/applicationIcons/facebook.png",
83+ "rawActions": ["reply_id", "Dummy"]})
84+ n.completed.connect(mockModel.onCompleted)
9585 mockModel.append(n)
9686 }
9787
9888 function addEphemeralNonShapedIconNotification() {
99− var n = {
100− type: Notification.Ephemeral,
101− hints: {"x-canonical-non-shaped-icon": "true"},
102− summary: "Contacts",
103− body: "Synchronised contacts-database with cloud-storage.",
104− icon: "../graphics/applicationIcons/contacts-app.png",
105− secondaryIcon: "",
106− actions: []
107− }
108−
89+ var component = Qt.createComponent("Notification.qml")
90+ var n = component.createObject("notification", {"nid": index++,
91+ "type": Notification.Ephemeral,
92+ "hints": {"x-canonical-non-shaped-icon": "true"},
93+ "summary": "Contacts",
94+ "body": "Synchronised contacts-database with cloud-storage.",
95+ "icon": "../graphics/applicationIcons/contacts-app.png",
96+ "secondaryIcon": "",
97+ "rawActions": ["reply_id", "Dummy"]})
98+ n.completed.connect(mockModel.onCompleted)
10999 mockModel.append(n)
110100 }
111101
112102 function addEphemeralIconSummaryNotification() {
113− var n = {
114− type: Notification.Ephemeral,
115− hints: {"x-canonical-non-shaped-icon": "false"},
116− summary: "Photo upload completed",
117− body: "",
118− icon: "../graphics/applicationIcons/facebook.png",
119− secondaryIcon: "",
120− actions: []
121− }
122−
103+ var component = Qt.createComponent("Notification.qml")
104+ var n = component.createObject("notification", {"nid": index++,
105+ "type": Notification.Ephemeral,
106+ "hints": {"x-canonical-non-shaped-icon": "false"},
107+ "summary": "Photo upload completed",
108+ "body": "",
109+ "icon": "../graphics/applicationIcons/facebook.png",
110+ "secondaryIcon": "",
111+ "rawActions": ["reply_id", "Dummy"]})
112+ n.completed.connect(mockModel.onCompleted)
123113 mockModel.append(n)
124114 }
125115
126116 function addInteractiveNotification() {
127− var n = {
128− type: Notification.Interactive,
129− summary: "Interactive notification",
130− body: "This is a notification that can be clicked",
131− icon: "../graphics/avatars/anna_olsson.png",
132− secondaryIcon: "",
133− actions: [{ id: "reply_id", label: "Dummy"}],
134− }
135−
117+ var component = Qt.createComponent("Notification.qml")
118+ var n = component.createObject("notification", {"nid": index++,
119+ "type": Notification.Interactive,
120+ "hints": {},
121+ "summary": "Interactive notification",
122+ "body": "This is a notification that can be clicked",
123+ "icon": "../graphics/avatars/anna_olsson.png",
124+ "secondaryIcon": "",
125+ "rawActions": ["reply_id", "Dummy"]})
126+ n.completed.connect(mockModel.onCompleted)
136127 mockModel.append(n)
137128 }
138129
139130 function addConfirmationNotification() {
140− var n = {
141− type: Notification.Confirmation,
142− hints: {"x-canonical-non-shaped-icon": "true"},
143− summary: "Confirmation notification",
144− body: "",
145− icon: "image://theme/audio-volume-medium",
146− secondaryIcon: "",
147− value: 50,
148− actions: [],
149− }
150−
131+ var component = Qt.createComponent("Notification.qml")
132+ var n = component.createObject("notification", {"nid": index++,
133+ "type": Notification.Confirmation,
134+ "hints": {"x-canonical-non-shaped-icon": "true"},
135+ "summary": "Confirmation notification",
136+ "body": "",
137+ "icon": "image://theme/audio-volume-medium",
138+ "secondaryIcon": "",
139+ "value": 50,
140+ "rawActions": ["reply_id", "Dummy"]})
141+ n.completed.connect(mockModel.onCompleted)
151142 mockModel.append(n)
152143 }
153144
154145 function add2ndConfirmationNotification() {
155− var n = {
156− type: Notification.Confirmation,
157− hints: {"x-canonical-non-shaped-icon": "true",
158− "x-canonical-value-bar-tint": "true"},
159− summary: "Confirmation notification",
160− body: "High Volume",
161− icon: "image://theme/audio-volume-high",
162− secondaryIcon: "",
163− value: 85,
164− actions: [],
165− }
166−
146+ var component = Qt.createComponent("Notification.qml")
147+ var n = component.createObject("notification", {"nid": index++,
148+ "type": Notification.Confirmation,
149+ "hints": {"x-canonical-non-shaped-icon": "true",
150+ "x-canonical-value-bar-tint": "true"},
151+ "summary": "Confirmation notification",
152+ "body": "High Volume",
153+ "icon": "image://theme/audio-volume-high",
154+ "secondaryIcon": "",
155+ "value": 85,
156+ "rawActions": ["reply_id", "Dummy"]})
157+ n.completed.connect(mockModel.onCompleted)
167158 mockModel.append(n)
168159 }
169160
@@ -174,8 +165,9 @@ Item {
174165 }
175166
176167 function remove1stNotification() {
177− if (mockModel.count > 1)
178− mockModel.remove(1)
168+ if (mockModel.count > 1) {
169+ mockModel.removeSecond()
170+ }
179171 }
180172
181173 Rectangle {
@@ -273,41 +265,122 @@ Item {
273265 name: "NotificationRendererTest"
274266 when: windowShown
275267
268+ property list<Notification> nlist: [
269+ Notification {
270+ nid: 1
271+ type: Notification.Ephemeral
272+ summary: "Photo upload completed"
273+ body: ""
274+ icon: "../graphics/applicationIcons/facebook.png"
275+ secondaryIcon: ""
276+ value: 0
277+ rawActions: []
278+ },
279+ Notification {
280+ nid: 2
281+ type: Notification.Ephemeral
282+ hints: {"x-canonical-private-affirmative-tint": "false",
283+ "sound-file": "dummy.ogg",
284+ "suppress-sound": "true"}
285+ summary: "New comment successfully published"
286+ body: ""
287+ icon: ""
288+ secondaryIcon: "../graphics/applicationIcons/facebook.png"
289+ value: 0
290+ rawActions: []
291+ },
292+ Notification {
293+ nid: 3
294+ type: Notification.Interactive
295+ hints: {"x-canonical-private-affirmative-tint": "false",
296+ "sound-file": "dummy.ogg"}
297+ summary: "Interactive notification"
298+ body: "This is a notification that can be clicked"
299+ icon: "../graphics/avatars/amanda.png"
300+ secondaryIcon: ""
301+ value: 0
302+ rawActions: ["reply_id", "Dummy"]
303+ },
304+ Notification {
305+ nid: 4
306+ type: Notification.SnapDecision
307+ hints: {"x-canonical-private-affirmative-tint": "false",
308+ "sound-file": "dummy.ogg"}
309+ summary: "Bro Coly"
310+ body: "At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet."
311+ icon: "../graphics/avatars/anna_olsson.png"
312+ secondaryIcon: ""
313+ value: 0
314+ rawActions: ["accept_id", "Accept",
315+ "reject_id", "Reject"]
316+ },
317+ Notification {
318+ nid: 5
319+ type: Notification.Ephemeral
320+ hints: {"x-canonical-private-affirmative-tint": "false",
321+ "sound-file": "dummy.ogg"}
322+ summary: "Cole Raby"
323+ body: "I did not expect it to be that late."
324+ icon: "../graphics/avatars/funky.png"
325+ secondaryIcon: "../graphics/applicationIcons/facebook.png"
326+ value: 0
327+ rawActions: []
328+ },
329+ Notification {
330+ nid: 6
331+ type: Notification.Ephemeral
332+ hints: {"x-canonical-private-affirmative-tint": "false",
333+ "x-canonical-non-shaped-icon": "true"}
334+ summary: "Contacts"
335+ body: "Synchronised contacts-database with cloud-storage."
336+ icon: "image://theme/contacts-app"
337+ secondaryIcon: ""
338+ value: 0
339+ rawActions: []
340+ },
341+ Notification {
342+ nid: 7
343+ type: Notification.Confirmation
344+ hints: {"x-canonical-non-shaped-icon": "true"}
345+ summary: ""
346+ body: ""
347+ icon: "image://theme/audio-volume-medium"
348+ secondaryIcon: ""
349+ value: 50
350+ rawActions: []
351+ },
352+ Notification {
353+ nid: 8
354+ type: Notification.Confirmation
355+ hints: {"x-canonical-non-shaped-icon": "true",
356+ "x-canonical-value-bar-tint" : "true"}
357+ summary: ""
358+ body: "High Volume"
359+ icon: "image://theme/audio-volume-high"
360+ secondaryIcon: ""
361+ value: 85
362+ rawActions: []
363+ },
364+ Notification {
365+ nid: 9
366+ type: Notification.SnapDecision
367+ hints: {"x-canonical-private-affirmative-tint": "true"}
368+ summary: "Theatre at Ferria Stadium"
369+ body: "at Ferria Stadium in Bilbao, Spain\n07578545317"
370+ icon: ""
371+ secondaryIcon: ""
372+ value: 0
373+ rawActions: ["ok_id", "Ok",
374+ "snooze_id", "Snooze",
375+ "view_id", "View"]
376+ }
377+ ]
378+
276379 function test_NotificationRenderer_data() {
277380 return [
278− {
279− tag: "2-over-1 Snap Decision with button-tint",
280− type: Notification.SnapDecision,
281− hints: {"x-canonical-private-affirmative-tint": "true"},
282− summary: "Theatre at Ferria Stadium",
283− body: "at Ferria Stadium in Bilbao, Spain\n07578545317",
284− icon: "",
285− secondaryIcon: "",
286− actions: [{ id: "ok_id", label: "Ok"},
287− { id: "snooze_id", label: "Snooze"},
288− { id: "view_id", label: "View"}],
289− summaryVisible: true,
290− bodyVisible: true,
291− iconVisible: false,
292− centeredIconVisible: false,
293− shaped: false,
294− secondaryIconVisible: false,
295− buttonRowVisible: false,
296− buttonTinted: true,
297− hasSound: false,
298− valueVisible: false,
299− valueLabelVisible: false,
300− valueTinted: false
301− },
302381 {
303382 tag: "Ephemeral notification - icon-summary layout",
304− type: Notification.Ephemeral,
305− hints: {},
306− summary: "Photo upload completed",
307− body: "",
308− icon: "../graphics/applicationIcons/facebook.png",
309− secondaryIcon: "",
310− actions: [],
383+ n: nlist[0],
311384 summaryVisible: true,
312385 bodyVisible: false,
313386 iconVisible: true,
@@ -323,15 +396,7 @@ Item {
323396 },
324397 {
325398 tag: "Ephemeral notification - check suppression of secondary icon for icon-summary layout",
326− type: Notification.Ephemeral,
327− hints: {"x-canonical-private-affirmative-tint": "false",
328− "sound-file": "dummy.ogg",
329− "suppress-sound": "true"},
330− summary: "New comment successfully published",
331− body: "",
332− icon: "",
333− secondaryIcon: "../graphics/applicationIcons/facebook.png",
334− actions: [],
399+ n: nlist[1],
335400 summaryVisible: true,
336401 bodyVisible: false,
337402 interactiveAreaEnabled: false,
@@ -348,14 +413,7 @@ Item {
348413 },
349414 {
350415 tag: "Interactive notification",
351− type: Notification.Interactive,
352− hints: {"x-canonical-private-affirmative-tint": "false",
353− "sound-file": "dummy.ogg"},
354− summary: "Interactive notification",
355− body: "This is a notification that can be clicked",
356− icon: "../graphics/avatars/amanda.png",
357− secondaryIcon: "",
358− actions: [{ id: "reply_id", label: "Dummy"}],
416+ n: nlist[2],
359417 summaryVisible: true,
360418 bodyVisible: true,
361419 iconVisible: true,
@@ -371,15 +429,7 @@ Item {
371429 },
372430 {
373431 tag: "Snap Decision without secondary icon and no button-tint",
374− type: Notification.SnapDecision,
375− hints: {"x-canonical-private-affirmative-tint": "false",
376− "sound-file": "dummy.ogg"},
377− summary: "Bro Coly",
378− body: "At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.",
379− icon: "../graphics/avatars/anna_olsson.png",
380− secondaryIcon: "",
381− actions: [{ id: "accept_id", label: "Accept"},
382− { id: "reject_id", label: "Reject"}],
432+ n: nlist[3],
383433 summaryVisible: true,
384434 bodyVisible: true,
385435 iconVisible: true,
@@ -395,14 +445,7 @@ Item {
395445 },
396446 {
397447 tag: "Ephemeral notification",
398− type: Notification.Ephemeral,
399− hints: {"x-canonical-private-affirmative-tint": "false",
400− "sound-file": "dummy.ogg"},
401− summary: "Cole Raby",
402− body: "I did not expect it to be that late.",
403− icon: "../graphics/avatars/funky.png",
404− secondaryIcon: "../graphics/applicationIcons/facebook.png",
405− actions: [],
448+ n: nlist[4],
406449 summaryVisible: true,
407450 bodyVisible: true,
408451 iconVisible: true,
@@ -418,14 +461,7 @@ Item {
418461 },
419462 {
420463 tag: "Ephemeral notification with non-shaped icon",
421− type: Notification.Ephemeral,
422− hints: {"x-canonical-private-affirmative-tint": "false",
423− "x-canonical-non-shaped-icon": "true"},
424− summary: "Contacts",
425− body: "Synchronised contacts-database with cloud-storage.",
426− icon: "image://theme/contacts-app",
427− secondaryIcon: "",
428− actions: [],
464+ n: nlist[5],
429465 summaryVisible: true,
430466 bodyVisible: true,
431467 iconVisible: true,
@@ -441,14 +477,7 @@ Item {
441477 },
442478 {
443479 tag: "Confirmation notification with value",
444− type: Notification.Confirmation,
445− hints: {"x-canonical-non-shaped-icon": "true"},
446− summary: "",
447− body: "",
448− icon: "image://theme/audio-volume-medium",
449− secondaryIcon: "",
450− value: 50,
451− actions: [],
480+ n: nlist[6],
452481 summaryVisible: false,
453482 bodyVisible: false,
454483 iconVisible: false,
@@ -464,15 +493,7 @@ Item {
464493 },
465494 {
466495 tag: "Confirmation notification with value, label and tint",
467− type: Notification.Confirmation,
468− hints: {"x-canonical-non-shaped-icon": "true",
469− "x-canonical-value-bar-tint" : "true"},
470− summary: "",
471− body: "High Volume",
472− icon: "image://theme/audio-volume-high",
473− secondaryIcon: "",
474− value: 85,
475− actions: [],
496+ n: nlist[7],
476497 summaryVisible: false,
477498 bodyVisible: false,
478499 iconVisible: false,
@@ -485,6 +506,22 @@ Item {
485506 valueVisible: true,
486507 valueLabelVisible: true,
487508 valueTinted: true
509+ },
510+ {
511+ tag: "2-over-1 Snap Decision with button-tint",
512+ n: nlist[8],
513+ summaryVisible: true,
514+ bodyVisible: true,
515+ iconVisible: false,
516+ centeredIconVisible: false,
517+ shaped: false,
518+ secondaryIconVisible: false,
519+ buttonRowVisible: false,
520+ buttonTinted: true,
521+ hasSound: false,
522+ valueVisible: false,
523+ valueLabelVisible: false,
524+ valueTinted: false
488525 }
489526 ]
490527 }
@@ -509,8 +546,14 @@ Item {
509546 }
510547
511548 function test_NotificationRenderer(data) {
549+ // make sure the clicks on mocked notifications can be checked against by "actionSpy" (mimicking the NotificationServer component)
550+ data.n.actionInvoked.connect(mockModel.actionInvoked)
551+
552+ // hook up notification's completed-signal with model's onCompleted-slot, so that remove() (model) happens on close() (notification)
553+ data.n.completed.connect(mockModel.onCompleted)
554+
512555 // populate model with some mock notifications
513− mockModel.append(data)
556+ mockModel.append(data.n)
514557
515558 // make sure the view is properly updated before going on
516559 notifications.forceLayout();
@@ -548,9 +591,9 @@ Item {
548591
549592 // test input does not fall through
550593 mouseClick(notification)
551− if(data.type == Notification.Interactive) {
594+ if(data.n.type === Notification.Interactive) {
552595 actionSpy.wait()
553− compare(actionSpy.signalArguments[0][0], data.actions[0]["id"], "got wrong id for interactive action")
596+ compare(actionSpy.signalArguments[0][0], data.n.actions.data(0, ActionModel.RoleActionId), "got wrong id for interactive action")
554597 }
555598 compare(clickThroughSpy.count, 0, "click on notification fell through")
556599
@@ -559,17 +602,19 @@ Item {
559602 compare(bodyLabel.visible, data.bodyVisible, "body-text visibility is incorrect")
560603 compare(buttonRow.visible, data.buttonRowVisible, "button visibility is incorrect")
561604
562− var audioItem = findInvisibleChild(notification, "sound")
563− compare(audioItem.playbackState, data.hasSound ? Audio.PlayingState : Audio.StoppedState, "Audio has wrong state")
605+ if (data.hasSound) {
606+ var audioItem = findInvisibleChild(notification, "sound")
607+ compare(audioItem.playbackState, data.hasSound ? Audio.PlayingState : Audio.StoppedState, "Audio has wrong state")
608+ }
564609
565610 if(data.buttonRowVisible) {
566611 var buttonCancel = findChild(buttonRow, "notify_button1")
567612 var buttonAccept = findChild(buttonRow, "notify_button0")
568613
569614 // only test the left/cancel-button if two actions have been passed in
570− if (data.actions.length == 2) {
615+ if (data.n.actions.count === 2) {
571616 tryCompareFunction(function() { mouseClick(buttonCancel); return actionSpy.signalArguments.length > 0; }, true);
572− compare(actionSpy.signalArguments[0][0], data.actions[1]["id"], "got wrong id for negative action")
617+ compare(actionSpy.signalArguments[0][0], data.n.actions.data(1, ActionModel.RoleActionId), "got wrong id for negative action")
573618 actionSpy.clear()
574619 }
575620
@@ -578,36 +623,49 @@ Item {
578623
579624 // click the positive/right button
580625 tryCompareFunction(function() { mouseClick(buttonAccept); return actionSpy.signalArguments.length > 0; }, true);
581− compare(actionSpy.signalArguments[0][0], data.actions[0]["id"], "got wrong id positive action")
626+ compare(actionSpy.signalArguments[0][0], data.n.actions.data(0, ActionModel.RoleActionId), "got wrong id positive action")
582627 actionSpy.clear()
583− waitForRendering (notification)
584628
585629 // check if there's a ComboButton created due to more actions being passed
586− if (data.actions.length > 2) {
630+ if (data.n.actions.count > 3) {
587631 var comboButton = findChild(notification, "notify_button2")
588− tryCompareFunction(function() { return comboButton.expanded == false; }, true);
632+ tryCompareFunction(function() { return comboButton.expanded === false; }, true);
589633
590634 // click to expand
591− tryCompareFunction(function() { mouseClick(comboButton, comboButton.width - comboButton.__styleInstance.dropDownWidth / 2, comboButton.height / 2); return comboButton.expanded == true; }, true);
635+ tryCompareFunction(function() { mouseClick(comboButton, comboButton.width / 2, comboButton.height / 2); return comboButton.expanded === true; }, true);
592636
593637 // try clicking on choices in expanded comboList
594638 var choiceButton1 = findChild(notification, "notify_button3")
595639 tryCompareFunction(function() { mouseClick(choiceButton1); return actionSpy.signalArguments.length > 0; }, true);
596− compare(actionSpy.signalArguments[0][0], data.actions[3]["id"], "got wrong id choice action 1")
640+ compare(actionSpy.signalArguments[0][0], data.n.actions.data(3, ActionModel.RoleActionId), "got wrong id choice action 1")
597641 actionSpy.clear()
598642
599643 var choiceButton2 = findChild(notification, "notify_button4")
600644 tryCompareFunction(function() { mouseClick(choiceButton2); return actionSpy.signalArguments.length > 0; }, true);
601− compare(actionSpy.signalArguments[0][0], data.actions[4]["id"], "got wrong id choice action 2")
645+ compare(actionSpy.signalArguments[0][0], data.n.actions.data(4, ActionModel.RoleActionId), "got wrong id choice action 2")
602646 actionSpy.clear()
603647
604648 // click to collapse
605− //tryCompareFunction(function() { mouseClick(comboButton, comboButton.width - comboButton.__styleInstance.dropDownWidth / 2, comboButton.height / 2); return comboButton.expanded == false; }, true);
649+ tryCompareFunction(function() { mouseClick(comboButton, comboButton.width / 2, comboButton.height / 2); return comboButton.expanded == false; }, true);
606650 } else {
607651 mouseClick(buttonCancel)
608− compare(actionSpy.signalArguments[0][0], data.actions[1]["id"], "got wrong id for negative action")
652+ compare(actionSpy.signalArguments[0][0], data.n.actions.data(1, ActionModel.RoleActionId), "got wrong id for negative action")
609653 }
610654 }
655+
656+ // swipe-to-dismiss check
657+ waitForRendering(notification)
658+ var before = mockModel.count
659+ var dragStart = notification.width * 0.25;
660+ var dragEnd = notification.width;
661+ var dragY = notification.height / 2;
662+ touchFlick(notification, dragStart, dragY, dragEnd, dragY)
663+ waitForRendering(notification)
664+ if ((data.n.type === Notification.SnapDecision && notification.state === "expanded") || data.n.type === Notification.Confirmation) {
665+ tryCompare(mockModel, "count", before)
666+ } else {
667+ tryCompare(mockModel, "count", before - 1)
668+ }
611669 }
612670 }
613671 }
tests/qmltests/Notifications/tst_OptionToggle.qml+11−6
@@ -1,17 +1,20 @@
11/*
2− * Copyright (C) 2014 Canonical, Ltd.
2+ * Copyright 2015 Canonical Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU General Public License as published by
5+ * it under the terms of the GNU Lesser General Public License as published by
66 * the Free Software Foundation; version 3.
77 *
88 * This program is distributed in the hope that it will be useful,
99 * but WITHOUT ANY WARRANTY; without even the implied warranty of
1010 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU General Public License for more details.
11+ * GNU Lesser General Public License for more details.
1212 *
13− * You should have received a copy of the GNU General Public License
13+ * You should have received a copy of the GNU Lesser General Public License
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ *
16+ * Authors:
17+ * Mirco Mueller <mirco.mueller@canonical.com>
1518 */
1619
1720import QtQuick 2.0
@@ -235,8 +238,10 @@ Row {
235238 compare(bodyLabel.visible, data.bodyVisible, "body-text visibility is incorrect")
236239 compare(buttonRow.visible, data.buttonRowVisible, "button visibility is incorrect")
237240
238− var audioItem = findInvisibleChild(notification, "sound")
239− compare(audioItem.playbackState, data.hasSound ? Audio.PlayingState : Audio.StoppedState, "Audio has wrong state")
241+ if (data.hasSound) {
242+ var audioItem = findInvisibleChild(notification, "sound")
243+ compare(audioItem.playbackState, data.hasSound ? Audio.PlayingState : Audio.StoppedState, "Audio has wrong state")
244+ }
240245
241246 if(data.buttonRowVisible) {
242247 var buttonCancel = findChild(buttonRow, "notify_button1")
tests/qmltests/Notifications/tst_SwipeToAct.qml+4
@@ -218,6 +218,10 @@ Item {
218218 // populate model with some mock notifications
219219 mockModel.append(data)
220220
221+ // add actions to action-model to test against
222+ myActionModel.append("ok_id", "Ok")
223+ myActionModel.append("cancel_id", "Cancel")
224+
221225 // make sure the view is properly updated before going on
222226 notifications.forceLayout();
223227 waitForRendering(notifications);
tests/qmltests/Panel/tst_Panel.qml+33−2
@@ -1,5 +1,5 @@
11/*
2− * Copyright 2013-2014 Canonical Ltd.
2+ * Copyright 2013-2015 Canonical Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -41,7 +41,12 @@ IndicatorTest {
4141 Layout.fillHeight: true
4242
4343 id: itemArea
44− color: "blue"
44+ color: backgroundMouseArea.pressed ? "red" : "blue"
45+
46+ MouseArea {
47+ id: backgroundMouseArea
48+ anchors.fill: parent
49+ }
4550
4651 Panel {
4752 id: panel
@@ -128,6 +133,12 @@ IndicatorTest {
128133 name: "Panel"
129134 when: windowShown
130135
136+ SignalSpy {
137+ id: backgroundPressedSpy
138+ target: backgroundMouseArea
139+ signalName: "pressedChanged"
140+ }
141+
131142 function init() {
132143 panel.fullscreenMode = false;
133144 callManager.foregroundCall = null;
@@ -140,6 +151,9 @@ IndicatorTest {
140151 // (switches between normal and fullscreen modes are animated)
141152 var indicatorArea = findChild(panel, "indicatorArea");
142153 tryCompare(indicatorArea, "y", 0);
154+
155+ backgroundPressedSpy.clear();
156+ compare(backgroundPressedSpy.valid, true);
143157 }
144158
145159 function get_indicator_item(index) {
@@ -299,5 +313,22 @@ IndicatorTest {
299313
300314 touchRelease(panel, mappedPosition.x, panel.minimizedPanelHeight / 2);
301315 }
316+
317+ /* Checks that no input reaches items behind the indicator bar.
318+ Ie., the indicator bar should eat all input events that hit it.
319+ */
320+ function test_indicatorBarEatsAllEvents() {
321+ // Perform several taps throughout the length of the indicator bar to ensure
322+ // that it doesn't have a "weak spot" from where taps pass through.
323+ var numTaps = 5;
324+ var stepLength = (panel.width / (numTaps + 1));
325+ var tapY = panel.indicators.minimizedPanelHeight / 2;
326+ for (var i = 1; i <= numTaps; ++i) {
327+ tap(panel, stepLength * i, tapY);
328+ tryCompare(panel.indicators, "fullyClosed", true);
329+ }
330+
331+ compare(backgroundPressedSpy.count, 0);
332+ }
302333 }
303334}
tests/qmltests/Stages/ApplicationCheckBox.qmlnew file+45
@@ -0,0 +1,45 @@
1+/*
2+ * Copyright (C) 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 General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.0
18+import QtQuick.Layouts 1.1
19+import Ubuntu.Components 1.1
20+import Unity.Application 0.1
21+
22+RowLayout {
23+ id: root
24+ property string appId
25+ property alias checked: checkbox.checked
26+
27+ Layout.fillWidth: true
28+ CheckBox {
29+ id: checkbox
30+ checked: false
31+ activeFocusOnPress: false
32+ onCheckedChanged: {
33+ if (checked) {
34+ ApplicationManager.startApplication(root.appId);
35+ } else {
36+ ApplicationManager.stopApplication(root.appId);
37+ }
38+ }
39+ }
40+ Label {
41+ text: root.appId
42+ color: "white"
43+ anchors.verticalCenter: parent.verticalCenter
44+ }
45+}
tests/qmltests/Stages/RecursingChildSessionControl.qml+3
@@ -89,6 +89,7 @@ ColumnLayout {
8989 CheckBox {
9090 id: _surfaceCheckbox;
9191 checked: false;
92+ activeFocusOnPress: false
9293 enabled: root.session
9394 onCheckedChanged: {
9495 if (checked) {
@@ -117,6 +118,7 @@ ColumnLayout {
117118
118119 Button {
119120 enabled: root.session
121+ activeFocusOnPress: false
120122 text: removable ? "Remove" : "Release"
121123 onClicked: {
122124 if (removable) {
@@ -131,6 +133,7 @@ ColumnLayout {
131133
132134 Button {
133135 enabled: root.session !== null
136+ activeFocusOnPress: false
134137 text: "Add Child"
135138 onClicked: {
136139 var screenshot = Math.round(Math.random()*100 % (screenshotIds.length-1));
tests/qmltests/Stages/tst_ApplicationWindow.qml+6−1
@@ -52,10 +52,12 @@ Rectangle {
5252 application: fakeApplication
5353 orientation: Qt.PortraitOrientation
5454 interactive: true
55+ focus: true
5556 }
5657 }
5758 Loader {
5859 id: applicationWindowLoader
60+ focus: true
5961 anchors {
6062 top: parent.top
6163 bottom: parent.bottom
@@ -83,6 +85,7 @@ Rectangle {
8385
8486 CheckBox {
8587 id: sessionCheckbox; checked: false
88+ activeFocusOnPress: false
8689 onCheckedChanged: {
8790 if (applicationWindowLoader.status !== Loader.Ready)
8891 return;
@@ -121,6 +124,7 @@ Rectangle {
121124
122125 ListItem.ItemSelector {
123126 id: appStateSelector
127+ activeFocusOnPress: false
124128 anchors { left: parent.left; right: parent.right }
125129 text: "Application state"
126130 model: ["Starting",
@@ -146,6 +150,7 @@ Rectangle {
146150
147151 Button {
148152 anchors { left: parent.left; right: parent.right }
153+ activeFocusOnPress: false
149154 text: "Rotate device \u27F3"
150155 onClicked: {
151156 var orientation = applicationWindowLoader.item.orientation
@@ -398,7 +403,7 @@ Rectangle {
398403 verify(stateGroup.state === "void");
399404 }
400405
401− function test_forceActiveFocusFollowsInterative() {
406+ function test_surfaceActiveFocusFollowsAppWindowInterative() {
402407 fakeApplication.createSession();
403408 applicationWindowLoader.item.interactive = false;
404409 applicationWindowLoader.item.interactive = true;
tests/qmltests/Stages/tst_PhoneStage.qml+4
@@ -29,6 +29,7 @@ Item {
2929 PhoneStage {
3030 id: phoneStage
3131 anchors { fill: parent; rightMargin: units.gu(30) }
32+ focus: true
3233 dragAreaWidth: units.gu(2)
3334 maximizedAppTopMargin: units.gu(3) + units.dp(2)
3435 interactive: true
@@ -51,6 +52,7 @@ Item {
5152 Button {
5253 anchors { left: parent.left; right: parent.right }
5354 text: "Add App"
55+ activeFocusOnPress: false
5456 onClicked: {
5557 testCase.addApps();
5658 }
@@ -58,6 +60,7 @@ Item {
5860 Button {
5961 anchors { left: parent.left; right: parent.right }
6062 text: "Remove Selected"
63+ activeFocusOnPress: false
6164 onClicked: {
6265 ApplicationManager.stopApplication(ApplicationManager.get(appList.selectedAppIndex).appId);
6366 }
@@ -65,6 +68,7 @@ Item {
6568 Button {
6669 anchors { left: parent.left; right: parent.right }
6770 text: "Stop Selected"
71+ activeFocusOnPress: false
6872 onClicked: {
6973 ApplicationManager.get(appList.selectedAppIndex).setState(ApplicationInfoInterface.Stopped);
7074 }
tests/qmltests/Stages/tst_SessionContainer.qml+14−7
@@ -42,11 +42,14 @@ Rectangle {
4242 id: sessionContainer
4343 anchors.fill: parent
4444 orientation: Qt.PortraitOrientation
45+ focus: true
46+ interactive: true
4547 }
4648 }
4749
4850 Loader {
4951 id: sessionContainerLoader
52+ focus: true
5053 anchors {
5154 top: parent.top
5255 bottom: parent.bottom
@@ -75,6 +78,7 @@ Rectangle {
7578 CheckBox {
7679 id: sessionCheckbox;
7780 checked: false;
81+ activeFocusOnPress: false
7882 onCheckedChanged: {
7983 if (sessionContainerLoader.status !== Loader.Ready)
8084 return;
@@ -190,8 +194,6 @@ Rectangle {
190194 var sessionContainer = sessionContainerLoader.item;
191195 compare(sessionContainer.childSessions.count(), 0);
192196
193− sessionContainer.interactive = true;
194−
195197 var i;
196198 var sessions = [];
197199 // 3 sessions should cover all edge cases
@@ -209,14 +211,19 @@ Rectangle {
209211
210212 var a_session;
211213 while(a_session = sessions.pop()) {
212− a_session.surface.forceActiveFocus();
213214 compare(a_session.surface.activeFocus, true);
214215
215− var parentSession = a_session.parentSession;
216− sessionContainer.session.removeChildSession(a_session);
217− compare(a_session.surface.activeFocus, false);
216+ ApplicationTest.removeSurface(a_session.surface);
217+ ApplicationTest.removeSession(a_session);
218218
219− compare(parentSession.surface.activeFocus, true);
219+ if (sessions.length > 0) {
220+ // active focus should have gone to the yongest remaining sibling
221+ var previousSiblingSurface = sessions[sessions.length - 1].surface
222+ tryCompare(previousSiblingSurface, "activeFocus", true);
223+ } else {
224+ // active focus should have gone to the parent surface
225+ tryCompare(sessionContainer.session.surface, "activeFocus", true);
226+ }
220227 }
221228 }
222229
tests/qmltests/Stages/tst_TabletStage.qmlnew file+154
@@ -0,0 +1,154 @@
1+/*
2+ * Copyright (C) 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 General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.0
18+import QtTest 1.0
19+import Ubuntu.Components 1.1
20+import Ubuntu.Components.ListItems 1.0 as ListItem
21+import Unity.Application 0.1
22+import Unity.Test 0.1
23+
24+import "../../../qml/Stages"
25+
26+Rectangle {
27+ id: root
28+ color: "grey"
29+ width: tabletStageLoader.width + controls.width
30+ height: tabletStageLoader.height
31+
32+ Loader {
33+ id: tabletStageLoader
34+
35+ x: ((root.width - controls.width) - width) / 2
36+ y: (root.height - height) / 2
37+ width: units.gu(160*0.7)
38+ height: units.gu(100*0.7)
39+
40+ focus: true
41+
42+ property bool itemDestroyed: false
43+ sourceComponent: Component {
44+ TabletStage {
45+ anchors.fill: parent
46+ Component.onDestruction: {
47+ tabletStageLoader.itemDestroyed = true;
48+ }
49+ dragAreaWidth: units.gu(2)
50+ maximizedAppTopMargin: units.gu(3) + units.dp(2)
51+ interactive: true
52+ focus: true
53+ }
54+ }
55+ }
56+
57+ Rectangle {
58+ id: controls
59+ color: "darkgrey"
60+ width: units.gu(30)
61+ anchors {
62+ top: parent.top
63+ bottom: parent.bottom
64+ right: parent.right
65+ }
66+
67+ Column {
68+ anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
69+ spacing: units.gu(1)
70+ ApplicationCheckBox {
71+ id: webbrowserCheckBox
72+ appId: "webbrowser-app"
73+ }
74+ ApplicationCheckBox {
75+ id: dialerCheckBox
76+ appId: "dialer-app"
77+ }
78+ }
79+ }
80+
81+ UnityTestCase {
82+ id: testCase
83+ name: "TabletStage"
84+ when: windowShown
85+
86+ property Item tabletStage: tabletStageLoader.status === Loader.Ready ? tabletStageLoader.item : null
87+
88+ function init() {
89+ tabletStageLoader.active = true;
90+ tryCompare(tabletStageLoader, "status", Loader.Ready);
91+ }
92+
93+ function cleanup() {
94+ tabletStageLoader.itemDestroyed = false;
95+ tabletStageLoader.active = false;
96+
97+ tryCompare(tabletStageLoader, "status", Loader.Null);
98+ tryCompare(tabletStageLoader, "item", null);
99+ // Loader.status might be Loader.Null and Loader.item might be null but the Loader
100+ // actually took place. Likely because Loader waits until the next event loop
101+ // iteration to do its work. So to ensure the reload, we will wait until the
102+ // Shell instance gets destroyed.
103+ tryCompare(tabletStageLoader, "itemDestroyed", true);
104+
105+ // kill all (fake) running apps
106+ webbrowserCheckBox.checked = false;
107+ dialerCheckBox.checked = false;
108+ }
109+
110+ function waitUntilAppSurfaceShowsUp(appId) {
111+ var appWindow = findChild(tabletStage, "appWindow_" + appId);
112+ verify(appWindow);
113+ var appWindowStates = findInvisibleChild(appWindow, "applicationWindowStateGroup");
114+ verify(appWindowStates);
115+ tryCompare(appWindowStates, "state", "surface");
116+ }
117+
118+ function test_tappingSwitchesFocusBetweenStages() {
119+ webbrowserCheckBox.checked = true;
120+ waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId);
121+ var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
122+ compare(webbrowserApp.stage, ApplicationInfoInterface.MainStage);
123+ tryCompare(webbrowserApp.session.surface, "activeFocus", true);
124+
125+ dialerCheckBox.checked = true;
126+ waitUntilAppSurfaceShowsUp(dialerCheckBox.appId);
127+ var dialerApp = ApplicationManager.findApplication(dialerCheckBox.appId);
128+ compare(dialerApp.stage, ApplicationInfoInterface.SideStage);
129+ tryCompare(dialerApp.session.surface, "activeFocus", true);
130+ tryCompare(webbrowserApp.session.surface, "activeFocus", false);
131+
132+ // Tap on the main stage application and check if the focus
133+ // has been passed to it.
134+
135+ var webbrowserWindow = findChild(tabletStage, "appWindow_" + webbrowserApp.appId);
136+ verify(webbrowserWindow);
137+ tap(webbrowserWindow);
138+
139+ tryCompare(dialerApp.session.surface, "activeFocus", false);
140+ tryCompare(webbrowserApp.session.surface, "activeFocus", true);
141+
142+ // Now tap on the side stage application and check if the focus
143+ // has been passed back to it.
144+
145+ var dialerWindow = findChild(tabletStage, "appWindow_" + dialerApp.appId);
146+ verify(dialerWindow);
147+ tap(dialerWindow);
148+
149+ tryCompare(dialerApp.session.surface, "activeFocus", true);
150+ tryCompare(webbrowserApp.session.surface, "activeFocus", false);
151+ }
152+ }
153+
154+}
tests/qmltests/Stages/tst_WindowMoveResizeArea.qml+117−18
@@ -1,5 +1,5 @@
11/*
2− * Copyright 2014 Canonical Ltd.
2+ * Copyright 2014-2015 Canonical Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -29,20 +29,46 @@ Item {
2929 height: units.gu(60)
3030 width: units.gu(60)
3131
32− Rectangle {
33− id: fakeWindow
34− height: units.gu(20)
35− width: units.gu(20)
36− color: "khaki"
37− WindowMoveResizeArea {
38− id: moveResizeArea
39− target: fakeWindow
40− resizeHandleWidth: units.gu(0.5)
41− minWidth: units.gu(15)
42− minHeight: units.gu(10)
32+ property var fakeWindow: windowLoader.item
33+
34+ Component {
35+ id: fakeWindowComponent
36+
37+ Item {
38+ id: fakeWindow
39+ property alias minWidth: moveResizeArea.minWidth
40+ property alias minHeight: moveResizeArea.minHeight
41+ x: units.gu(20)
42+ y: units.gu(20)
43+ height: units.gu(20)
44+ width: units.gu(20)
45+
46+ WindowMoveResizeArea {
47+ id: moveResizeArea
48+ target: fakeWindow
49+ resizeHandleWidth: units.gu(0.5)
50+ minWidth: units.gu(15)
51+ minHeight: units.gu(10)
52+ windowId: "test-window-id"
53+ }
54+
55+ Rectangle {
56+ anchors.fill: moveResizeArea
57+ color: "red"
58+ }
59+
60+ Rectangle {
61+ anchors.fill: fakeWindow
62+ color: "blue"
63+ }
4364 }
4465 }
4566
67+ Loader {
68+ id: windowLoader
69+ sourceComponent: fakeWindowComponent
70+ }
71+
4672 UT.UnityTestCase {
4773 when: windowShown
4874
@@ -100,8 +126,8 @@ Item {
100126 var startDragY = initialWindowY + initialWindowHeight + 1
101127 mouseFlick(root, startDragX, startDragY, startDragX + data.dx, startDragY + data.dy, true, true, units.gu(.5), 10);
102128
103− tryCompare(fakeWindow, "width", Math.max(initialWindowWidth + data.dx, moveResizeArea.minWidth));
104− tryCompare(fakeWindow, "height", Math.max(initialWindowHeight + data.dy, moveResizeArea.minHeight));
129+ tryCompare(fakeWindow, "width", Math.max(initialWindowWidth + data.dx, fakeWindow.minWidth));
130+ tryCompare(fakeWindow, "height", Math.max(initialWindowHeight + data.dy, fakeWindow.minHeight));
105131
106132 compare(fakeWindow.x, initialWindowX);
107133 compare(fakeWindow.y, initialWindowY);
@@ -126,13 +152,86 @@ Item {
126152 var startDragY = initialWindowY - 1
127153 mouseFlick(root, startDragX, startDragY, startDragX + data.dx, startDragY + data.dy, true, true, units.gu(.5), 10);
128154
129− tryCompare(fakeWindow, "width", Math.max(initialWindowWidth - data.dx, moveResizeArea.minWidth));
130− tryCompare(fakeWindow, "height", Math.max(initialWindowHeight - data.dy, moveResizeArea.minHeight));
155+ tryCompare(fakeWindow, "width", Math.max(initialWindowWidth - data.dx, fakeWindow.minWidth));
156+ tryCompare(fakeWindow, "height", Math.max(initialWindowHeight - data.dy, fakeWindow.minHeight));
131157
132− var maxMoveX = initialWindowWidth - moveResizeArea.minWidth;
133− var maxMoveY = initialWindowHeight - moveResizeArea.minHeight;
158+ var maxMoveX = initialWindowWidth - fakeWindow.minWidth;
159+ var maxMoveY = initialWindowHeight - fakeWindow.minHeight;
134160 compare(fakeWindow.x, Math.min(initialWindowX + data.dx, initialWindowX + maxMoveX));
135161 compare(fakeWindow.y, Math.min(initialWindowY + data.dy, initialWindowY + maxMoveY));
136162 }
163+
164+ function test_saveRestorePosition() {
165+ var initialWindowX = fakeWindow.x;
166+ var initialWindowY = fakeWindow.y;
167+ var initialWindowWidth = fakeWindow.width;
168+ var initialWindowHeight = fakeWindow.height;
169+
170+ var moveDelta = units.gu(5);
171+ var startDragX = initialWindowX + fakeWindow.width / 2;
172+ var startDragY = initialWindowY + fakeWindow.height / 2;
173+ mouseFlick(root, startDragX, startDragY, startDragX + moveDelta, startDragY + moveDelta, true, true, units.gu(.5), 10)
174+
175+ tryCompare(fakeWindow, "x", initialWindowX + moveDelta)
176+ tryCompare(fakeWindow, "y", initialWindowX + moveDelta)
177+
178+ // This will destroy the window and recreate it
179+ windowLoader.active = false;
180+ waitForRendering(root);
181+ windowLoader.active = true;
182+
183+ // Make sure it's again where we left it before destroying
184+ tryCompare(fakeWindow, "x", initialWindowX + moveDelta)
185+ tryCompare(fakeWindow, "y", initialWindowX + moveDelta)
186+ }
187+
188+ function test_saveRestoreSize() {
189+ var initialWindowX = fakeWindow.x;
190+ var initialWindowY = fakeWindow.y;
191+ var initialWindowWidth = fakeWindow.width
192+ var initialWindowHeight = fakeWindow.height
193+
194+ var resizeDelta = units.gu(5)
195+ var startDragX = initialWindowX + initialWindowWidth + 1
196+ var startDragY = initialWindowY + initialWindowHeight + 1
197+ mouseFlick(root, startDragX, startDragY, startDragX + resizeDelta, startDragY + resizeDelta, true, true, units.gu(.5), 10);
198+
199+ tryCompare(fakeWindow, "width", Math.max(initialWindowWidth + resizeDelta, fakeWindow.minWidth));
200+ tryCompare(fakeWindow, "height", Math.max(initialWindowHeight + resizeDelta, fakeWindow.minHeight));
201+
202+ // This will destroy the window and recreate it
203+ windowLoader.active = false;
204+ waitForRendering(root);
205+ windowLoader.active = true;
206+
207+ // Make sure its size is again the same as before
208+ tryCompare(fakeWindow, "width", Math.max(initialWindowWidth + resizeDelta, fakeWindow.minWidth));
209+ tryCompare(fakeWindow, "height", Math.max(initialWindowHeight + resizeDelta, fakeWindow.minHeight));
210+ }
211+
212+ // This tests if dragging smaller than minSize and then larger again, will keep the edge sticking
213+ // to the mouse, instead of immediately making the window grow again when switching direction
214+ function test_resizeSmallerAndLarger_data() {
215+ return [
216+ { tag: "topLeft", startX: -1, startY: -1, dx: units.gu(15), dy: units.gu(15) },
217+ { tag: "bottomRight", startX: fakeWindow.width + 1, startY: fakeWindow.height + 1, dx: -units.gu(15), dy: -units.gu(15) }
218+ ]
219+ }
220+
221+ function test_resizeSmallerAndLarger(data) {
222+ var initialWindowX = fakeWindow.x;
223+ var initialWindowY = fakeWindow.y;
224+ var initialWindowWidth = fakeWindow.width
225+ var initialWindowHeight = fakeWindow.height
226+
227+ var startDragX = initialWindowX + data.startX
228+ var startDragY = initialWindowY + data.startY
229+ mouseFlick(root, startDragX, startDragY, startDragX + data.dx, startDragY + data.dy, true, false, units.gu(.05), 10);
230+ tryCompare(fakeWindow, "width", Math.max(initialWindowWidth - Math.abs(data.dx), fakeWindow.minWidth));
231+ tryCompare(fakeWindow, "height", Math.max(initialWindowHeight - Math.abs(data.dy), fakeWindow.minHeight));
232+ mouseFlick(root, startDragX + data.dx, startDragY + data.dy, startDragX, startDragY, false, true, units.gu(.05), 10);
233+ tryCompare(fakeWindow, "width", initialWindowWidth);
234+ tryCompare(fakeWindow, "height", initialWindowHeight);
235+ }
137236 }
138237}
tests/qmltests/Tutorial/tst_Tutorial.qmlnew file+348
@@ -0,0 +1,348 @@
1+/*
2+ * Copyright (C) 2013,2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.0
18+import QtTest 1.0
19+import AccountsService 0.1
20+import LightDM 0.1 as LightDM
21+import Ubuntu.Components 1.1
22+import Unity.Application 0.1
23+import Unity.Test 0.1 as UT
24+
25+import "../../../qml"
26+
27+Item {
28+ id: root
29+ width: shellLoader.width + buttons.width
30+ height: shellLoader.height
31+
32+ QtObject {
33+ id: applicationArguments
34+
35+ function hasGeometry() {
36+ return false;
37+ }
38+
39+ function width() {
40+ return 0;
41+ }
42+
43+ function height() {
44+ return 0;
45+ }
46+ }
47+
48+ Component.onCompleted: {
49+ // must set the mock mode before loading the Shell
50+ LightDM.Greeter.mockMode = "single-pin";
51+ LightDM.Users.mockMode = "single-pin";
52+ shellLoader.active = true;
53+ }
54+
55+ Row {
56+ spacing: 0
57+ anchors.fill: parent
58+
59+ Loader {
60+ id: shellLoader
61+
62+ active: false
63+ width: units.gu(40)
64+ height: units.gu(71)
65+
66+ property bool itemDestroyed: false
67+ sourceComponent: Component {
68+ Shell {
69+ property string indicatorProfile: "phone"
70+
71+ Component.onDestruction: {
72+ shellLoader.itemDestroyed = true;
73+ }
74+ }
75+ }
76+ }
77+
78+ Rectangle {
79+ id: buttons
80+ color: "white"
81+ width: units.gu(30)
82+ height: shellLoader.height
83+
84+ Column {
85+ anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
86+ spacing: units.gu(1)
87+ Row {
88+ anchors { left: parent.left; right: parent.right }
89+ Button {
90+ text: "Restart Tutorial"
91+ onClicked: {
92+ if (shellLoader.status !== Loader.Ready)
93+ return;
94+
95+ AccountsService.demoEdges = false;
96+ AccountsService.demoEdges = true;
97+ }
98+ }
99+ }
100+ }
101+ }
102+ }
103+
104+ UT.UnityTestCase {
105+ id: testCase
106+ name: "Tutorial"
107+ when: windowShown
108+
109+ property Item shell: shellLoader.status === Loader.Ready ? shellLoader.item : null
110+ property real halfWidth: shell ? shell.width / 2 : 0
111+ property real halfHeight: shell ? shell.height / 2 : 0
112+
113+ function init() {
114+ tryCompare(shell, "enabled", true); // enabled by greeter when ready
115+ swipeAwayGreeter();
116+ AccountsService.demoEdges = false;
117+ AccountsService.demoEdges = true;
118+ }
119+
120+ function cleanup() {
121+ shellLoader.itemDestroyed = false;
122+
123+ shellLoader.active = false;
124+
125+ tryCompare(shellLoader, "status", Loader.Null);
126+ tryCompare(shellLoader, "item", null);
127+ // Loader.status might be Loader.Null and Loader.item might be null but the Loader
128+ // item might still be alive. So if we set Loader.active back to true
129+ // again right now we will get the very same Shell instance back. So no reload
130+ // actually took place. Likely because Loader waits until the next event loop
131+ // iteration to do its work. So to ensure the reload, we will wait until the
132+ // Shell instance gets destroyed.
133+ tryCompare(shellLoader, "itemDestroyed", true);
134+
135+ // kill all (fake) running apps
136+ killApps();
137+
138+ // reload our test subject to get it in a fresh state once again
139+ shellLoader.active = true;
140+
141+ tryCompare(shellLoader, "status", Loader.Ready);
142+ removeTimeConstraintsFromDirectionalDragAreas(shellLoader.item);
143+ }
144+
145+ function killApps() {
146+ while (ApplicationManager.count > 1) {
147+ var appIndex = ApplicationManager.get(0).appId == "unity8-dash" ? 1 : 0
148+ ApplicationManager.stopApplication(ApplicationManager.get(appIndex).appId);
149+ }
150+ compare(ApplicationManager.count, 1)
151+ }
152+
153+ function swipeAwayGreeter() {
154+ var greeter = findChild(shell, "greeter");
155+ tryCompare(greeter, "showProgress", 1);
156+
157+ touchFlick(shell, halfWidth, halfHeight, shell.width, halfHeight);
158+
159+ // wait until the animation has finished
160+ tryCompare(greeter, "showProgress", 0);
161+ waitForRendering(greeter);
162+ }
163+
164+ function waitForPage(name) {
165+ waitForRendering(findChild(shell, name));
166+ var page = findChild(shell, name);
167+ tryCompare(page, "shown", true);
168+ tryCompare(page.showAnimation, "running", false);
169+ return page;
170+ }
171+
172+ function checkTopEdge() {
173+ touchFlick(shell, halfWidth, 0, halfWidth, halfHeight);
174+
175+ var panel = findChild(shell, "panel");
176+ tryCompare(panel.indicators, "fullyClosed", true);
177+ }
178+
179+ function checkLeftEdge() {
180+ touchFlick(shell, 0, halfHeight, halfWidth, halfHeight);
181+
182+ var launcher = findChild(shell, "launcher");
183+ tryCompare(launcher, "state", "");
184+ }
185+
186+ function checkRightEdge() {
187+ touchFlick(shell, shell.width, halfHeight, halfWidth, halfHeight);
188+
189+ var stage = findChild(shell, "stage");
190+ var spreadView = findChild(stage, "spreadView");
191+ tryCompare(spreadView, "phase", 0);
192+ }
193+
194+ function checkBottomEdge() {
195+ // Can't actually check effect of swipe, since dash isn't really loaded
196+ var applicationsDisplayLoader = findChild(shell, "applicationsDisplayLoader");
197+ tryCompare(applicationsDisplayLoader.item, "interactive", false);
198+ }
199+
200+ function checkFinished() {
201+ tryCompare(AccountsService, "demoEdges", false);
202+
203+ var tutorial = findChild(shell, "tutorial");
204+ tryCompare(tutorial, "running", false);
205+
206+ var launcher = findChild(shell, "launcher");
207+ tryCompare(launcher, "shown", false);
208+ }
209+
210+ function goToPage(name) {
211+ var page = waitForPage("tutorialLeft");
212+ checkTopEdge();
213+ checkRightEdge();
214+ checkBottomEdge();
215+ if (name === "tutorialLeft") return page;
216+ touchFlick(shell, 0, halfHeight, halfWidth, halfHeight);
217+
218+ page = waitForPage("tutorialLeftFinish");
219+ if (name === "tutorialLeftFinish") return page;
220+ var tick = findChild(page, "tick");
221+ tap(tick);
222+
223+ page = waitForPage("tutorialRight");
224+ checkTopEdge();
225+ checkLeftEdge();
226+ checkBottomEdge();
227+ if (name === "tutorialRight") return page;
228+ touchFlick(shell, shell.width, halfHeight, halfWidth, halfHeight);
229+ var overlay = findChild(page, "overlay");
230+ tryCompare(overlay, "shown", true);
231+ var tick = findChild(page, "tick");
232+ tap(tick);
233+
234+ var page = waitForPage("tutorialBottom");
235+ checkTopEdge();
236+ checkLeftEdge();
237+ checkRightEdge();
238+ if (name === "tutorialBottom") return page;
239+ touchFlick(shell, halfWidth, shell.height, halfWidth, halfHeight);
240+
241+ var page = waitForPage("tutorialBottomFinish");
242+ checkTopEdge();
243+ checkLeftEdge();
244+ checkRightEdge();
245+ checkBottomEdge();
246+ if (name === "tutorialBottomFinish") return page;
247+ var tick = findChild(page, "tick");
248+ tap(tick);
249+
250+ checkFinished();
251+ return null;
252+ }
253+
254+ function test_walkthrough() {
255+ goToPage(null);
256+ }
257+
258+ function test_launcherShortDrag() {
259+ // goToPage does a normal launcher pull. But here we want to test
260+ // just barely pulling the launcher out and letting go (i.e. not
261+ // triggering the "progress" property of Launcher).
262+
263+ var left = goToPage("tutorialLeft");
264+
265+ // Make sure we don't do anything if we don't pull the launcher
266+ // out much.
267+ var launcher = findChild(shell, "launcher");
268+ touchFlick(shell, 0, halfHeight, launcher.panelWidth * 0.4, halfHeight);
269+ tryCompare(launcher, "state", ""); // should remain hidden
270+ tryCompare(left, "shown", true); // and we should still be on left
271+
272+ // Now drag out but not past launcher itself
273+ touchFlick(shell, 0, halfHeight, launcher.panelWidth * 0.9, halfHeight);
274+
275+ waitForPage("tutorialLeftFinish");
276+ }
277+
278+ function test_launcherLongDrag() {
279+ // goToPage does a normal launcher pull. But here we want to test
280+ // a full pull across the page.
281+
282+ var left = goToPage("tutorialLeft");
283+
284+ var launcher = findChild(shell, "launcher");
285+ touchFlick(shell, 0, halfHeight, shell.width, halfHeight);
286+
287+ var errorTextLabel = findChild(left, "errorTextLabel");
288+ var errorTitleLabel = findChild(left, "errorTitleLabel");
289+ tryCompare(launcher, "state", ""); // launcher goes away
290+ tryCompare(left, "shown", true); // still on left page
291+ tryCompare(errorTextLabel, "opacity", 1); // show error
292+ tryCompare(errorTitleLabel, "opacity", 1); // show error
293+ }
294+
295+ function test_launcherDragBack() {
296+ // goToPage does a full launcher pull. But here we test pulling
297+ // all the way out, then dragging back into place.
298+
299+ var left = goToPage("tutorialLeft");
300+ touchFlick(shell, 0, halfHeight, halfWidth, halfHeight, true, false);
301+ touchFlick(shell, halfWidth, halfHeight, 0, halfHeight, false, true);
302+
303+ tryCompare(left, "shown", true); // and we should still be on left
304+ }
305+
306+ function test_spread() {
307+ // Unfortunately, most of what we want to test of the spread is
308+ // "did it render correctly?" but that's hard to test. So instead,
309+ // just poke and prod it a little bit to see if some of the values
310+ // we'd expect to be correct, are so.
311+
312+ var right = goToPage("tutorialRight");
313+ var stage = findChild(right, "stage");
314+ var delegate0 = findChild(right, "appDelegate0");
315+
316+ tryCompare(stage, "dragProgress", 0);
317+ touchFlick(shell, shell.width, halfHeight, shell.width * 0.8, halfHeight, true, false);
318+ verify(stage.dragProgress > 0);
319+ compare(stage.dragProgress, -delegate0.xTranslate);
320+ touchFlick(shell, shell.width * 0.8, halfHeight, shell.width, halfHeight, false, true);
321+ tryCompare(stage, "dragProgress", 0);
322+
323+ tryCompare(delegate0, "x", shell.width);
324+
325+ var screenshotImage = findChild(right, "screenshotImage");
326+ tryCompare(screenshotImage, "source", Qt.resolvedUrl("../../../qml/Tutorial/graphics/facebook.png"));
327+ tryCompare(screenshotImage, "visible", true);
328+ }
329+
330+ function test_bottomShortDrag() {
331+ var bottom = goToPage("tutorialBottom");
332+
333+ touchFlick(shell, halfWidth, shell.height, halfWidth, shell.height * 0.8);
334+
335+ var errorTextLabel = findChild(bottom, "errorTextLabel");
336+ var errorTitleLabel = findChild(bottom, "errorTitleLabel");
337+ tryCompare(bottom, "shown", true); // still on bottom page
338+ tryCompare(errorTextLabel, "opacity", 1); // show error
339+ tryCompare(errorTitleLabel, "opacity", 1); // show error
340+ }
341+
342+ function test_interrupted() {
343+ goToPage("tutorialLeft");
344+ ApplicationManager.startApplication("dialer-app");
345+ checkFinished();
346+ }
347+ }
348+}
tests/qmltests/tst_Shell.qml+55−5
@@ -25,6 +25,7 @@ import Ubuntu.Components 1.1
2525import Ubuntu.Telephony 0.1 as Telephony
2626import Unity.Application 0.1
2727import Unity.Connectivity 0.1
28+import Unity.Indicators 0.1
2829import Unity.Notifications 1.0
2930import Unity.Test 0.1 as UT
3031import Powerd 0.1
@@ -63,6 +64,8 @@ Item {
6364 anchors.fill: parent
6465 Loader {
6566 id: shellLoader
67+ focus: true
68+
6669 active: false
6770 property bool itemDestroyed: false
6871 sourceComponent: Component {
@@ -88,6 +91,7 @@ Item {
8891 anchors { left: parent.left; right: parent.right }
8992 Button {
9093 text: "Show Greeter"
94+ activeFocusOnPress: false
9195 onClicked: {
9296 if (shellLoader.status !== Loader.Ready)
9397 return;
@@ -169,6 +173,7 @@ Item {
169173
170174 function init() {
171175 tryCompare(shell, "enabled", true); // enabled by greeter when ready
176+ shell.indicatorProfile = "phone";
172177
173178 swipeAwayGreeter();
174179
@@ -434,11 +439,11 @@ Item {
434439 waitUntilTransitionsEnd(appWindowStateGroup);
435440 }
436441
437− function test_surfaceLosesFocusWhilePanelIsOpen() {
442+ function test_surfaceLosesActiveFocusWhilePanelIsOpen() {
438443 var app = ApplicationManager.startApplication("dialer-app");
439444 waitUntilAppWindowIsFullyLoaded(app);
440445
441− tryCompare(app.session.surface, "focus", true);
446+ tryCompare(app.session.surface, "activeFocus", true);
442447
443448 // Drag the indicators panel half-open
444449 var touchX = shell.width / 2;
@@ -449,7 +454,7 @@ Item {
449454 true /* beginTouch */, false /* endTouch */);
450455 verify(indicators.partiallyOpened);
451456
452− tryCompare(app.session.surface, "focus", false);
457+ tryCompare(app.session.surface, "activeFocus", false);
453458
454459 // And finish getting it open
455460 touchFlick(indicators,
@@ -458,11 +463,21 @@ Item {
458463 false /* beginTouch */, true /* endTouch */);
459464 tryCompare(indicators, "fullyOpened", true);
460465
461− tryCompare(app.session.surface, "focus", false);
466+ tryCompare(app.session.surface, "activeFocus", false);
462467
463468 dragToCloseIndicatorsPanel();
464469
465− tryCompare(app.session.surface, "focus", true);
470+ tryCompare(app.session.surface, "activeFocus", true);
471+ }
472+
473+ function test_launchedAppHasActiveFocus() {
474+ var dialerApp = ApplicationManager.startApplication("dialer-app");
475+ verify(dialerApp);
476+ waitUntilAppSurfaceShowsUp("dialer-app")
477+
478+ verify(dialerApp.session.surface);
479+
480+ tryCompare(dialerApp.session.surface, "activeFocus", true);
466481 }
467482
468483 // Wait for the whole UI to settle down
@@ -477,6 +492,14 @@ Item {
477492 waitForRendering(shell)
478493 }
479494
495+ function waitUntilAppSurfaceShowsUp(appId) {
496+ var appWindow = findChild(shell, "appWindow_" + appId);
497+ verify(appWindow);
498+ var appWindowStates = findInvisibleChild(appWindow, "applicationWindowStateGroup");
499+ verify(appWindowStates);
500+ tryCompare(appWindowStates, "state", "surface");
501+ }
502+
480503 function dragToCloseIndicatorsPanel() {
481504 var indicators = findChild(shell, "indicators");
482505
@@ -546,6 +569,33 @@ Item {
546569 && itemRectInShell.y + itemRectInShell.height <= shell.height;
547570 }
548571
572+ function test_greeterDoesNotChangeIndicatorProfile() {
573+ var panel = findChild(shell, "panel");
574+ tryCompare(panel.indicators.indicatorsModel, "profile", shell.indicatorProfile);
575+
576+ LightDM.Greeter.showGreeter();
577+ tryCompare(panel.indicators.indicatorsModel, "profile", shell.indicatorProfile);
578+
579+ LightDM.Greeter.hideGreeter();
580+ tryCompare(panel.indicators.indicatorsModel, "profile", shell.indicatorProfile);
581+ }
582+
583+ function test_shellProfileChangesReachIndicators() {
584+ var panel = findChild(shell, "panel");
585+
586+ shell.indicatorProfile = "test1";
587+ for (var i = 0; i < panel.indicators.indicatorsModel.count; ++i) {
588+ var properties = panel.indicators.indicatorsModel.data(i, IndicatorsModelRole.IndicatorProperties);
589+ verify(properties["menuObjectPath"].substr(-5), "test1");
590+ }
591+
592+ shell.indicatorProfile = "test2";
593+ for (var i = 0; i < panel.indicators.indicatorsModel.count; ++i) {
594+ var properties = panel.indicators.indicatorsModel.data(i, IndicatorsModelRole.IndicatorProperties);
595+ verify(properties["menuObjectPath"].substr(-5), "test2");
596+ }
597+ }
598+
549599 function test_focusRequestedHidesGreeter() {
550600 var greeter = findChild(shell, "greeter");
551601
tests/qmltests/tst_ShellWithPin.qml+16
@@ -232,6 +232,22 @@ Item {
232232 tryCompare(ApplicationManager, "focusedApplicationId", app)
233233 }
234234
235+ function test_greeterChangesIndicatorProfile() {
236+ skip("Not supported yet, waiting on design for new settings panel");
237+
238+ var panel = findChild(shell, "panel");
239+ tryCompare(panel.indicators.indicatorsModel, "profile", shell.indicatorProfile + "_greeter");
240+
241+ LightDM.Greeter.hideGreeter();
242+ tryCompare(panel.indicators.indicatorsModel, "profile", shell.indicatorProfile);
243+
244+ LightDM.Greeter.showGreeter();
245+ tryCompare(panel.indicators.indicatorsModel, "profile", shell.indicatorProfile + "_greeter");
246+
247+ LightDM.Greeter.hideGreeter();
248+ tryCompare(panel.indicators.indicatorsModel, "profile", shell.indicatorProfile);
249+ }
250+
235251 function test_login() {
236252 sessionSpy.clear()
237253 tryCompare(sessionSpy, "count", 0)
tests/qmltests/tst_TabletShell.qml+2−2
@@ -303,8 +303,8 @@ Row {
303303 selectUser(data.user)
304304
305305 AccountsService.demoEdges = data.demo
306− var edgeDemo = findChild(shell, "edgeDemo")
307− tryCompare(edgeDemo, "running", data.demo)
306+ var tutorial = findChild(shell, "tutorial");
307+ tryCompare(tutorial, "running", data.demo);
308308
309309 swipeFromLeftEdge(shell.width * 0.75)
310310 wait(500) // to give time to handle dash() signal from Launcher
tests/uqmlscene/ActiveFocusLogger.cppnew file+48
@@ -0,0 +1,48 @@
1+/*
2+ * Copyright (C) 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 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 "ActiveFocusLogger.h"
18+
19+#include <QDebug>
20+#include <QQuickItem>
21+
22+void ActiveFocusLogger::setWindow(QQuickWindow *window)
23+{
24+ m_window = window;
25+ QObject::connect(window, &QQuickWindow::activeFocusItemChanged,
26+ this, &ActiveFocusLogger::printActiveFocusInfo);
27+}
28+
29+void ActiveFocusLogger::printActiveFocusInfo()
30+{
31+ if (!m_window) {
32+ return;
33+ }
34+
35+ qDebug() << "============== Active focus info START ================";
36+ if (m_window->activeFocusItem()) {
37+ qDebug() << m_window->activeFocusItem();
38+ qDebug() << "Ancestry:";
39+ QQuickItem *item = m_window->activeFocusItem()->parentItem();
40+ while (item != nullptr) {
41+ qDebug() << item << ", isFocusScope =" << item->isFocusScope();
42+ item = item->parentItem();
43+ }
44+ } else {
45+ qDebug() << "NULL";
46+ }
47+ qDebug() << "============== Active focus info END ================";
48+}
tests/uqmlscene/ActiveFocusLogger.hnew file+37
@@ -0,0 +1,37 @@
1+/*
2+ * Copyright (C) 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 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 ACTIVE_FOCUS_LOGGER_H
18+#define ACTIVE_FOCUS_LOGGER_H
19+
20+#include <QObject>
21+#include <QQuickWindow>
22+#include <QPointer>
23+
24+class ActiveFocusLogger : public QObject {
25+ Q_OBJECT
26+
27+public:
28+ void setWindow(QQuickWindow *window);
29+
30+private Q_SLOTS:
31+ void printActiveFocusInfo();
32+
33+private:
34+ QPointer<QQuickWindow> m_window;
35+};
36+
37+#endif // ACTIVE_FOCUS_LOGGER_H
tests/uqmlscene/CMakeLists.txt+1
@@ -1,5 +1,6 @@
11add_executable(uqmlscene
22 ${shellapplication_MOC_SRCS}
3+ ActiveFocusLogger.cpp
34 main.cpp
45 ${CMAKE_SOURCE_DIR}/src/MouseTouchAdaptor.cpp
56 )
tests/uqmlscene/main.cpp+13
@@ -68,6 +68,12 @@
6868// UbuntuGestures lib
6969#include <TouchRegistry.h>
7070
71+#define UQMLSCENE_DEBUG_ACTIVE_FOCUS 0
72+
73+#if UQMLSCENE_DEBUG_ACTIVE_FOCUS
74+ #include "ActiveFocusLogger.h"
75+#endif
76+
7177#ifdef QML_RUNTIME_TESTING
7278class RenderStatistics
7379{
@@ -378,6 +384,10 @@ int main(int argc, char ** argv)
378384{
379385 Options options;
380386
387+ #if UQMLSCENE_DEBUG_ACTIVE_FOCUS
388+ ActiveFocusLogger activeFocusLogger;
389+ #endif
390+
381391 QStringList imports;
382392 QList<QPair<QString, QString> > bundles;
383393 for (int i = 1; i < argc; ++i) {
@@ -503,6 +513,9 @@ int main(int argc, char ** argv)
503513 QQuickItem *contentItem = qobject_cast<QQuickItem *>(topLevel);
504514 if (contentItem) {
505515 qxView = new QQuickView(&engine, nullptr);
516+ #if UQMLSCENE_DEBUG_ACTIVE_FOCUS
517+ activeFocusLogger.setWindow(qxView);
518+ #endif
506519 TouchRegistry::instance()->setParent(qxView);
507520 qxView->installEventFilter(TouchRegistry::instance());
508521 window = qxView;