on xenial help sign in
merge trunk
commitac310bfd96bdedc55c0619e5340978faa14bdf96 authorLukáš Tinkl <lukas.tinkl@canonical.com> dateWed 23 Mar 2016, 22:03 (11 years ago) parente11e7bbc7e6c92
275 files changed+12588−4033
CMakeLists.txt+2−1 CODING+2 debian/changelog+49 debian/control+9−7 libs/UbuntuGestures/Timer.h+5 libs/UbuntuGestures/TouchRegistry.cpp+15−10 plugins/AccountsService/AccountsService.cpp+69−3 plugins/AccountsService/AccountsService.h+21−3 plugins/AccountsService/AccountsService.qmltypes+5 plugins/AccountsService/com.canonical.unity.AccountsService.xml+5 plugins/Cursor/Cursor.qmltypes+78 plugins/Dash/ScopeStyle.qml+1−1 plugins/GlobalShortcut/GlobalShortcut.qmltypes+31 plugins/ScreenGrabber/screengrabber.cpp−2 plugins/Ubuntu/Gestures/CMakeLists.txt+1 plugins/Ubuntu/Gestures/DirectionalDragArea.cpp+43−5 plugins/Ubuntu/Gestures/DirectionalDragArea.h+8 plugins/Ubuntu/Gestures/DirectionalDragArea_p.h+2 plugins/Ubuntu/Gestures/Gestures.qmltypes+86−45 plugins/Ubuntu/Gestures/TouchGestureArea.cpp+875 plugins/Ubuntu/Gestures/TouchGestureArea.h+229 plugins/Ubuntu/Gestures/plugin.cpp+3 plugins/Unity/Indicators/indicatorsmanager.cpp+3−5 plugins/Unity/InputInfo/InputInfo.qmltypes+71 plugins/Unity/Platform/Platform.qmltypes+20 plugins/Unity/Session/dbusunitysessionservice.cpp+4−1 plugins/Utils/CMakeLists.txt+2−1 plugins/Utils/Utils.qmltypes+2−2 plugins/Utils/globalfunctions.cpp+56 plugins/Utils/globalfunctions.h+42 plugins/Utils/plugin.cpp+11−2 plugins/Utils/timezoneFormatter.cpp+10 plugins/Utils/timezoneFormatter.h+1 plugins/Utils/windowkeysfilter.cpp → plugins/Utils/windowinputfilter.cpp+14−16 plugins/Utils/windowkeysfilter.h → plugins/Utils/windowinputfilter.h+11−11 plugins/Utils/windowstatestorage.cpp+36−4 plugins/Utils/windowstatestorage.h+3 plugins/Wizard/CMakeLists.txt+7−1 plugins/Wizard/LocalePlugin.cpp+291 plugins/Wizard/LocalePlugin.h+58 plugins/Wizard/PageList.cpp+4−4 plugins/Wizard/Status.cpp+143 plugins/Wizard/Status.h+60 plugins/Wizard/System.cpp+5−4 plugins/Wizard/System.h+3−2 plugins/Wizard/plugin.cpp+7−8 plugins/Wizard/plugin.h+1−2 plugins/Wizard/timezonemodel.cpp+244 plugins/Wizard/timezonemodel.h+80 po/unity8.pot+256−178 qml/Components/Dialogs.qml+6−47 qml/Components/InputMethod.qml+4−1 qml/Components/Showable.qml+10 qml/Components/Unity8Settings.qml−50 qml/Dash/DashContent.qml+2−2 qml/Dash/DashNavigation.qml+159−78 qml/Dash/DashNavigationButton.qml−233 qml/Dash/DashNavigationHeader.qml+73 qml/Dash/DashNavigationList.qml+33−85 qml/Dash/DashPageHeader.qml+140−131 qml/Dash/Filters/FilterOptionSelector.qml+91 qml/Dash/Filters/FilterRangeInput.qml+136 qml/Dash/Filters/FilterValueSlider.qml+87 qml/Dash/Filters/FilterWidget.qml+29 qml/Dash/Filters/FilterWidgetFactory.qml+80 qml/Dash/FiltersPopover.qml+105 qml/Dash/GenericScopeView.qml+77−47 qml/Dash/PageHeaderExtraPanel.qml+176 qml/Greeter/Greeter.qml+16−6 qml/Launcher/Launcher.qml+2−3 qml/Launcher/LauncherPanel.qml+3−2 qml/OrientedShell.qml+27−17 qml/Panel/IndicatorItemRow.qml+5−2 qml/Panel/Indicators/MenuItemFactory.qml+16−3 qml/Panel/IndicatorsMenu.qml−2 qml/Panel/MenuContent.qml+1 qml/Shell.qml+84−53 qml/Stages/AbstractStage.qml+5−1 qml/Stages/ApplicationWindow.qml+7−3 qml/Stages/DesktopStage.qml+76−13 qml/Stages/PhoneStage.qml+15−6 qml/Stages/SideStage.qml+118 qml/Stages/SpreadDelegate.qml+11−1 qml/Stages/StagedFullscreenPolicy.qml+58 qml/Stages/SurfaceContainer.qml+14 qml/Stages/TabletSideStageTouchGesture.qml+154 qml/Stages/TabletStage.qml+405−172 qml/Stages/TransformedTabletSpreadDelegate.qml+84−56 qml/Stages/WindowResizeArea.qml+2−2 qml/Stages/WindowedFullscreenPolicy.qml+41 qml/Stages/graphics/sidestage_drag.svg+207 qml/Stages/graphics/sidestage_open.svg+181 qml/Tutorial/Arrow.qml−56 qml/Tutorial/InactivityTimer.qml+62 qml/Tutorial/Slider.qml−123 qml/Tutorial/Tutorial.qml+39−14 qml/Tutorial/TutorialBottom.qml+86−66 qml/Tutorial/TutorialBottomFinish.qml−41 qml/Tutorial/TutorialContent.qml+227−55 qml/Tutorial/TutorialLeft.qml+21−68 qml/Tutorial/TutorialLeftLong.qml+54 qml/Tutorial/TutorialPage.qml+62−186 qml/Tutorial/TutorialRight.qml+30−192 qml/Tutorial/TutorialTop.qml+61 qml/Tutorial/graphics/arrow.svg+19 qml/Tutorial/graphics/background1.png qml/Tutorial/graphics/background2.png 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 qml/Wizard/CheckableSetting.qml+11−3 qml/Wizard/Page.qml+261−44 qml/Wizard/Pages.qml+75−31 qml/Wizard/Pages/10-welcome.qml+128−48 qml/Wizard/Pages/20-sim.qml−77 qml/Wizard/Pages/30-passwd-type.qml−140 qml/Wizard/Pages/30-wifi.qml+210 qml/Wizard/Pages/40-location.qml+290 qml/Wizard/Pages/40-wifi.qml−219 qml/Wizard/Pages/50-location.qml−150 qml/Wizard/Pages/50-timezone.qml+275 qml/Wizard/Pages/60-reporting.qml → qml/Wizard/Pages/60-account.qml+30−20 qml/Wizard/Pages/70-passwd-type.qml+166 qml/Wizard/Pages/75-report-check.qml+91 qml/Wizard/Pages/80-finished.qml+94−16 qml/Wizard/Pages/data/Desktop_header_bkg.png qml/Wizard/Pages/data/Desktop_splash_screen_bkg.png qml/Wizard/Pages/data/Phone_header_bkg.png qml/Wizard/Pages/data/Phone_splash_screen_bkg.png qml/Wizard/Pages/data/Tick@30.png qml/Wizard/Pages/data/meet_ubuntu_simcard@30.png qml/Wizard/Pages/data/timezonemap/map.png qml/Wizard/Pages/data/timezonemap/pin.png qml/Wizard/Pages/data/timezonemap/timezone_-1.png qml/Wizard/Pages/data/timezonemap/timezone_-10.png qml/Wizard/Pages/data/timezonemap/timezone_-11.png qml/Wizard/Pages/data/timezonemap/timezone_-2.png qml/Wizard/Pages/data/timezonemap/timezone_-3.5.png qml/Wizard/Pages/data/timezonemap/timezone_-3.png qml/Wizard/Pages/data/timezonemap/timezone_-4.5.png qml/Wizard/Pages/data/timezonemap/timezone_-4.png qml/Wizard/Pages/data/timezonemap/timezone_-5.png qml/Wizard/Pages/data/timezonemap/timezone_-6.png qml/Wizard/Pages/data/timezonemap/timezone_-7.png qml/Wizard/Pages/data/timezonemap/timezone_-8.png qml/Wizard/Pages/data/timezonemap/timezone_-9.5.png qml/Wizard/Pages/data/timezonemap/timezone_-9.png qml/Wizard/Pages/data/timezonemap/timezone_0.png qml/Wizard/Pages/data/timezonemap/timezone_1.png qml/Wizard/Pages/data/timezonemap/timezone_10.5.png qml/Wizard/Pages/data/timezonemap/timezone_10.png qml/Wizard/Pages/data/timezonemap/timezone_11.png qml/Wizard/Pages/data/timezonemap/timezone_12.75.png qml/Wizard/Pages/data/timezonemap/timezone_12.png qml/Wizard/Pages/data/timezonemap/timezone_13.png qml/Wizard/Pages/data/timezonemap/timezone_2.png qml/Wizard/Pages/data/timezonemap/timezone_3.5.png qml/Wizard/Pages/data/timezonemap/timezone_3.png qml/Wizard/Pages/data/timezonemap/timezone_4.5.png qml/Wizard/Pages/data/timezonemap/timezone_4.png qml/Wizard/Pages/data/timezonemap/timezone_5.5.png qml/Wizard/Pages/data/timezonemap/timezone_5.75.png qml/Wizard/Pages/data/timezonemap/timezone_5.png qml/Wizard/Pages/data/timezonemap/timezone_6.5.png qml/Wizard/Pages/data/timezonemap/timezone_6.png qml/Wizard/Pages/data/timezonemap/timezone_7.png qml/Wizard/Pages/data/timezonemap/timezone_8.5.png qml/Wizard/Pages/data/timezonemap/timezone_8.png qml/Wizard/Pages/data/timezonemap/timezone_9.5.png qml/Wizard/Pages/data/timezonemap/timezone_9.png qml/Wizard/Pages/here-terms.qml+71−66 qml/Wizard/Pages/passwd-confirm.qml → qml/Wizard/Pages/passcode-confirm.qml+13−30 qml/Wizard/Pages/passcode-desktop.qml+132 qml/Wizard/Pages/passwd-set.qml → qml/Wizard/Pages/passcode-set.qml+13−29 qml/Wizard/Pages/password-set.qml+149 qml/Wizard/Pages/sim.qml+108 qml/Wizard/PasswordMeter.qml+106 qml/Wizard/StackButton.qml+7−12 qml/Wizard/Wizard.qml−9 qml/Tutorial/TutorialLeftFinish.qml → qml/Wizard/WizardTextField.qml+5−21 src/MouseTouchAdaptor.cpp+53−9 src/MouseTouchAdaptor.h+5−3 tests/autopilot/unity8/shell/emulators.py−2 tests/autopilot/unity8/shell/tests/test_tutorial.py−57 tests/autopilot/unity8/tutorial.py−73 tests/libs/UbuntuGestures/tst_TouchRegistry.cpp+59 tests/mocks/AccountsService/AccountsService.cpp+60−3 tests/mocks/AccountsService/AccountsService.h+30−5 tests/mocks/AccountsService/AccountsService.qmltypes+5 tests/mocks/MeeGo/QOfono/MockOfonoSimManager.qml+1 tests/mocks/QMenuModel/QDBusActionGroup.qml+5−5 tests/mocks/Ubuntu/Connectivity/plugin.cpp+1 tests/mocks/Ubuntu/SystemSettings/CMakeLists.txt+2 tests/mocks/Ubuntu/SystemSettings/Diagnostics/CMakeLists.txt+1 qml/Tutorial/Tick.qml → tests/mocks/Ubuntu/SystemSettings/Diagnostics/MockDiagnostics.qml+4−9 tests/mocks/Ubuntu/SystemSettings/Diagnostics/qmldir+2 tests/mocks/Ubuntu/SystemSettings/LanguagePlugin/MockKeyboardPlugin.qml+21 tests/mocks/Ubuntu/SystemSettings/LanguagePlugin/MockLanguagePlugin.qml+2−2 tests/mocks/Ubuntu/SystemSettings/LanguagePlugin/qmldir+1 tests/mocks/Ubuntu/SystemSettings/SecurityPrivacy/MockSecurityPrivacy.cpp+1−1 tests/mocks/Ubuntu/SystemSettings/TimeDate/CMakeLists.txt+1 tests/mocks/Ubuntu/SystemSettings/TimeDate/MockTimeDate.qml+25 tests/mocks/Ubuntu/SystemSettings/TimeDate/qmldir+2 tests/mocks/Unity/Application/ApplicationInfo.cpp+21−3 tests/mocks/Unity/Application/ApplicationInfo.h+6−2 tests/mocks/Unity/Application/ApplicationManager.cpp+13−26 tests/mocks/Unity/Application/ApplicationManager.h−8 tests/mocks/Unity/Application/MirSurface.cpp+32 tests/mocks/Unity/Application/MirSurface.h+10−3 tests/mocks/Unity/Application/MirSurfaceItem.cpp+28−5 tests/mocks/Unity/Application/MirSurfaceItem.h+2 tests/mocks/Unity/Application/Session.cpp+26−1 tests/mocks/Unity/Application/Session.h+7 tests/mocks/Unity/Application/resources/MirSurfaceItem.qml+59 tests/mocks/Unity/CMakeLists.txt+13 tests/mocks/Unity/Indicators/ModelActionRootState.qml+26−4 tests/mocks/Unity/fake_filters.cpp+125 tests/mocks/Unity/fake_filters.h+46 tests/mocks/Unity/fake_navigation.cpp+9−10 tests/mocks/Unity/fake_optionselectorfilter.cpp+65 tests/mocks/Unity/fake_optionselectorfilter.h+52 tests/mocks/Unity/fake_optionselectoroptions.cpp+77 tests/mocks/Unity/fake_optionselectoroptions.h+47 tests/mocks/Unity/fake_rangeinputfilter.cpp+167 tests/mocks/Unity/fake_rangeinputfilter.h+79 tests/mocks/Unity/fake_scope.cpp+52−27 tests/mocks/Unity/fake_scope.h+20−7 tests/mocks/Unity/fake_unity_plugin.cpp+7 tests/mocks/Unity/fake_valuesliderfilter.cpp+84 tests/mocks/Unity/fake_valuesliderfilter.h+55 tests/mocks/Unity/fake_valueslidervalues.cpp+51 tests/mocks/Unity/fake_valueslidervalues.h+36 tests/mocks/Utils/CMakeLists.txt+3−2 tests/mocks/Utils/Utils.qmltypes+2−2 tests/mocks/Utils/plugin.cpp+14−2 tests/mocks/Utils/windowstatestorage.cpp+13 tests/mocks/Utils/windowstatestorage.h+4 tests/mocks/Wizard/CMakeLists.txt+6−1 tests/mocks/Wizard/MockSystem.cpp+4−2 tests/mocks/Wizard/MockSystem.h+2−2 tests/mocks/Wizard/mockplugin.cpp+7−8 tests/plugins/AccountsService/PropertiesServer.cpp+10 tests/plugins/AccountsService/PropertiesServer.h−1 tests/plugins/AccountsService/client.cpp+67 tests/plugins/Dash/tst_ScopeStyle.qml+1−1 tests/plugins/Ubuntu/Gestures/CMakeLists.txt+7−1 tests/plugins/Ubuntu/Gestures/GestureTest.cpp+44 tests/plugins/Ubuntu/Gestures/GestureTest.h+13−1 tests/plugins/Ubuntu/Gestures/tst_DirectionalDragArea.cpp−56 tests/plugins/Ubuntu/Gestures/tst_TouchGestureArea.cpp+270 tests/plugins/Ubuntu/Gestures/tst_TouchGestureArea.qml+90 tests/plugins/Unity/Launcher/launchermodeltest.cpp+1 tests/qmltests/CMakeLists.txt+4 tests/qmltests/Components/tst_PhysicalKeysMapper.qml+3−3 tests/qmltests/Dash/Filters/tst_FilterOptionSelector.qml+96 tests/qmltests/Dash/Filters/tst_FilterRangeInput.qml+247 tests/qmltests/Dash/Filters/tst_FilterValueSlider.qml+101 tests/qmltests/Dash/Filters/tst_FilterWidgetFactory.qml+58 tests/qmltests/Dash/Previews/tst_Preview.qml+1 tests/qmltests/Dash/tst_DashContent.qml+165−152 tests/qmltests/Dash/tst_DashPageHeader.qml+24−15 tests/qmltests/Dash/tst_GenericScopeView.qml+3−3 tests/qmltests/Greeter/tst_Greeter.qml+7−1 tests/qmltests/Stages/tst_DesktopStage.qml−2 tests/qmltests/Stages/tst_TabletStage.qml+143 tests/qmltests/Stages/tst_WindowResizeArea.qml+7 tests/qmltests/Tutorial/tst_Tutorial.qml+611−193 tests/qmltests/Wizard/tst_Wizard.qml+126−214 tests/qmltests/tst_OrientedShell.qml+126−15 tests/qmltests/tst_Shell.qml+125−8 tests/qmltests/tst_ShellWithPin.qml−22 tests/utils/modules/Unity/Test/UnityTestCase.qml+67−11
CMakeLists.txt+2−1
@@ -57,7 +57,8 @@ find_package(Qt5DBus 5.4 REQUIRED)
5757find_package(Qt5Concurrent 5.4 REQUIRED)
5858find_package(Qt5Sql 5.4 REQUIRED)
5959
60−pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application>=13)
60+pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=14)
61+pkg_check_modules(GEONAMES REQUIRED geonames>=0.2)
6162pkg_check_modules(GIO REQUIRED gio-2.0>=2.32)
6263pkg_check_modules(GLIB REQUIRED glib-2.0>=2.32)
6364pkg_check_modules(QMENUMODEL REQUIRED qmenumodel)
CODING+2
@@ -54,6 +54,8 @@ $ ./run.sh -p OR -k
5454Notes
5555-----
5656- to navigate, utilize the mouse left button as you would your finger
57+- a three point drag (for side stage operations) can be performed by using
58+ the Shift+Control+Alt (all together) keyboard modifiers while using the mouse.
5759- to get the translations work, currently you have to do make install to
5860 put the *.mo files into correct structure. We'll look for a better solution
5961 later.
debian/changelog+49
@@ -1,3 +1,52 @@
1+unity8 (8.12+16.04.20160323.2-0ubuntu1) xenial; urgency=medium
2+
3+ [ Michael Terry ]
4+ *
5+ * Fix setting OSK layout in OOBE wizard
6+ * Stop the left edge tutorial from being shown when the bottom edge
7+ tutorial wants to be shown after receiving a call.
8+
9+ [ Albert Astals Cid ]
10+ * Filter implementation with Option Selector filter as the only filter
11+ * Get the engine from the parent context
12+ * Implement the API changes due to filters
13+ * Match design visuals for the new dash navigation/page header popup
14+ * New Dash Navigation UX
15+ * Range Input filter
16+ * Value slider filter
17+ * Workaround SDK ListItem bug
18+
19+ [ Albert Astals Cid, Lukáš Tinkl ]
20+ * Fix various tests
21+
22+ [ CI Train Bot ]
23+ * Resync trunk.
24+ * Update translation template
25+
26+ [ Lukáš Tinkl, Michael Terry ]
27+ * New first-run wizard for the phone (part of the OOBE effort) (LP:
28+ #1536688, #1536617, #1543790, #1555222)
29+
30+ [ Lukáš Tinkl, Michał Sawicz ]
31+ * Keymap switching support (LP: #1524400, #1412492, #1491340)
32+
33+ [ Michael Terry, Michael Zanetti ]
34+ * Redesign the first-boot edge tutorial
35+
36+ [ Michael Zanetti ]
37+ * disabling some tests that are flaky in adt but can't be made fail on
38+ our machines
39+
40+ [ Michael Zanetti, Michał Sawicz ]
41+ * Change the behavior alond with the switch in indicator-display (LP:
42+ #1538591)
43+
44+ [ Michael Zanetti, Michał Sawicz, Nick Dedekind ]
45+ * Add support for low shell chrome. (LP: #1535397)
46+ * Sidestage load/unload redesign (LP: #1544479)
47+
48+ -- Michael Zanetti <michael.zanetti@canonical.com> Wed, 23 Mar 2016 09:53:17 +0000
49+
150unity8 (8.11+16.04.20160318.1-0ubuntu1) xenial; urgency=medium
251
352 [ CI Train Bot ]
debian/control+9−7
@@ -8,13 +8,14 @@ Build-Depends: cmake,
88 doxyqml,
99# To allow cross-compiling to work, we also must append :native
1010# to g++ so we don't try to run armhf g++
11−# on an x86 CPU for eaxmple, when cross-compiling.
11+# on an x86 CPU for example, when cross-compiling.
1212 g++:native,
1313 libandroid-properties-dev,
1414 graphviz,
1515 gsettings-ubuntu-schemas (>= 0.0.2+14.10.20140815),
1616 libconnectivity-qt1-dev (>= 0.7.1),
1717 libevdev-dev,
18+ libgeonames-dev (>= 0.2),
1819 libgl1-mesa-dev[!armhf] | libgl-dev[!armhf],
1920 libgl1-mesa-dri,
2021 libgles2-mesa-dev[armhf],
@@ -29,7 +30,7 @@ Build-Depends: cmake,
2930 libqt5xmlpatterns5-dev,
3031 libsystemsettings-dev,
3132 libudev-dev,
32− libunity-api-dev (>= 7.107),
33+ libunity-api-dev (>= 7.108),
3334 libusermetricsoutput1-dev,
3435# Need those X11 libs touch emulation from mouse events in manual QML tests on a X11 desktop
3536 libx11-dev[!armhf],
@@ -101,7 +102,7 @@ Depends: dmz-cursor-theme,
101102 qml-module-qt-labs-folderlistmodel,
102103 qml-module-qtquick-xmllistmodel,
103104 qml-module-qtsysteminfo,
104− qtdeclarative5-qtmir-plugin (>= 0.4.5),
105+ qtdeclarative5-qtmir-plugin (>= 0.4.8),
105106 qtdeclarative5-ubuntu-telephony0.1,
106107 qtdeclarative5-ubuntu-web-plugin,
107108 ubuntu-system-settings,
@@ -111,7 +112,8 @@ Depends: dmz-cursor-theme,
111112 unity8-private | unity-launcher-impl,
112113 ${misc:Depends},
113114 ${shlibs:Depends},
114−Recommends: unity-scope-click,
115+Recommends: indicator-session,
116+ unity-scope-click,
115117 unity-scope-mediascanner2,
116118Breaks: indicator-network (<< 0.5.1+14.10.20141014),
117119 ubuntu-touch-session (<< 0.107),
@@ -130,10 +132,10 @@ Depends: qml-module-qtquick-layouts,
130132 qtdeclarative5-ubuntu-ui-toolkit-plugin (>= 1.3.1845) | qtdeclarative5-ubuntu-ui-toolkit-plugin-gles (>= 1.3.1845),
131133 qtdeclarative5-unity-notifications-plugin (>= 0.1.2) | unity-notifications-impl,
132134 ubuntu-thumbnailer-impl-0,
133− unity-application-impl-13,
135+ unity-application-impl-14,
134136 unity-notifications-impl-3,
135137 unity-plugin-scopes | unity-scopes-impl,
136− unity-scopes-impl-10,
138+ unity-scopes-impl-11,
137139 unity8-fake-env | unity-application-impl,
138140 ${misc:Depends},
139141Breaks: unity8 (<< 7.86),
@@ -176,7 +178,7 @@ Pre-Depends: ${misc:Pre-Depends},
176178Depends: ${misc:Depends},
177179 ${shlibs:Depends},
178180Provides: unity-application-impl,
179− unity-application-impl-13,
181+ unity-application-impl-14,
180182Replaces: unity8-autopilot (<< 8.02+15.04.20150422-0ubuntu1)
181183Description: Fake environment for running Unity 8 shell
182184 Provides fake implementations of some QML modules used by Unity 8 shell
libs/UbuntuGestures/Timer.h+5
@@ -35,6 +35,11 @@ public:
3535 virtual int interval() const = 0;
3636 virtual void setInterval(int msecs) = 0;
3737 virtual void start() { m_isRunning = true; }
38+ virtual void start(int msecs)
39+ {
40+ setInterval(msecs);
41+ start();
42+ }
3843 virtual void stop() { m_isRunning = false; }
3944 bool isRunning() const { return m_isRunning; }
4045 virtual bool isSingleShot() const = 0;
libs/UbuntuGestures/TouchRegistry.cpp+15−10
@@ -359,10 +359,6 @@ void TouchRegistry::removeCandidateOwnerForTouchByIndex(Pool<TouchRegistry::Touc
359359
360360void TouchRegistry::requestTouchOwnership(int id, QQuickItem *candidate)
361361{
362− #if TOUCHREGISTRY_DEBUG
363− UG_DEBUG << "requestTouchOwnership id " << id << "candidate" << candidate;
364− #endif
365−
366362 Pool<TouchInfo>::Iterator touchInfo = findTouchInfo(id);
367363 if (!touchInfo) { qFatal("TouchRegistry: Failed to find TouchInfo"); }
368364
@@ -379,6 +375,9 @@ void TouchRegistry::requestTouchOwnership(int id, QQuickItem *candidate)
379375 break;
380376 }
381377 }
378+ #if TOUCHREGISTRY_DEBUG
379+ UG_DEBUG << "requestTouchOwnership id " << id << "candidate" << candidate << "index: " << candidateIndex;
380+ #endif
382381
383382 // add it as a candidate if not present yet
384383 if (candidateIndex < 0) {
@@ -533,16 +532,22 @@ void TouchRegistry::TouchInfo::notifyCandidatesOfOwnershipResolution()
533532 << " gained) to candidate" << candidates[0].item;
534533 #endif
535534
536− TouchOwnershipEvent gainedOwnershipEvent(id, true /*gained*/);
537− QCoreApplication::sendEvent(candidates[0].item, &gainedOwnershipEvent);
535+ // need to take a copy of the item list in case
536+ // we call back in to remove candidate during the lost ownership event.
537+ QList<QPointer<QQuickItem>> items;
538+ Q_FOREACH(const CandidateInfo& info, candidates) {
539+ items << info.item;
540+ }
538541
542+ TouchOwnershipEvent gainedOwnershipEvent(id, true /*gained*/);
543+ QCoreApplication::sendEvent(items[0], &gainedOwnershipEvent);
539544
540545 TouchOwnershipEvent lostOwnershipEvent(id, false /*gained*/);
541− for (int i = 1; i < candidates.count(); ++i) {
546+ for (int i = 1; i < items.count(); ++i) {
542547 #if TOUCHREGISTRY_DEBUG
543− UG_DEBUG << "sending TouchWonershipEvent(id =" << id << " lost) to candidate"
544− << candidates[i].item;
548+ UG_DEBUG << "sending TouchOwnershipEvent(id =" << id << " lost) to candidate"
549+ << items[i];
545550 #endif
546− QCoreApplication::sendEvent(candidates[i].item, &lostOwnershipEvent);
551+ QCoreApplication::sendEvent(items[i], &lostOwnershipEvent);
547552 }
548553}
plugins/AccountsService/AccountsService.cpp+69−3
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013 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
@@ -12,8 +12,6 @@
1212 *
1313 * You should have received a copy of the GNU General Public License
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− *
16− * Author: Michael Terry <michael.terry@canonical.com>
1715 */
1816
1917#include "AccountsService.h"
@@ -34,9 +32,12 @@
3432
3533#define PROP_BACKGROUND_FILE QStringLiteral("BackgroundFile")
3634#define PROP_DEMO_EDGES QStringLiteral("demo-edges")
35+#define PROP_DEMO_EDGES_COMPLETED QStringLiteral("DemoEdgesCompleted")
36+#define PROP_EMAIL QStringLiteral("Email")
3737#define PROP_ENABLE_INDICATORS_WHILE_LOCKED QStringLiteral("EnableIndicatorsWhileLocked")
3838#define PROP_ENABLE_LAUNCHER_WHILE_LOCKED QStringLiteral("EnableLauncherWhileLocked")
3939#define PROP_FAILED_LOGINS QStringLiteral("FailedLogins")
40+#define PROP_INPUT_SOURCES QStringLiteral("InputSources")
4041#define PROP_LICENSE_ACCEPTED QStringLiteral("LicenseAccepted")
4142#define PROP_LICENSE_BASE_PATH QStringLiteral("LicenseBasePath")
4243#define PROP_MOUSE_CURSOR_SPEED QStringLiteral("MouseCursorSpeed")
@@ -44,6 +45,7 @@
4445#define PROP_MOUSE_PRIMARY_BUTTON QStringLiteral("MousePrimaryButton")
4546#define PROP_MOUSE_SCROLL_SPEED QStringLiteral("MouseScrollSpeed")
4647#define PROP_PASSWORD_DISPLAY_HINT QStringLiteral("PasswordDisplayHint")
48+#define PROP_REAL_NAME QStringLiteral("RealName")
4749#define PROP_STATS_WELCOME_SCREEN QStringLiteral("StatsWelcomeScreen")
4850#define PROP_TOUCHPAD_CURSOR_SPEED QStringLiteral("TouchpadCursorSpeed")
4951#define PROP_TOUCHPAD_DISABLE_WHILE_TYPING QStringLiteral("TouchpadDisableWhileTyping")
@@ -54,6 +56,10 @@
5456#define PROP_TOUCHPAD_TAP_TO_CLICK QStringLiteral("TouchpadTapToClick")
5557#define PROP_TOUCHPAD_TWO_FINGER_SCROLL QStringLiteral("TouchpadTwoFingerScroll")
5658
59+using StringMap = QMap<QString,QString>;
60+using StringMapList = QList<StringMap>;
61+Q_DECLARE_METATYPE(StringMapList)
62+
5763
5864QVariant primaryButtonConverter(const QVariant &value)
5965{
@@ -80,6 +86,9 @@ AccountsService::AccountsService(QObject* parent, const QString &user)
8086 connect(m_service, &AccountsServiceDBusAdaptor::maybeChanged, this, &AccountsService::onMaybeChanged);
8187
8288 registerProperty(IFACE_ACCOUNTS_USER, PROP_BACKGROUND_FILE, QStringLiteral("backgroundFileChanged"));
89+ registerProperty(IFACE_ACCOUNTS_USER, PROP_EMAIL, QStringLiteral("emailChanged"));
90+ registerProperty(IFACE_ACCOUNTS_USER, PROP_REAL_NAME, QStringLiteral("realNameChanged"));
91+ registerProperty(IFACE_ACCOUNTS_USER, PROP_INPUT_SOURCES, QStringLiteral("keymapsChanged"));
8392 registerProperty(IFACE_LOCATION_HERE, PROP_LICENSE_ACCEPTED, QStringLiteral("hereEnabledChanged"));
8493 registerProperty(IFACE_LOCATION_HERE, PROP_LICENSE_BASE_PATH, QStringLiteral("hereLicensePathChanged"));
8594 registerProperty(IFACE_UBUNTU_SECURITY, PROP_ENABLE_LAUNCHER_WHILE_LOCKED, QStringLiteral("enableLauncherWhileLockedChanged"));
@@ -87,6 +96,7 @@ AccountsService::AccountsService(QObject* parent, const QString &user)
8796 registerProperty(IFACE_UBUNTU_SECURITY, PROP_PASSWORD_DISPLAY_HINT, QStringLiteral("passwordDisplayHintChanged"));
8897 registerProperty(IFACE_UBUNTU_SECURITY_OLD, PROP_STATS_WELCOME_SCREEN, QStringLiteral("statsWelcomeScreenChanged"));
8998 registerProperty(IFACE_UNITY, PROP_DEMO_EDGES, QStringLiteral("demoEdgesChanged"));
99+ registerProperty(IFACE_UNITY, PROP_DEMO_EDGES_COMPLETED, QStringLiteral("demoEdgesCompletedChanged"));
90100 registerProperty(IFACE_UNITY_PRIVATE, PROP_FAILED_LOGINS, QStringLiteral("failedLoginsChanged"));
91101
92102 registerProxy(IFACE_UBUNTU_INPUT, PROP_MOUSE_CURSOR_SPEED,
@@ -150,6 +160,20 @@ void AccountsService::setDemoEdges(bool demoEdges)
150160 setProperty(IFACE_UNITY, PROP_DEMO_EDGES, demoEdges);
151161}
152162
163+QStringList AccountsService::demoEdgesCompleted() const
164+{
165+ auto value = getProperty(IFACE_UNITY, PROP_DEMO_EDGES_COMPLETED);
166+ return value.toStringList();
167+}
168+
169+void AccountsService::markDemoEdgeCompleted(const QString &edge)
170+{
171+ auto currentList = demoEdgesCompleted();
172+ if (!currentList.contains(edge)) {
173+ setProperty(IFACE_UNITY, PROP_DEMO_EDGES_COMPLETED, currentList << edge);
174+ }
175+}
176+
153177bool AccountsService::enableLauncherWhileLocked() const
154178{
155179 auto value = getProperty(IFACE_UBUNTU_SECURITY, PROP_ENABLE_LAUNCHER_WHILE_LOCKED);
@@ -206,6 +230,48 @@ bool AccountsService::hereLicensePathValid() const
206230 return !value.toString().isNull();
207231}
208232
233+QString AccountsService::realName() const
234+{
235+ auto value = getProperty(IFACE_ACCOUNTS_USER, PROP_REAL_NAME);
236+ return value.toString();
237+}
238+
239+void AccountsService::setRealName(const QString &realName)
240+{
241+ setProperty(IFACE_ACCOUNTS_USER, PROP_REAL_NAME, realName);
242+}
243+
244+QString AccountsService::email() const
245+{
246+ auto value = getProperty(IFACE_ACCOUNTS_USER, PROP_EMAIL);
247+ return value.toString();
248+}
249+
250+void AccountsService::setEmail(const QString &email)
251+{
252+ setProperty(IFACE_ACCOUNTS_USER, PROP_EMAIL, email);
253+}
254+
255+QStringList AccountsService::keymaps() const
256+{
257+ auto value = getProperty(IFACE_ACCOUNTS_USER, PROP_INPUT_SOURCES);
258+ QDBusArgument arg = value.value<QDBusArgument>();
259+ StringMapList maps = qdbus_cast<StringMapList>(arg);
260+ QStringList simplifiedMaps;
261+
262+ Q_FOREACH(const StringMap &map, maps) {
263+ Q_FOREACH(const QString &entry, map) {
264+ simplifiedMaps.append(entry);
265+ }
266+ }
267+
268+ if (!simplifiedMaps.isEmpty()) {
269+ return simplifiedMaps;
270+ }
271+
272+ return {QStringLiteral("us")};
273+}
274+
209275uint AccountsService::failedLogins() const
210276{
211277 return getProperty(IFACE_UNITY_PRIVATE, PROP_FAILED_LOGINS).toUInt();
plugins/AccountsService/AccountsService.h+21−3
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013 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
@@ -12,8 +12,6 @@
1212 *
1313 * You should have received a copy of the GNU General Public License
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− *
16− * Authors: Michael Terry <michael.terry@canonical.com>
1715 */
1816
1917#ifndef UNITY_ACCOUNTSSERVICE_H
@@ -22,6 +20,7 @@
2220#include <QHash>
2321#include <QObject>
2422#include <QString>
23+#include <QStringList>
2524#include <QVariant>
2625
2726class AccountsServiceDBusAdaptor;
@@ -39,6 +38,9 @@ class AccountsService: public QObject
3938 READ demoEdges
4039 WRITE setDemoEdges
4140 NOTIFY demoEdgesChanged)
41+ Q_PROPERTY (QStringList demoEdgesCompleted
42+ READ demoEdgesCompleted
43+ NOTIFY demoEdgesCompletedChanged)
4244 Q_PROPERTY (bool enableLauncherWhileLocked
4345 READ enableLauncherWhileLocked
4446 NOTIFY enableLauncherWhileLockedChanged)
@@ -68,6 +70,11 @@ class AccountsService: public QObject
6870 Q_PROPERTY(bool hereLicensePathValid // qml sees a null string as "", so we use proxy setting for that
6971 READ hereLicensePathValid
7072 NOTIFY hereLicensePathChanged)
73+ Q_PROPERTY(QString realName READ realName WRITE setRealName NOTIFY realNameChanged)
74+ Q_PROPERTY(QString email READ email WRITE setEmail NOTIFY emailChanged)
75+ Q_PROPERTY(QStringList keymaps
76+ READ keymaps
77+ NOTIFY keymapsChanged)
7178
7279public:
7380 enum PasswordDisplayHint {
@@ -82,6 +89,8 @@ public:
8289 void setUser(const QString &user);
8390 bool demoEdges() const;
8491 void setDemoEdges(bool demoEdges);
92+ QStringList demoEdgesCompleted() const;
93+ Q_INVOKABLE void markDemoEdgeCompleted(const QString &edge);
8594 bool enableLauncherWhileLocked() const;
8695 bool enableIndicatorsWhileLocked() const;
8796 QString backgroundFile() const;
@@ -93,10 +102,16 @@ public:
93102 void setHereEnabled(bool enabled);
94103 QString hereLicensePath() const;
95104 bool hereLicensePathValid() const;
105+ QString realName() const;
106+ void setRealName(const QString &realName);
107+ QString email() const;
108+ void setEmail(const QString &email);
109+ QStringList keymaps() const;
96110
97111Q_SIGNALS:
98112 void userChanged();
99113 void demoEdgesChanged();
114+ void demoEdgesCompletedChanged();
100115 void enableLauncherWhileLockedChanged();
101116 void enableIndicatorsWhileLockedChanged();
102117 void backgroundFileChanged();
@@ -105,6 +120,9 @@ Q_SIGNALS:
105120 void failedLoginsChanged();
106121 void hereEnabledChanged();
107122 void hereLicensePathChanged();
123+ void realNameChanged();
124+ void emailChanged();
125+ void keymapsChanged();
108126
109127private Q_SLOTS:
110128 void onPropertiesChanged(const QString &user, const QString &interface, const QStringList &changed);
plugins/AccountsService/AccountsService.qmltypes+5
@@ -23,6 +23,7 @@ Module {
2323 }
2424 Property { name: "user"; type: "string" }
2525 Property { name: "demoEdges"; type: "bool" }
26+ Property { name: "demoEdgesCompleted"; type: "QStringList"; isReadonly: true }
2627 Property { name: "enableLauncherWhileLocked"; type: "bool"; isReadonly: true }
2728 Property { name: "enableIndicatorsWhileLocked"; type: "bool"; isReadonly: true }
2829 Property { name: "backgroundFile"; type: "string"; isReadonly: true }
@@ -32,5 +33,9 @@ Module {
3233 Property { name: "hereEnabled"; type: "bool" }
3334 Property { name: "hereLicensePath"; type: "string"; isReadonly: true }
3435 Property { name: "hereLicensePathValid"; type: "bool"; isReadonly: true }
36+ Method {
37+ name: "markDemoEdgeCompleted"
38+ Parameter { name: "edge"; type: "string" }
39+ }
3540 }
3641}
plugins/AccountsService/com.canonical.unity.AccountsService.xml+5
@@ -14,6 +14,11 @@
1414 <annotation name="org.freedesktop.Accounts.DefaultValue" value="true"/>
1515 </property>
1616
17+ <!-- List of tutorial pages that have been completed by the user -->
18+ <property name="DemoEdgesCompleted" type="as" access="readwrite">
19+ <annotation name="org.freedesktop.Accounts.DefaultValue" value="[]"/>
20+ </property>
21+
1722 <property name="LauncherItems" type="aa{sv}" access="readwrite">
1823 <annotation name="org.freedesktop.Accounts.DefaultValue" value="[{'defaults': <true>}]"/>
1924 </property>
plugins/Cursor/Cursor.qmltypesnew file+78
@@ -0,0 +1,78 @@
1+import QtQuick.tooling 1.1
2+
3+// This file describes the plugin-supplied types contained in the library.
4+// It is used for QML tooling purposes only.
5+//
6+// This file was auto-generated by:
7+// 'qmlplugindump -notrelocatable Cursor 1.0 plugins'
8+
9+Module {
10+ Component {
11+ name: "MirMousePointerInterface"
12+ defaultProperty: "data"
13+ prototype: "QQuickItem"
14+ Property { name: "cursorName"; type: "string"; isReadonly: true }
15+ Property { name: "themeName"; type: "string"; isReadonly: true }
16+ Property { name: "hotspotX"; type: "double"; isReadonly: true }
17+ Property { name: "hotspotY"; type: "double"; isReadonly: true }
18+ Signal {
19+ name: "cursorNameChanged"
20+ Parameter { name: "name"; type: "string" }
21+ }
22+ Signal {
23+ name: "themeNameChanged"
24+ Parameter { name: "name"; type: "string" }
25+ }
26+ Signal {
27+ name: "hotspotXChanged"
28+ Parameter { name: "value"; type: "double" }
29+ }
30+ Signal {
31+ name: "hotspotYChanged"
32+ Parameter { name: "value"; type: "double" }
33+ }
34+ Method {
35+ name: "handleMouseEvent"
36+ Parameter { name: "timestamp"; type: "ulong" }
37+ Parameter { name: "movement"; type: "QPointF" }
38+ Parameter { name: "buttons"; type: "Qt::MouseButtons" }
39+ Parameter { name: "modifiers"; type: "Qt::KeyboardModifiers" }
40+ }
41+ Method {
42+ name: "handleWheelEvent"
43+ Parameter { name: "timestamp"; type: "ulong" }
44+ Parameter { name: "angleDelta"; type: "QPoint" }
45+ Parameter { name: "modifiers"; type: "Qt::KeyboardModifiers" }
46+ }
47+ }
48+ Component {
49+ name: "MousePointer"
50+ defaultProperty: "data"
51+ prototype: "MirMousePointerInterface"
52+ exports: ["Cursor/MousePointer 1.0"]
53+ exportMetaObjectRevisions: [0]
54+ Signal {
55+ name: "pushedLeftBoundary"
56+ Parameter { name: "amount"; type: "double" }
57+ Parameter { name: "buttons"; type: "Qt::MouseButtons" }
58+ }
59+ Signal {
60+ name: "pushedRightBoundary"
61+ Parameter { name: "amount"; type: "double" }
62+ Parameter { name: "buttons"; type: "Qt::MouseButtons" }
63+ }
64+ Method {
65+ name: "handleMouseEvent"
66+ Parameter { name: "timestamp"; type: "ulong" }
67+ Parameter { name: "movement"; type: "QPointF" }
68+ Parameter { name: "buttons"; type: "Qt::MouseButtons" }
69+ Parameter { name: "modifiers"; type: "Qt::KeyboardModifiers" }
70+ }
71+ Method {
72+ name: "handleWheelEvent"
73+ Parameter { name: "timestamp"; type: "ulong" }
74+ Parameter { name: "angleDelta"; type: "QPoint" }
75+ Parameter { name: "modifiers"; type: "Qt::KeyboardModifiers" }
76+ }
77+ }
78+}
plugins/Dash/ScopeStyle.qml+1−1
@@ -58,7 +58,7 @@ QtObject {
5858 readonly property url headerLogo: "logo" in d.headerStyle ? d.headerStyle["logo"] : ""
5959
6060 /// Background style for the header
61− readonly property url headerBackground: "background" in d.headerStyle ? d.headerStyle["background"] : "color:///#f5f5f5"
61+ readonly property url headerBackground: "background" in d.headerStyle ? d.headerStyle["background"] : "color:///#ffffff"
6262
6363 /// Foreground color for the header
6464 readonly property color headerForeground: "foreground-color" in d.headerStyle ? d.headerStyle["foreground-color"] : foreground
plugins/GlobalShortcut/GlobalShortcut.qmltypesnew file+31
@@ -0,0 +1,31 @@
1+import QtQuick.tooling 1.1
2+
3+// This file describes the plugin-supplied types contained in the library.
4+// It is used for QML tooling purposes only.
5+//
6+// This file was auto-generated by:
7+// 'qmlplugindump -notrelocatable GlobalShortcut 1.0 plugins'
8+
9+Module {
10+ Component {
11+ name: "GlobalShortcut"
12+ defaultProperty: "data"
13+ prototype: "QQuickItem"
14+ exports: ["GlobalShortcut/GlobalShortcut 1.0"]
15+ exportMetaObjectRevisions: [0]
16+ Property { name: "shortcut"; type: "QVariant" }
17+ Property { name: "active"; type: "bool" }
18+ Signal {
19+ name: "shortcutChanged"
20+ Parameter { name: "shortcut"; type: "QVariant" }
21+ }
22+ Signal {
23+ name: "triggered"
24+ Parameter { name: "shortcut"; type: "string" }
25+ }
26+ Signal {
27+ name: "activeChanged"
28+ Parameter { name: "active"; type: "bool" }
29+ }
30+ }
31+}
plugins/ScreenGrabber/screengrabber.cpp−2
@@ -46,12 +46,10 @@ ScreenGrabber::ScreenGrabber(QObject *parent)
4646
4747 QDir screenshotsDir;
4848 if (qEnvironmentVariableIsSet("UNITY_TESTING")) {
49− qDebug() << "Using test environment";
5049 QTemporaryDir tDir;
5150 tDir.setAutoRemove(false);
5251 screenshotsDir = tDir.path();
5352 } else {
54− qDebug() << "Using real environment";
5553 screenshotsDir = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
5654 }
5755 screenshotsDir.mkpath(QStringLiteral("Screenshots"));
plugins/Ubuntu/Gestures/CMakeLists.txt+1
@@ -11,6 +11,7 @@ set(UbuntuGesturesQml_SOURCES
1111 PressedOutsideNotifier.cpp
1212 TouchDispatcher.cpp
1313 TouchGate.cpp
14+ TouchGestureArea.cpp
1415)
1516
1617add_definitions(-DUBUNTUGESTURESQML_LIBRARY)
plugins/Ubuntu/Gestures/DirectionalDragArea.cpp+43−5
@@ -194,6 +194,26 @@ void DirectionalDragArea::setImmediateRecognition(bool enabled)
194194 }
195195}
196196
197+bool DirectionalDragArea::monitorOnly() const
198+{
199+ return d->monitorOnly;
200+}
201+
202+void DirectionalDragArea::setMonitorOnly(bool monitorOnly)
203+{
204+ if (d->monitorOnly != monitorOnly) {
205+ d->monitorOnly = monitorOnly;
206+
207+ if (monitorOnly && d->status == DirectionalDragAreaPrivate::Undecided) {
208+ TouchRegistry::instance()->removeCandidateOwnerForTouch(d->touchId, this);
209+ // We still wanna know when it ends for keeping the composition time window up-to-date
210+ TouchRegistry::instance()->addTouchWatcher(d->touchId, this);
211+ }
212+
213+ Q_EMIT monitorOnlyChanged(monitorOnly);
214+ }
215+}
216+
197217void DirectionalDragArea::removeTimeConstraints()
198218{
199219 d->setMaxTime(60 * 60 * 1000);
@@ -260,7 +280,10 @@ void DirectionalDragAreaPrivate::unownedTouchEvent(UnownedTouchEvent *unownedTou
260280 unownedTouchEvent_undecided(unownedTouchEvent);
261281 break;
262282 default: // Recognized:
263− // do nothing
283+ if (monitorOnly) {
284+ // Treat unowned event as if we owned it, but we are really just watching it
285+ touchEvent_recognized(event);
286+ }
264287 break;
265288 }
266289
@@ -311,7 +334,9 @@ void DirectionalDragAreaPrivate::unownedTouchEvent_undecided(UnownedTouchEvent *
311334 }
312335
313336 if (movedFarEnoughAlongGestureAxis()) {
314− TouchRegistry::instance()->requestTouchOwnership(touchId, q);
337+ if (!monitorOnly) {
338+ TouchRegistry::instance()->requestTouchOwnership(touchId, q);
339+ }
315340 setStatus(Recognized);
316341 setPublicPos(touchPoint->pos());
317342 setPublicScenePos(touchScenePos);
@@ -411,12 +436,21 @@ void DirectionalDragAreaPrivate::touchEvent_absent(QTouchEvent *event)
411436 if (recognitionIsDisabled()) {
412437 // Behave like a dumb TouchArea
413438 ddaDebug("Gesture recognition is disabled. Requesting touch ownership immediately.");
414− TouchRegistry::instance()->requestTouchOwnership(touchId, q);
415439 setStatus(Recognized);
416− event->accept();
440+ if (monitorOnly) {
441+ watchPressedTouchPoints(touchPoints);
442+ event->ignore();
443+ } else {
444+ TouchRegistry::instance()->requestTouchOwnership(touchId, q);
445+ event->accept();
446+ }
417447 } else {
418448 // just monitor the touch points for now.
419− TouchRegistry::instance()->addCandidateOwnerForTouch(touchId, q);
449+ if (monitorOnly) {
450+ watchPressedTouchPoints(touchPoints);
451+ } else {
452+ TouchRegistry::instance()->addCandidateOwnerForTouch(touchId, q);
453+ }
420454
421455 setStatus(Undecided);
422456 // Let the item below have it. We will monitor it and grab it later if a gesture
@@ -606,6 +640,9 @@ void DirectionalDragAreaPrivate::setStatus(Status newStatus)
606640 Q_EMIT q->pressedChanged(true);
607641 break;
608642 case Recognized:
643+ if (oldStatus == WaitingForTouch) { // for immediate recognition
644+ Q_EMIT q->pressedChanged(true);
645+ }
609646 Q_EMIT q->draggingChanged(true);
610647 break;
611648 default:
@@ -890,5 +927,6 @@ DirectionalDragAreaPrivate::DirectionalDragAreaPrivate(DirectionalDragArea *q)
890927 , recognitionTimer(nullptr)
891928 , timeSource(new RealTimeSource)
892929 , activeTouches(timeSource)
930+ , monitorOnly(false)
893931{
894932}
plugins/Ubuntu/Gestures/DirectionalDragArea.h+8
@@ -77,6 +77,10 @@ class UBUNTUGESTURESQML_EXPORT DirectionalDragArea : public QQuickItem {
7777 WRITE setImmediateRecognition
7878 NOTIFY immediateRecognitionChanged)
7979
80+ // Whether we are merely monitoring touch events (in which case, we don't
81+ // claim ownership of the touch).
82+ Q_PROPERTY(bool monitorOnly READ monitorOnly WRITE setMonitorOnly NOTIFY monitorOnlyChanged)
83+
8084 Q_ENUMS(Direction)
8185public:
8286 DirectionalDragArea(QQuickItem *parent = 0);
@@ -100,6 +104,9 @@ public:
100104 bool immediateRecognition() const;
101105 void setImmediateRecognition(bool enabled);
102106
107+ bool monitorOnly() const;
108+ void setMonitorOnly(bool monitorOnly);
109+
103110 bool event(QEvent *e) override;
104111
105112 /*
@@ -123,6 +130,7 @@ Q_SIGNALS:
123130 void touchSceneXChanged(qreal value);
124131 void touchSceneYChanged(qreal value);
125132 void immediateRecognitionChanged(bool value);
133+ void monitorOnlyChanged(bool value);
126134
127135protected:
128136 void touchEvent(QTouchEvent *event) override;
plugins/Ubuntu/Gestures/DirectionalDragArea_p.h+2
@@ -160,6 +160,8 @@ public:
160160
161161 ActiveTouchesInfo activeTouches;
162162
163+ bool monitorOnly;
164+
163165Q_SIGNALS:
164166 void statusChanged(Status value);
165167};
plugins/Ubuntu/Gestures/Gestures.qmltypes+86−45
@@ -34,7 +34,8 @@ Module {
3434 "Leftwards": 1,
3535 "Downwards": 2,
3636 "Upwards": 3,
37− "Horizontal": 4
37+ "Horizontal": 4,
38+ "Vertical": 5
3839 }
3940 }
4041 Method {
@@ -59,14 +60,6 @@ Module {
5960 prototype: "QQuickItem"
6061 exports: ["Ubuntu.Gestures/DirectionalDragArea 0.1"]
6162 exportMetaObjectRevisions: [0]
62− Enum {
63− name: "Status"
64− values: {
65− "WaitingForTouch": 0,
66− "Undecided": 1,
67− "Recognized": 2
68− }
69− }
7063 Property { name: "direction"; type: "Direction::Type" }
7164 Property { name: "distance"; type: "double"; isReadonly: true }
7265 Property { name: "sceneDistance"; type: "double"; isReadonly: true }
@@ -74,24 +67,19 @@ Module {
7467 Property { name: "touchY"; type: "double"; isReadonly: true }
7568 Property { name: "touchSceneX"; type: "double"; isReadonly: true }
7669 Property { name: "touchSceneY"; type: "double"; isReadonly: true }
77− Property { name: "status"; type: "Status"; isReadonly: true }
7870 Property { name: "dragging"; type: "bool"; isReadonly: true }
79− Property { name: "maxDeviation"; type: "double" }
80− Property { name: "wideningAngle"; type: "double" }
81− Property { name: "distanceThreshold"; type: "double" }
82− Property { name: "minSpeed"; type: "double" }
83− Property { name: "maxSilenceTime"; type: "int" }
84− Property { name: "compositionTime"; type: "int" }
71+ Property { name: "pressed"; type: "bool"; isReadonly: true }
72+ Property { name: "immediateRecognition"; type: "bool" }
8573 Signal {
8674 name: "directionChanged"
8775 Parameter { name: "direction"; type: "Direction::Type" }
8876 }
8977 Signal {
90− name: "statusChanged"
91− Parameter { name: "value"; type: "Status" }
78+ name: "draggingChanged"
79+ Parameter { name: "value"; type: "bool" }
9280 }
9381 Signal {
94− name: "draggingChanged"
82+ name: "pressedChanged"
9583 Parameter { name: "value"; type: "bool" }
9684 }
9785 Signal {
@@ -102,30 +90,6 @@ Module {
10290 name: "sceneDistanceChanged"
10391 Parameter { name: "value"; type: "double" }
10492 }
105− Signal {
106− name: "maxDeviationChanged"
107− Parameter { name: "value"; type: "double" }
108− }
109− Signal {
110− name: "wideningAngleChanged"
111− Parameter { name: "value"; type: "double" }
112− }
113− Signal {
114− name: "distanceThresholdChanged"
115− Parameter { name: "value"; type: "double" }
116− }
117− Signal {
118− name: "minSpeedChanged"
119− Parameter { name: "value"; type: "double" }
120− }
121− Signal {
122− name: "maxSilenceTimeChanged"
123− Parameter { name: "value"; type: "int" }
124− }
125− Signal {
126− name: "compositionTimeChanged"
127− Parameter { name: "value"; type: "int" }
128− }
12993 Signal {
13094 name: "touchXChanged"
13195 Parameter { name: "value"; type: "double" }
@@ -142,7 +106,35 @@ Module {
142106 name: "touchSceneYChanged"
143107 Parameter { name: "value"; type: "double" }
144108 }
145− Signal { name: "tapped" }
109+ Signal {
110+ name: "immediateRecognitionChanged"
111+ Parameter { name: "value"; type: "bool" }
112+ }
113+ Method { name: "removeTimeConstraints" }
114+ }
115+ Component {
116+ name: "FloatingFlickable"
117+ defaultProperty: "data"
118+ prototype: "QQuickItem"
119+ exports: ["Ubuntu.Gestures/FloatingFlickable 0.1"]
120+ exportMetaObjectRevisions: [0]
121+ Property { name: "contentWidth"; type: "double" }
122+ Property { name: "contentHeight"; type: "double" }
123+ Property { name: "contentX"; type: "double" }
124+ Property { name: "contentY"; type: "double" }
125+ Property { name: "direction"; type: "Direction::Type" }
126+ }
127+ Component {
128+ name: "GestureTouchPoint"
129+ prototype: "QObject"
130+ exports: ["Ubuntu.Gestures/GestureTouchPoint 0.1"]
131+ isCreatable: false
132+ exportMetaObjectRevisions: [0]
133+ Property { name: "pointId"; type: "int"; isReadonly: true }
134+ Property { name: "pressed"; type: "bool"; isReadonly: true }
135+ Property { name: "x"; type: "double"; isReadonly: true }
136+ Property { name: "y"; type: "double"; isReadonly: true }
137+ Property { name: "dragging"; type: "bool"; isReadonly: true }
146138 }
147139 Component {
148140 name: "PressedOutsideNotifier"
@@ -163,6 +155,55 @@ Module {
163155 name: "targetItemChanged"
164156 Parameter { name: "item"; type: "QQuickItem"; isPointer: true }
165157 }
166− Signal { name: "pressed" }
158+ }
159+ Component {
160+ name: "TouchGestureArea"
161+ defaultProperty: "data"
162+ prototype: "QQuickItem"
163+ exports: ["Ubuntu.Gestures/TouchGestureArea 0.1"]
164+ exportMetaObjectRevisions: [0]
165+ Enum {
166+ name: "Status"
167+ values: {
168+ "WaitingForTouch": 0,
169+ "Undecided": 1,
170+ "Recognized": 2,
171+ "Rejected": 3
172+ }
173+ }
174+ Property { name: "touchPoints"; type: "GestureTouchPoint"; isList: true; isReadonly: true }
175+ Property { name: "dragging"; type: "bool"; isReadonly: true }
176+ Property { name: "minimumTouchPoints"; type: "int" }
177+ Property { name: "maximumTouchPoints"; type: "int" }
178+ Signal {
179+ name: "statusChanged"
180+ Parameter { name: "status"; type: "Status" }
181+ }
182+ Signal { name: "touchPointsUpdated" }
183+ Signal {
184+ name: "draggingChanged"
185+ Parameter { name: "dragging"; type: "bool" }
186+ }
187+ Signal {
188+ name: "minimumTouchPointsChanged"
189+ Parameter { name: "value"; type: "bool" }
190+ }
191+ Signal {
192+ name: "maximumTouchPointsChanged"
193+ Parameter { name: "value"; type: "bool" }
194+ }
195+ Signal {
196+ name: "pressed"
197+ Parameter { name: "points"; type: "QList<QObject*>" }
198+ }
199+ Signal {
200+ name: "released"
201+ Parameter { name: "points"; type: "QList<QObject*>" }
202+ }
203+ Signal {
204+ name: "updated"
205+ Parameter { name: "points"; type: "QList<QObject*>" }
206+ }
207+ Signal { name: "clicked" }
167208 }
168209}
plugins/Ubuntu/Gestures/TouchGestureArea.cppnew file+875
@@ -0,0 +1,875 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "TouchGestureArea.h"
18+
19+// local
20+#include "TouchOwnershipEvent.h"
21+#include "TouchRegistry.h"
22+#include "UnownedTouchEvent.h"
23+
24+#include <QGuiApplication>
25+#include <QStyleHints>
26+#include <private/qquickwindow_p.h>
27+
28+#define TOUCHGESTUREAREA_DEBUG 0
29+
30+// TODO - understand more about why we lose touch event releases.
31+// Workaround for now is to monitor all the touch points from first touch till
32+// all have been released; no matter if we've rejected the gesture.
33+
34+namespace {
35+
36+struct InternalStatus {
37+ enum Status {
38+ WaitingForTouch,
39+ WaitingForMoreTouches,
40+ WaitingForOwnership, //Recognizing,
41+ Recognized,
42+ WaitingForRejection,
43+ Rejected
44+ };
45+};
46+
47+TouchGestureArea::Status internalStatusToGestureStatus(int internalStatus) {
48+ switch (internalStatus) {
49+ case InternalStatus::WaitingForTouch: return TouchGestureArea::WaitingForTouch;
50+ case InternalStatus::WaitingForMoreTouches: return TouchGestureArea::Undecided;
51+ case InternalStatus::WaitingForOwnership: return TouchGestureArea::Undecided;
52+ case InternalStatus::Recognized: return TouchGestureArea::Recognized;
53+ case InternalStatus::WaitingForRejection: return TouchGestureArea::Recognized;
54+ case InternalStatus::Rejected: return TouchGestureArea::Rejected;
55+ }
56+ return TouchGestureArea::WaitingForTouch;
57+}
58+
59+}
60+
61+#if TOUCHGESTUREAREA_DEBUG
62+#define tgaDebug(params) qDebug().nospace() << "[TGA(" << qPrintable(objectName()) << ")] " << params
63+#include "DebugHelpers.h"
64+
65+namespace {
66+
67+const char *statusToString(int status)
68+{
69+ if (status == InternalStatus::WaitingForTouch) {
70+ return "WaitingForTouch";
71+ } else if (status == InternalStatus::WaitingForMoreTouches) {
72+ return "WaitingForMoreTouches";
73+ } else if (status == InternalStatus::WaitingForOwnership) {
74+ return "WaitingForOwnership";
75+ } else if (status == InternalStatus::Rejected) {
76+ return "Rejected";
77+ } else if (status == InternalStatus::WaitingForRejection) {
78+ return "WaitingForRejection";
79+ } else {
80+ return "Recognized";
81+ }
82+ return "Unknown";
83+}
84+
85+QString touchState(Qt::TouchPointState state) {
86+ switch (state) {
87+ case Qt::TouchPointPressed: return "pressed";
88+ case Qt::TouchPointMoved: return "moved";
89+ case Qt::TouchPointStationary: return "stationary";
90+ case Qt::TouchPointReleased: return "released";
91+ break;
92+ }
93+ return "unknown";
94+}
95+
96+QString touchesString(const QList<QObject*> touches) {
97+ QString str;
98+ Q_FOREACH(QObject* object, touches) {
99+ GestureTouchPoint* touchPoint = qobject_cast<GestureTouchPoint*>(object);
100+ if (touchPoint) {
101+ str += QStringLiteral("[%1 @ (%2, %3)], ").arg(touchPoint->id())
102+ .arg(touchPoint->x())
103+ .arg(touchPoint->y());
104+ }
105+ }
106+ return str;
107+}
108+
109+QString touchEventString(QTouchEvent* event) {
110+ if (!event) return QString();
111+ QString str;
112+ Q_FOREACH(const auto& touchPoint, event->touchPoints()) {
113+ str += QStringLiteral("[%1:%2 @ (%3, %4)], ").arg(touchPoint.id())
114+ .arg(touchState(touchPoint.state()))
115+ .arg(touchPoint.pos().x())
116+ .arg(touchPoint.pos().y());
117+ }
118+ return str;
119+}
120+
121+
122+} // namespace {
123+#else // TOUCHGESTUREAREA_DEBUG
124+#define tgaDebug(params) ((void)0)
125+#endif // TOUCHGESTUREAREA_DEBUG
126+
127+TouchGestureArea::TouchGestureArea(QQuickItem* parent)
128+ : QQuickItem(parent)
129+ , m_status(WaitingForTouch)
130+ , m_recognitionTimer(nullptr)
131+ , m_dragging(false)
132+ , m_minimumTouchPoints(1)
133+ , m_maximumTouchPoints(INT_MAX)
134+ , m_recognitionPeriod(50)
135+ , m_releaseRejectPeriod(100)
136+{
137+ setRecognitionTimer(new UbuntuGestures::Timer(this));
138+ m_recognitionTimer->setInterval(m_recognitionPeriod);
139+ m_recognitionTimer->setSingleShot(true);
140+}
141+
142+TouchGestureArea::~TouchGestureArea()
143+{
144+ clearTouchLists();
145+ qDeleteAll(m_liveTouchPoints);
146+ m_liveTouchPoints.clear();
147+ qDeleteAll(m_cachedTouchPoints);
148+ m_cachedTouchPoints.clear();
149+}
150+
151+bool TouchGestureArea::event(QEvent *event)
152+{
153+ // Process unowned touch events (handles update/release for incomplete gestures)
154+ if (event->type() == TouchOwnershipEvent::touchOwnershipEventType()) {
155+ touchOwnershipEvent(static_cast<TouchOwnershipEvent *>(event));
156+ return true;
157+ } else if (event->type() == UnownedTouchEvent::unownedTouchEventType()) {
158+ unownedTouchEvent(static_cast<UnownedTouchEvent *>(event)->touchEvent());
159+ return true;
160+ }
161+
162+ return QQuickItem::event(event);
163+}
164+
165+void TouchGestureArea::touchOwnershipEvent(TouchOwnershipEvent *event)
166+{
167+ int touchId = event->touchId();
168+ tgaDebug("touchOwnershipEvent - id:" << touchId << ", gained:" << event->gained());
169+
170+ if (event->gained()) {
171+ grabTouchPoints(QVector<int>() << touchId);
172+ m_candidateTouches.remove(touchId);
173+ TouchRegistry::instance()->addTouchWatcher(touchId, this);
174+ m_watchedTouches.insert(touchId);
175+
176+ if (m_watchedTouches.count() >= m_minimumTouchPoints) {
177+ setInternalStatus(InternalStatus::Recognized);
178+ }
179+ } else {
180+ rejectGesture();
181+ }
182+}
183+
184+void TouchGestureArea::touchEvent(QTouchEvent *event)
185+{
186+ if (!isEnabled() || !isVisible()) {
187+ tgaDebug(QString("NOT ENABLED touchEvent(%1) %2").arg(statusToString(m_status)).arg(touchEventString(event)));
188+ QQuickItem::touchEvent(event);
189+ return;
190+ }
191+
192+ tgaDebug(QString("touchEvent(%1) %2").arg(statusToString(m_status)).arg(touchEventString(event)));
193+
194+ switch (m_status) {
195+ case InternalStatus::WaitingForTouch:
196+ touchEvent_waitingForTouch(event);
197+ break;
198+ case InternalStatus::WaitingForMoreTouches:
199+ touchEvent_waitingForMoreTouches(event);
200+ break;
201+ case InternalStatus::WaitingForOwnership:
202+ touchEvent_waitingForOwnership(event);
203+ break;
204+ case InternalStatus::Recognized:
205+ case InternalStatus::WaitingForRejection:
206+ touchEvent_recognized(event);
207+ break;
208+ case InternalStatus::Rejected:
209+ touchEvent_rejected(event);
210+ break;
211+ default: // Recognized:
212+ break;
213+ }
214+
215+ updateTouchPoints(event);
216+}
217+
218+void TouchGestureArea::touchEvent_waitingForTouch(QTouchEvent *event)
219+{
220+ Q_FOREACH(const QTouchEvent::TouchPoint& touchPoint, event->touchPoints()) {
221+ Qt::TouchPointState touchPointState = touchPoint.state();
222+ int touchId = touchPoint.id();
223+
224+ if (touchPointState == Qt::TouchPointPressed) {
225+ if (!m_candidateTouches.contains(touchId)) {
226+ TouchRegistry::instance()->addCandidateOwnerForTouch(touchId, this);
227+ m_candidateTouches.insert(touchId);
228+ }
229+ }
230+ }
231+ event->ignore();
232+
233+ if (m_candidateTouches.count() > m_maximumTouchPoints) {
234+ rejectGesture();
235+ } else if (m_candidateTouches.count() >= m_minimumTouchPoints) {
236+ setInternalStatus(InternalStatus::WaitingForOwnership);
237+
238+ QSet<int> tmpCandidates(m_candidateTouches);
239+ Q_FOREACH(int candidateTouchId, tmpCandidates) {
240+ TouchRegistry::instance()->requestTouchOwnership(candidateTouchId, this);
241+ }
242+ // We accept the gesture; so don't pass to lower items
243+ event->accept();
244+ } else if (m_candidateTouches.count() > 0) {
245+ setInternalStatus(InternalStatus::WaitingForMoreTouches);
246+ }
247+}
248+
249+void TouchGestureArea::touchEvent_waitingForMoreTouches(QTouchEvent *event)
250+{
251+ Q_FOREACH(const QTouchEvent::TouchPoint& touchPoint, event->touchPoints()) {
252+ Qt::TouchPointState touchPointState = touchPoint.state();
253+ int touchId = touchPoint.id();
254+
255+ if (touchPointState == Qt::TouchPointPressed) {
256+ if (!m_candidateTouches.contains(touchId)) {
257+ TouchRegistry::instance()->addCandidateOwnerForTouch(touchId, this);
258+ m_candidateTouches.insert(touchId);
259+ }
260+ }
261+ }
262+ event->ignore();
263+
264+ if (m_candidateTouches.count() > m_maximumTouchPoints) {
265+ rejectGesture();
266+ } else if (m_candidateTouches.count() >= m_minimumTouchPoints) {
267+ setInternalStatus(InternalStatus::WaitingForOwnership);
268+
269+ QSet<int> tmpCandidates(m_candidateTouches);
270+ Q_FOREACH(int candidateTouchId, tmpCandidates) {
271+ TouchRegistry::instance()->requestTouchOwnership(candidateTouchId, this);
272+ }
273+ // We accept the gesture; so don't pass to lower items
274+ event->accept();
275+ }
276+}
277+
278+void TouchGestureArea::touchEvent_waitingForOwnership(QTouchEvent *event)
279+{
280+ Q_FOREACH(const QTouchEvent::TouchPoint& touchPoint, event->touchPoints()) {
281+ Qt::TouchPointState touchPointState = touchPoint.state();
282+ int touchId = touchPoint.id();
283+
284+ if (touchPointState == Qt::TouchPointPressed) {
285+ if (!m_watchedTouches.contains(touchId)) {
286+ TouchRegistry::instance()->addTouchWatcher(touchId, this);
287+ m_watchedTouches.insert(touchId);
288+ }
289+ }
290+ }
291+}
292+
293+void TouchGestureArea::touchEvent_recognized(QTouchEvent *event)
294+{
295+ Q_FOREACH(const QTouchEvent::TouchPoint& touchPoint, event->touchPoints()) {
296+ Qt::TouchPointState touchPointState = touchPoint.state();
297+ int touchId = touchPoint.id();
298+
299+ if (touchPointState == Qt::TouchPointPressed) {
300+ if (!m_watchedTouches.contains(touchId)) {
301+ TouchRegistry::instance()->addTouchWatcher(touchId, this);
302+ m_watchedTouches.insert(touchId);
303+ }
304+ }
305+ }
306+
307+ if (m_watchedTouches.count() > m_maximumTouchPoints) {
308+ rejectGesture();
309+ } else if (m_watchedTouches.count() >= m_minimumTouchPoints &&
310+ m_status==InternalStatus::WaitingForRejection) {
311+ setInternalStatus(InternalStatus::Recognized);
312+ }
313+}
314+
315+void TouchGestureArea::touchEvent_rejected(QTouchEvent *event)
316+{
317+ Q_FOREACH(const QTouchEvent::TouchPoint& touchPoint, event->touchPoints()) {
318+ Qt::TouchPointState touchPointState = touchPoint.state();
319+ int touchId = touchPoint.id();
320+
321+ if (touchPointState == Qt::TouchPointPressed) {
322+ if (!m_watchedTouches.contains(touchId)) {
323+ TouchRegistry::instance()->addTouchWatcher(touchId, this);
324+ m_watchedTouches.insert(touchId);
325+ }
326+ }
327+ }
328+}
329+
330+void TouchGestureArea::unownedTouchEvent(QTouchEvent *unownedTouchEvent)
331+{
332+ tgaDebug(QString("unownedTouchEvent(%1) %2").arg(statusToString(m_status)).arg(touchEventString(unownedTouchEvent)));
333+
334+ // Only monitor unowned touch events for presses/releases
335+ if ((unownedTouchEvent->touchPointStates() & (Qt::TouchPointPressed|Qt::TouchPointReleased)) == 0) {
336+ return;
337+ }
338+
339+ switch (m_status) {
340+ case InternalStatus::WaitingForTouch:
341+ break;
342+ case InternalStatus::WaitingForMoreTouches:
343+ unownedTouchEvent_waitingForMoreTouches(unownedTouchEvent);
344+ // do nothing
345+ break;
346+ case InternalStatus::WaitingForOwnership:
347+ unownedTouchEvent_waitingForOwnership(unownedTouchEvent);
348+ break;
349+ case InternalStatus::Recognized:
350+ case InternalStatus::WaitingForRejection:
351+ unownedTouchEvent_recognised(unownedTouchEvent);
352+ break;
353+ case InternalStatus::Rejected:
354+ unownedTouchEvent_rejected(unownedTouchEvent);
355+ break;
356+ default:
357+ break;
358+ }
359+
360+ updateTouchPoints(unownedTouchEvent);
361+}
362+
363+void TouchGestureArea::unownedTouchEvent_waitingForMoreTouches(QTouchEvent *event)
364+{
365+ Q_FOREACH(const QTouchEvent::TouchPoint& touchPoint, event->touchPoints()) {
366+ Qt::TouchPointState touchPointState = touchPoint.state();
367+ int touchId = touchPoint.id();
368+
369+ if (touchPointState == Qt::TouchPointReleased) {
370+ if (m_candidateTouches.contains(touchId)) {
371+ TouchRegistry::instance()->removeCandidateOwnerForTouch(touchId, this);
372+ m_candidateTouches.remove(touchId);
373+ }
374+ }
375+ }
376+
377+ if (m_candidateTouches.count() == 0) {
378+ setInternalStatus(InternalStatus::WaitingForTouch);
379+ }
380+}
381+
382+void TouchGestureArea::unownedTouchEvent_waitingForOwnership(QTouchEvent *event)
383+{
384+ Q_FOREACH(const QTouchEvent::TouchPoint& touchPoint, event->touchPoints()) {
385+ Qt::TouchPointState touchPointState = touchPoint.state();
386+ int touchId = touchPoint.id();
387+
388+ if (touchPointState == Qt::TouchPointReleased) {
389+ if (m_candidateTouches.contains(touchId)) {
390+ TouchRegistry::instance()->removeCandidateOwnerForTouch(touchId, this);
391+ m_candidateTouches.remove(touchId);
392+ }
393+ if (m_watchedTouches.contains(touchId)) {
394+ m_watchedTouches.remove(touchId);
395+ }
396+ }
397+ }
398+
399+ if (m_candidateTouches.count() + m_watchedTouches.count() == 0) {
400+ setInternalStatus(InternalStatus::WaitingForTouch);
401+ }
402+}
403+
404+void TouchGestureArea::unownedTouchEvent_recognised(QTouchEvent *event)
405+{
406+ Q_FOREACH(const QTouchEvent::TouchPoint& touchPoint, event->touchPoints()) {
407+ Qt::TouchPointState touchPointState = touchPoint.state();
408+ int touchId = touchPoint.id();
409+
410+ if (touchPointState == Qt::TouchPointReleased) {
411+ if (m_watchedTouches.contains(touchId)) {
412+ m_watchedTouches.remove(touchId);
413+ }
414+ }
415+ }
416+
417+ if (m_watchedTouches.count() < m_minimumTouchPoints && m_status==InternalStatus::Recognized) {
418+ setInternalStatus(InternalStatus::WaitingForRejection);
419+ }
420+}
421+
422+void TouchGestureArea::unownedTouchEvent_rejected(QTouchEvent *event)
423+{
424+ Q_FOREACH(const QTouchEvent::TouchPoint& touchPoint, event->touchPoints()) {
425+ Qt::TouchPointState touchPointState = touchPoint.state();
426+ int touchId = touchPoint.id();
427+
428+ if (touchPointState == Qt::TouchPointPressed) {
429+ if (!m_watchedTouches.contains(touchId)) {
430+ TouchRegistry::instance()->addTouchWatcher(touchId, this);
431+ m_watchedTouches.insert(touchId);
432+ }
433+ }
434+ if (touchPointState == Qt::TouchPointReleased) {
435+ if (m_watchedTouches.contains(touchId)) {
436+ m_watchedTouches.remove(touchId);
437+ }
438+ }
439+ }
440+
441+ if (m_watchedTouches.count() == 0) {
442+ setInternalStatus(InternalStatus::WaitingForTouch);
443+ }
444+}
445+
446+void TouchGestureArea::updateTouchPoints(QTouchEvent *touchEvent)
447+{
448+ bool added = false;
449+ bool ended = false;
450+ bool moved = false;
451+
452+ const int dragThreshold = qApp->styleHints()->startDragDistance();
453+ const int dragVelocity = qApp->styleHints()->startDragVelocity();
454+
455+ clearTouchLists();
456+ bool updateable = m_status != InternalStatus::WaitingForRejection;
457+
458+ Q_FOREACH(const QTouchEvent::TouchPoint& touchPoint, touchEvent->touchPoints()) {
459+ Qt::TouchPointState touchPointState = touchPoint.state();
460+ int touchId = touchPoint.id();
461+
462+ if (touchPointState & Qt::TouchPointReleased) {
463+ GestureTouchPoint* gtp = m_liveTouchPoints.value(touchId);
464+ if (!gtp) continue;
465+
466+ gtp->setPos(touchPoint.pos());
467+ gtp->setPressed(false);
468+ m_releasedTouchPoints.append(gtp);
469+ m_liveTouchPoints.remove(touchId);
470+
471+ if (updateable) {
472+ if (m_cachedTouchPoints.contains(touchId)) {
473+ GestureTouchPoint* cachedPoint = m_cachedTouchPoints.take(touchId);
474+ cachedPoint->deleteLater();
475+ }
476+ }
477+ ended = true;
478+ } else {
479+ GestureTouchPoint* gtp = m_liveTouchPoints.value(touchPoint.id(), nullptr);
480+ if (!gtp) {
481+ gtp = addTouchPoint(&touchPoint);
482+ m_pressedTouchPoints.append(gtp);
483+
484+ if (updateable) {
485+ if (m_cachedTouchPoints.contains(touchId)) {
486+ m_cachedTouchPoints[touchId]->setPos(touchPoint.pos());
487+ } else {
488+ m_cachedTouchPoints[touchId] = new GestureTouchPoint(*gtp);
489+ }
490+ }
491+ added = true;
492+ } else if (touchPointState & Qt::TouchPointMoved) {
493+ gtp->setPos(touchPoint.pos());
494+ m_movedTouchPoints.append(gtp);
495+ moved = true;
496+
497+ const QPointF &currentPos = touchPoint.scenePos();
498+ const QPointF &startPos = touchPoint.startScenePos();
499+
500+ bool overDragThreshold = false;
501+ bool supportsVelocity = (touchEvent->device()->capabilities() & QTouchDevice::Velocity) && dragVelocity;
502+ overDragThreshold |= qAbs(currentPos.x() - startPos.x()) > dragThreshold ||
503+ qAbs(currentPos.y() - startPos.y()) > dragThreshold;
504+ if (supportsVelocity) {
505+ QVector2D velocityVec = touchPoint.velocity();
506+ overDragThreshold |= qAbs(velocityVec.x()) > dragVelocity;
507+ overDragThreshold |= qAbs(velocityVec.y()) > dragVelocity;
508+ }
509+
510+ if (overDragThreshold) {
511+ gtp->setDragging(true);
512+ }
513+
514+ if (updateable) {
515+ if (m_cachedTouchPoints.contains(touchId)) {
516+ m_cachedTouchPoints[touchId]->setPos(touchPoint.pos());
517+ if (overDragThreshold) {
518+ m_cachedTouchPoints[touchId]->setDragging(true);
519+ }
520+ }
521+ }
522+ }
523+ }
524+ }
525+
526+ if (updateable) {
527+ if (!dragging() && m_status == InternalStatus::Recognized) {
528+ bool allWantDrag = !m_liveTouchPoints.isEmpty();
529+ Q_FOREACH(auto point, m_liveTouchPoints) {
530+ allWantDrag &= point->dragging();
531+ }
532+ // only dragging if all points are dragging.
533+ if (allWantDrag) {
534+ setDragging(true);
535+ }
536+ }
537+
538+ if (ended) {
539+ if (m_liveTouchPoints.isEmpty()) {
540+ if (!dragging()) Q_EMIT clicked();
541+ setDragging(false);
542+ }
543+ tgaDebug("Released " << touchesString(m_releasedTouchPoints));
544+ Q_EMIT released(m_releasedTouchPoints);
545+ }
546+ if (added) {
547+ tgaDebug("Pressed " << touchesString(m_pressedTouchPoints));
548+ Q_EMIT pressed(m_pressedTouchPoints);
549+ }
550+ if (moved) {
551+ tgaDebug("Updated " << touchesString(m_movedTouchPoints));
552+ Q_EMIT updated(m_movedTouchPoints);
553+ }
554+ if (added || ended || moved) {
555+ Q_EMIT touchPointsUpdated();
556+ }
557+ }
558+}
559+
560+void TouchGestureArea::clearTouchLists()
561+{
562+ Q_FOREACH (QObject *gtp, m_releasedTouchPoints) {
563+ delete gtp;
564+ }
565+ m_releasedTouchPoints.clear();
566+ m_pressedTouchPoints.clear();
567+ m_movedTouchPoints.clear();
568+}
569+
570+void TouchGestureArea::setInternalStatus(uint newStatus)
571+{
572+ if (newStatus == m_status)
573+ return;
574+
575+ uint oldStatus = m_status;
576+
577+ m_status = newStatus;
578+ Q_EMIT statusChanged(status());
579+
580+ if (oldStatus == InternalStatus::WaitingForMoreTouches || oldStatus == InternalStatus::WaitingForRejection) {
581+ m_recognitionTimer->stop();
582+ }
583+
584+ tgaDebug(statusToString(oldStatus) << " -> " << statusToString(newStatus));
585+
586+ switch (newStatus) {
587+ case InternalStatus::WaitingForTouch:
588+ resyncCachedTouchPoints();
589+ break;
590+ case InternalStatus::WaitingForMoreTouches:
591+ m_recognitionTimer->start(m_recognitionPeriod);
592+ break;
593+ case InternalStatus::Recognized:
594+ resyncCachedTouchPoints();
595+ break;
596+ case InternalStatus::WaitingForRejection:
597+ m_recognitionTimer->start(m_releaseRejectPeriod);
598+ break;
599+ case InternalStatus::Rejected:
600+ resyncCachedTouchPoints();
601+ break;
602+ default:
603+ // no-op
604+ break;
605+ }
606+}
607+
608+void TouchGestureArea::setRecognitionTimer(UbuntuGestures::AbstractTimer *timer)
609+{
610+ int interval = 0;
611+ bool timerWasRunning = false;
612+ bool wasSingleShot = false;
613+
614+ // can be null when called from the constructor
615+ if (m_recognitionTimer) {
616+ interval = m_recognitionTimer->interval();
617+ timerWasRunning = m_recognitionTimer->isRunning();
618+ if (m_recognitionTimer->parent() == this) {
619+ delete m_recognitionTimer;
620+ }
621+ }
622+
623+ m_recognitionTimer = timer;
624+ timer->setInterval(interval);
625+ timer->setSingleShot(wasSingleShot);
626+ connect(timer, SIGNAL(timeout()),
627+ this, SLOT(rejectGesture()));
628+ if (timerWasRunning) {
629+ m_recognitionTimer->start();
630+ }
631+}
632+
633+int TouchGestureArea::status() const
634+{
635+ return internalStatusToGestureStatus(m_status);
636+}
637+
638+bool TouchGestureArea::dragging() const
639+{
640+ return m_dragging;
641+}
642+
643+QQmlListProperty<GestureTouchPoint> TouchGestureArea::touchPoints()
644+{
645+ return QQmlListProperty<GestureTouchPoint>(this,
646+ 0,
647+ nullptr,
648+ TouchGestureArea::touchPoint_count,
649+ TouchGestureArea::touchPoint_at,
650+ 0);
651+}
652+
653+int TouchGestureArea::minimumTouchPoints() const
654+{
655+ return m_minimumTouchPoints;
656+}
657+
658+void TouchGestureArea::setMinimumTouchPoints(int value)
659+{
660+ if (m_minimumTouchPoints != value) {
661+ m_minimumTouchPoints = value;
662+ Q_EMIT minimumTouchPointsChanged(value);
663+ }
664+}
665+
666+int TouchGestureArea::maximumTouchPoints() const
667+{
668+ return m_maximumTouchPoints;
669+}
670+
671+void TouchGestureArea::setMaximumTouchPoints(int value)
672+{
673+ if (m_maximumTouchPoints != value) {
674+ m_maximumTouchPoints = value;
675+ Q_EMIT maximumTouchPointsChanged(value);
676+ }
677+}
678+
679+int TouchGestureArea::recognitionPeriod() const
680+{
681+ return m_recognitionPeriod;
682+}
683+
684+void TouchGestureArea::setRecognitionPeriod(int value)
685+{
686+ if (value != m_recognitionPeriod) {
687+ m_recognitionPeriod = value;
688+ Q_EMIT recognitionPeriodChanged(value);
689+ }
690+}
691+
692+int TouchGestureArea::releaseRejectPeriod() const
693+{
694+ return m_releaseRejectPeriod;
695+}
696+
697+void TouchGestureArea::setReleaseRejectPeriod(int value)
698+{
699+ if (value != m_releaseRejectPeriod) {
700+ m_releaseRejectPeriod = value;
701+ Q_EMIT releaseRejectPeriodChanged(value);
702+ }
703+}
704+
705+void TouchGestureArea::rejectGesture()
706+{
707+ tgaDebug("rejectGesture");
708+ ungrabTouchPoints();
709+
710+ Q_FOREACH(int touchId, m_candidateTouches) {
711+ TouchRegistry::instance()->removeCandidateOwnerForTouch(touchId, this);
712+ }
713+
714+ // Monitor the candidates
715+ Q_FOREACH(int touchId, m_candidateTouches) {
716+ TouchRegistry::instance()->addTouchWatcher(touchId, this);
717+ m_watchedTouches.insert(touchId);
718+ }
719+ m_candidateTouches.clear();
720+
721+ if (m_watchedTouches.count() == 0) {
722+ setInternalStatus(InternalStatus::WaitingForTouch);
723+ } else {
724+ setInternalStatus(InternalStatus::Rejected);
725+ }
726+}
727+
728+void TouchGestureArea::resyncCachedTouchPoints()
729+{
730+ clearTouchLists();
731+
732+ bool added = false;
733+ bool ended = false;
734+ bool moved = false;
735+ bool wantsDrag = false;
736+
737+ // list of deletes
738+ QMutableHashIterator<int, GestureTouchPoint*> removeIter(m_cachedTouchPoints);
739+ while(removeIter.hasNext()) {
740+ removeIter.next();
741+ if (!m_liveTouchPoints.contains(removeIter.key())) {
742+ m_releasedTouchPoints.append(removeIter.value());
743+ removeIter.remove();
744+ ended = true;
745+ }
746+ }
747+
748+ // list of adds/moves
749+ Q_FOREACH(GestureTouchPoint* touchPoint, m_liveTouchPoints) {
750+ if (m_cachedTouchPoints.contains(touchPoint->id())) {
751+ GestureTouchPoint* cachedPoint = m_cachedTouchPoints[touchPoint->id()];
752+
753+ if (*cachedPoint != *touchPoint) {
754+ *cachedPoint = *touchPoint;
755+ m_movedTouchPoints.append(touchPoint);
756+ moved = true;
757+ }
758+ } else {
759+ m_cachedTouchPoints.insert(touchPoint->id(), new GestureTouchPoint(*touchPoint));
760+ m_pressedTouchPoints.append(touchPoint);
761+ added = true;
762+ }
763+ }
764+
765+ if (wantsDrag && !dragging()) {
766+ setDragging(true);
767+ }
768+
769+ if (ended) {
770+ if (m_cachedTouchPoints.isEmpty()) {
771+ if (!dragging()) Q_EMIT clicked();
772+ setDragging(false);
773+ }
774+ tgaDebug("Cached Release " << touchesString(m_releasedTouchPoints));
775+ Q_EMIT released(m_releasedTouchPoints);
776+ }
777+ if (added) {
778+ tgaDebug("Cached Press " << touchesString(m_pressedTouchPoints));
779+ Q_EMIT pressed(m_pressedTouchPoints);
780+ }
781+ if (moved) {
782+ tgaDebug("Cached Update " << touchesString(m_movedTouchPoints));
783+ Q_EMIT updated(m_movedTouchPoints);
784+ }
785+ if (added || ended || moved) Q_EMIT touchPointsUpdated();
786+}
787+
788+int TouchGestureArea::touchPoint_count(QQmlListProperty<GestureTouchPoint> *list)
789+{
790+ TouchGestureArea *q = static_cast<TouchGestureArea*>(list->object);
791+ return q->m_cachedTouchPoints.count();
792+}
793+
794+GestureTouchPoint *TouchGestureArea::touchPoint_at(QQmlListProperty<GestureTouchPoint> *list, int index)
795+{
796+ TouchGestureArea *q = static_cast<TouchGestureArea*>(list->object);
797+ return (q->m_cachedTouchPoints.begin()+index).value();
798+}
799+
800+GestureTouchPoint* TouchGestureArea::addTouchPoint(QTouchEvent::TouchPoint const* tp)
801+{
802+ GestureTouchPoint* gtp = new GestureTouchPoint();
803+ gtp->setId(tp->id());
804+ gtp->setPressed(true);
805+ gtp->setPos(tp->pos());
806+ m_liveTouchPoints.insert(tp->id(), gtp);
807+ return gtp;
808+}
809+
810+void TouchGestureArea::itemChange(ItemChange change, const ItemChangeData &value)
811+{
812+ if (change == QQuickItem::ItemSceneChange) {
813+ if (value.window != nullptr) {
814+ value.window->installEventFilter(TouchRegistry::instance());
815+ }
816+ }
817+}
818+
819+void TouchGestureArea::setDragging(bool dragging)
820+{
821+ if (m_dragging == dragging)
822+ return;
823+
824+ tgaDebug("setDragging " << dragging);
825+
826+ m_dragging = dragging;
827+ Q_EMIT draggingChanged(m_dragging);
828+}
829+
830+void GestureTouchPoint::setId(int id)
831+{
832+ if (m_id == id)
833+ return;
834+ m_id = id;
835+ Q_EMIT idChanged();
836+}
837+
838+void GestureTouchPoint::setPressed(bool pressed)
839+{
840+ if (m_pressed == pressed)
841+ return;
842+ m_pressed = pressed;
843+ Q_EMIT pressedChanged();
844+}
845+
846+void GestureTouchPoint::setX(qreal x)
847+{
848+ if (m_x == x)
849+ return;
850+ m_x = x;
851+ Q_EMIT xChanged();
852+}
853+
854+void GestureTouchPoint::setY(qreal y)
855+{
856+ if (m_y == y)
857+ return;
858+ m_y = y;
859+ Q_EMIT yChanged();
860+}
861+
862+void GestureTouchPoint::setDragging(bool dragging)
863+{
864+ if (m_dragging == dragging)
865+ return;
866+
867+ m_dragging = dragging;
868+ Q_EMIT draggingChanged();
869+}
870+
871+void GestureTouchPoint::setPos(const QPointF &pos)
872+{
873+ setX(pos.x());
874+ setY(pos.y());
875+}
plugins/Ubuntu/Gestures/TouchGestureArea.hnew file+229
@@ -0,0 +1,229 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef TOUCHGESTUREAREA_H
18+#define TOUCHGESTUREAREA_H
19+
20+#include "UbuntuGesturesQmlGlobal.h"
21+
22+#include <QQuickItem>
23+
24+// lib UbuntuGestures
25+#include <Timer.h>
26+
27+class TouchOwnershipEvent;
28+class UnownedTouchEvent;
29+
30+class GestureTouchPoint : public QObject
31+{
32+ Q_OBJECT
33+ Q_PROPERTY(int id READ id NOTIFY idChanged)
34+ Q_PROPERTY(bool pressed READ pressed NOTIFY pressedChanged)
35+ Q_PROPERTY(qreal x READ x NOTIFY xChanged)
36+ Q_PROPERTY(qreal y READ y NOTIFY yChanged)
37+ Q_PROPERTY(bool dragging READ dragging NOTIFY draggingChanged)
38+public:
39+ GestureTouchPoint()
40+ : m_id(-1)
41+ , m_pressed(false)
42+ , m_x(0)
43+ , m_y(0)
44+ , m_dragging(false)
45+ {
46+ }
47+
48+ GestureTouchPoint(const GestureTouchPoint& other)
49+ : QObject(nullptr)
50+ {
51+ operator=(other);
52+ }
53+
54+ int id() const { return m_id; }
55+ void setId(int id);
56+
57+ bool pressed() const { return m_pressed; }
58+ void setPressed(bool pressed);
59+
60+ qreal x() const { return m_x; }
61+ void setX(qreal x);
62+
63+ qreal y() const { return m_y; }
64+ void setY(qreal y);
65+
66+ bool dragging() const { return m_dragging; }
67+ void setDragging(bool dragging);
68+
69+ GestureTouchPoint& operator=(const GestureTouchPoint& rhs) {
70+ if (&rhs == this) return *this;
71+ m_id = rhs.m_id;
72+ m_pressed = rhs.m_pressed;
73+ m_x = rhs.m_x;
74+ m_y = rhs.m_y;
75+ m_dragging = rhs.m_dragging;
76+ return *this;
77+ }
78+
79+ bool operator=(const GestureTouchPoint& rhs) const {
80+ if (&rhs == this) return true;
81+ return m_id == rhs.m_id &&
82+ m_pressed == rhs.m_pressed &&
83+ m_x == rhs.m_x &&
84+ m_y == rhs.m_y &&
85+ m_dragging == rhs.m_dragging;
86+ }
87+ bool operator!=(const GestureTouchPoint& rhs) const { return !operator=(rhs); }
88+
89+ void setPos(const QPointF &pos);
90+
91+Q_SIGNALS:
92+ void idChanged();
93+ void pressedChanged();
94+ void xChanged();
95+ void yChanged();
96+ void draggingChanged();
97+
98+private:
99+ int m_id;
100+ bool m_pressed;
101+ qreal m_x;
102+ qreal m_y;
103+ bool m_dragging;
104+};
105+
106+/*
107+ An area that detects multi-finger gestures.
108+
109+ We can use this to detect gestures contstrained by a minimim and/or maximum number of touch points.
110+ This components uses the touch registry to apply for ownership of touch points.
111+ This way we can use the component in conjuntion with the directional drag area to compete for ownwership
112+ or gestures; unlike the MultiPointTouchArea.
113+ */
114+class UBUNTUGESTURESQML_EXPORT TouchGestureArea : public QQuickItem
115+{
116+ Q_OBJECT
117+ Q_ENUMS(Status)
118+
119+ Q_PROPERTY(int status READ status NOTIFY statusChanged)
120+ Q_PROPERTY(bool dragging READ dragging NOTIFY draggingChanged)
121+ Q_PROPERTY(QQmlListProperty<GestureTouchPoint> touchPoints READ touchPoints NOTIFY touchPointsUpdated)
122+
123+ Q_PROPERTY(int minimumTouchPoints READ minimumTouchPoints WRITE setMinimumTouchPoints NOTIFY minimumTouchPointsChanged)
124+ Q_PROPERTY(int maximumTouchPoints READ maximumTouchPoints WRITE setMaximumTouchPoints NOTIFY maximumTouchPointsChanged)
125+
126+ // Time(ms) the component will wait for after receiving an initial touch to recognise a gesutre before rejecting it.
127+ Q_PROPERTY(int recognitionPeriod READ recognitionPeriod WRITE setRecognitionPeriod NOTIFY recognitionPeriodChanged)
128+ // Time(ms) the component will allow a recognised gesture to intermitently release a touch point before rejecting the gesture.
129+ // This is so we will not immediately reject a gesture if there are fleeting touch point releases while dragging.
130+ Q_PROPERTY(int releaseRejectPeriod READ releaseRejectPeriod WRITE setReleaseRejectPeriod NOTIFY releaseRejectPeriodChanged)
131+
132+public:
133+ // Describes the state of the touch gesture area.
134+ enum Status {
135+ WaitingForTouch,
136+ Undecided,
137+ Recognized,
138+ Rejected
139+ };
140+ TouchGestureArea(QQuickItem* parent = NULL);
141+ ~TouchGestureArea();
142+
143+ bool event(QEvent *e) override;
144+
145+ void setRecognitionTimer(UbuntuGestures::AbstractTimer *timer);
146+
147+ int status() const;
148+ bool dragging() const;
149+ QQmlListProperty<GestureTouchPoint> touchPoints();
150+
151+ int minimumTouchPoints() const;
152+ void setMinimumTouchPoints(int value);
153+
154+ int maximumTouchPoints() const;
155+ void setMaximumTouchPoints(int value);
156+
157+ int recognitionPeriod() const;
158+ void setRecognitionPeriod(int value);
159+
160+ int releaseRejectPeriod() const;
161+ void setReleaseRejectPeriod(int value);
162+
163+Q_SIGNALS:
164+ void statusChanged(int status);
165+
166+ void touchPointsUpdated();
167+ void draggingChanged(bool dragging);
168+ void minimumTouchPointsChanged(bool value);
169+ void maximumTouchPointsChanged(bool value);
170+ void recognitionPeriodChanged(bool value);
171+ void releaseRejectPeriodChanged(bool value);
172+
173+ void pressed(const QList<QObject*>& points);
174+ void released(const QList<QObject*>& points);
175+ void updated(const QList<QObject*>& points);
176+ void clicked();
177+
178+protected:
179+ void itemChange(ItemChange change, const ItemChangeData &value);
180+
181+private Q_SLOTS:
182+ void rejectGesture();
183+
184+private:
185+ void touchEvent(QTouchEvent *event) override;
186+ void touchEvent_waitingForTouch(QTouchEvent *event);
187+ void touchEvent_waitingForMoreTouches(QTouchEvent *event);
188+ void touchEvent_waitingForOwnership(QTouchEvent *event);
189+ void touchEvent_recognized(QTouchEvent *event);
190+ void touchEvent_rejected(QTouchEvent *event);
191+
192+ void unownedTouchEvent(QTouchEvent *unownedTouchEvent);
193+ void unownedTouchEvent_waitingForMoreTouches(QTouchEvent *unownedTouchEvent);
194+ void unownedTouchEvent_waitingForOwnership(QTouchEvent *unownedTouchEvent);
195+ void unownedTouchEvent_recognised(QTouchEvent *unownedTouchEvent);
196+ void unownedTouchEvent_rejected(QTouchEvent *unownedTouchEvent);
197+
198+ void touchOwnershipEvent(TouchOwnershipEvent *event);
199+ void updateTouchPoints(QTouchEvent *event);
200+
201+ GestureTouchPoint* addTouchPoint(const QTouchEvent::TouchPoint *tp);
202+ void clearTouchLists();
203+ void setDragging(bool dragging);
204+ void setInternalStatus(uint status);
205+ void resyncCachedTouchPoints();
206+
207+ static int touchPoint_count(QQmlListProperty<GestureTouchPoint> *list);
208+ static GestureTouchPoint* touchPoint_at(QQmlListProperty<GestureTouchPoint> *list, int index);
209+
210+ uint m_status;
211+ QSet<int> m_candidateTouches;
212+ QSet<int> m_watchedTouches;
213+ UbuntuGestures::AbstractTimer *m_recognitionTimer;
214+
215+ bool m_dragging;
216+ QHash<int, GestureTouchPoint*> m_liveTouchPoints;
217+ QHash<int, GestureTouchPoint*> m_cachedTouchPoints;
218+ QList<QObject*> m_releasedTouchPoints;
219+ QList<QObject*> m_pressedTouchPoints;
220+ QList<QObject*> m_movedTouchPoints;
221+ int m_minimumTouchPoints;
222+ int m_maximumTouchPoints;
223+ int m_recognitionPeriod;
224+ int m_releaseRejectPeriod;
225+};
226+
227+QML_DECLARE_TYPE(GestureTouchPoint)
228+
229+#endif // TOUCHGESTUREAREA_H
plugins/Ubuntu/Gestures/plugin.cpp+3
@@ -21,6 +21,7 @@
2121#include "FloatingFlickable.h"
2222#include "PressedOutsideNotifier.h"
2323#include "TouchGate.h"
24+#include "TouchGestureArea.h"
2425
2526#include <qqml.h>
2627
@@ -38,4 +39,6 @@ void UbuntuGesturesQmlPlugin::registerTypes(const char *uri)
3839 qmlRegisterType<FloatingFlickable>(uri, 0, 1, "FloatingFlickable");
3940 qmlRegisterType<PressedOutsideNotifier>(uri, 0, 1, "PressedOutsideNotifier");
4041 qmlRegisterType<TouchGate>(uri, 0, 1, "TouchGate");
42+ qmlRegisterType<TouchGestureArea>(uri, 0, 1, "TouchGestureArea");
43+ qmlRegisterUncreatableType<GestureTouchPoint>(uri, 0, 1, "GestureTouchPoint", "Cannot create GestureTouchPoints");
4144}
plugins/Unity/Indicators/indicatorsmanager.cpp+3−5
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013 Canonical, Ltd.
2+ * Copyright (C) 2013-2016 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
@@ -12,8 +12,6 @@
1212 *
1313 * You should have received a copy of the GNU General Public License
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− *
16− * Author: Nick Dedekind <nick.dedekind@canonical.com>
1715 */
1816
1917#include "indicatorsmanager.h"
@@ -286,12 +284,12 @@ Indicator::Ptr IndicatorsManager::indicator(const QString& indicator_name)
286284 new_indicator->init(data->m_fileInfo.fileName(), settings);
287285
288286 // convergence:
289− // 1) enable session indicator conditionally, typically when running in a multisession/multiuser environment
287+ // 1) enable session indicator
290288 // 2) on a PC, switch the battery/power indicator to desktop mode,
291289 // can't control brightness for now and phone-on-desktop broken (FIXME)
292290 //
293291 // The rest of the indicators respect their default profile (which is "phone", even on desktop PCs)
294− if ((new_indicator->identifier() == QStringLiteral("indicator-session") && m_platform.isMultiSession())
292+ if ((new_indicator->identifier() == QStringLiteral("indicator-session"))
295293 || (new_indicator->identifier() == QStringLiteral("indicator-power") && m_platform.isPC())) {
296294 new_indicator->setProfile(QStringLiteral("desktop"));
297295 } else {
plugins/Unity/InputInfo/InputInfo.qmltypesnew file+71
@@ -0,0 +1,71 @@
1+import QtQuick.tooling 1.1
2+
3+// This file describes the plugin-supplied types contained in the library.
4+// It is used for QML tooling purposes only.
5+//
6+// This file was auto-generated by:
7+// 'qmlplugindump -notrelocatable Unity.InputInfo 0.1 plugins'
8+
9+Module {
10+ Component {
11+ name: "QDeclarativeInputDeviceModel"
12+ prototype: "QAbstractListModel"
13+ exports: ["Unity.InputInfo/InputDeviceModel 0.1"]
14+ exportMetaObjectRevisions: [0]
15+ Property { name: "deviceFilter"; type: "QInputDevice::InputType" }
16+ Property { name: "count"; type: "int"; isReadonly: true }
17+ Signal {
18+ name: "deviceAdded"
19+ Parameter { name: "devicePath"; type: "string" }
20+ }
21+ Signal {
22+ name: "deviceRemoved"
23+ Parameter { name: "devicePath"; type: "string" }
24+ }
25+ Signal {
26+ name: "deviceFilterChanged"
27+ Parameter { name: "filter"; type: "QInputDevice::InputType" }
28+ }
29+ Method { name: "updateDeviceList" }
30+ Method {
31+ name: "indexOf"
32+ type: "int"
33+ Parameter { name: "devicePath"; type: "string" }
34+ }
35+ Method {
36+ name: "get"
37+ type: "QInputDevice*"
38+ Parameter { name: "index"; type: "int" }
39+ }
40+ }
41+ Component {
42+ name: "QInputDevice"
43+ prototype: "QObject"
44+ exports: ["Unity.InputInfo/InputInfo 0.1"]
45+ exportMetaObjectRevisions: [0]
46+ Enum {
47+ name: "InputType"
48+ values: {
49+ "Unknown": 0,
50+ "Button": 1,
51+ "Mouse": 2,
52+ "TouchPad": 4,
53+ "TouchScreen": 8,
54+ "Keyboard": 16,
55+ "Switch": 32
56+ }
57+ }
58+ Enum {
59+ name: "InputTypeFlags"
60+ values: {
61+ "Unknown": 0,
62+ "Button": 1,
63+ "Mouse": 2,
64+ "TouchPad": 4,
65+ "TouchScreen": 8,
66+ "Keyboard": 16,
67+ "Switch": 32
68+ }
69+ }
70+ }
71+}
plugins/Unity/Platform/Platform.qmltypesnew file+20
@@ -0,0 +1,20 @@
1+import QtQuick.tooling 1.1
2+
3+// This file describes the plugin-supplied types contained in the library.
4+// It is used for QML tooling purposes only.
5+//
6+// This file was auto-generated by:
7+// 'qmlplugindump -notrelocatable Unity.Platform 1.0 plugins'
8+
9+Module {
10+ Component {
11+ name: "Platform"
12+ prototype: "QObject"
13+ exports: ["Unity.Platform/Platform 1.0"]
14+ isCreatable: false
15+ isSingleton: true
16+ exportMetaObjectRevisions: [0]
17+ Property { name: "chassis"; type: "string"; isReadonly: true }
18+ Property { name: "isPC"; type: "bool"; isReadonly: true }
19+ }
20+}
plugins/Unity/Session/dbusunitysessionservice.cpp+4−1
@@ -336,6 +336,9 @@ void DBusUnitySessionService::PromptLock()
336336
337337void DBusUnitySessionService::Lock()
338338{
339+ // signal u8 to show the lockscreen/greeter
340+ PromptLock();
341+
339342 // lock the session using the org.freedesktop.DisplayManager system DBUS service
340343 const QString sessionPath = QString::fromLocal8Bit(qgetenv("XDG_SESSION_PATH"));
341344 QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.DisplayManager"),
@@ -348,7 +351,7 @@ void DBusUnitySessionService::Lock()
348351 connect(watcher, &QDBusPendingCallWatcher::finished,
349352 this, [this](QDBusPendingCallWatcher* watcher) {
350353
351− QDBusPendingReply<QVariant> reply = *watcher;
354+ QDBusPendingReply<void> reply = *watcher;
352355 watcher->deleteLater();
353356 if (reply.isError()) {
354357 qWarning() << "Lock call failed" << reply.error().message();
plugins/Utils/CMakeLists.txt+2−1
@@ -17,13 +17,14 @@ set(QMLPLUGIN_SRC
1717 unitysortfilterproxymodelqml.cpp
1818 Timer.cpp
1919 unitymenumodelpaths.cpp
20− windowkeysfilter.cpp
20+ windowinputfilter.cpp
2121 windowscreenshotprovider.cpp
2222 easingcurve.cpp
2323 windowstatestorage.cpp
2424 timezoneFormatter.cpp
2525 inputeventgenerator.cpp
2626 deviceconfigparser.cpp
27+ globalfunctions.cpp
2728 plugin.cpp
2829 )
2930
plugins/Utils/Utils.qmltypes+2−2
@@ -147,10 +147,10 @@ Module {
147147 }
148148 }
149149 Component {
150− name: "WindowKeysFilter"
150+ name: "WindowInputFilter"
151151 defaultProperty: "data"
152152 prototype: "QQuickItem"
153− exports: ["Utils/WindowKeysFilter 0.1"]
153+ exports: ["Utils/WindowInputFilter 0.1"]
154154 exportMetaObjectRevisions: [0]
155155 }
156156 Component {
plugins/Utils/globalfunctions.cppnew file+56
@@ -0,0 +1,56 @@
1+/*
2+ * Copyright 2015 Canonical Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU Lesser General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+*/
16+
17+#include "globalfunctions.h"
18+
19+#pragma GCC diagnostic push
20+#pragma GCC diagnostic ignored "-pedantic"
21+#include <private/qquickitem_p.h>
22+#pragma GCC diagnostic pop
23+#include <QQmlEngine>
24+
25+GlobalFunctions::GlobalFunctions(QObject *parent)
26+ : QObject(parent)
27+{
28+}
29+
30+QQuickItem *GlobalFunctions::itemAt(QQuickItem* parent, int x, int y, QJSValue matcher)
31+{
32+ if (!parent) return nullptr;
33+ QList<QQuickItem *> children = QQuickItemPrivate::get(parent)->paintOrderChildItems();
34+
35+ for (int i = children.count() - 1; i >= 0; --i) {
36+ QQuickItem *child = children.at(i);
37+
38+ // Map coordinates to the child element's coordinate space
39+ QPointF point = parent->mapToItem(child, QPointF(x, y));
40+ if (child->isVisible() && point.x() >= 0
41+ && child->width() >= point.x()
42+ && point.y() >= 0
43+ && child->height() >= point.y()) {
44+ if (!matcher.isCallable()) return child;
45+
46+ QQmlEngine* engine = qmlEngine(child);
47+ if (!engine) return child;
48+
49+ QJSValue newObj = engine->newQObject(child);
50+ if (matcher.call(QJSValueList() << newObj).toBool()) {
51+ return child;
52+ }
53+ }
54+ }
55+ return nullptr;
56+}
plugins/Utils/globalfunctions.hnew file+42
@@ -0,0 +1,42 @@
1+/*
2+ * Copyright 2015 Canonical Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU Lesser General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+*/
16+
17+#ifndef GLOBALFUNCTIONS_H
18+#define GLOBALFUNCTIONS_H
19+
20+#include <QObject>
21+#include <QJSValue>
22+class QQuickItem;
23+
24+/**
25+ * @brief The GlobalFunctions class
26+ *
27+ * This singleton class exposes utility functions to QML
28+ *
29+ */
30+class GlobalFunctions : public QObject
31+{
32+ Q_OBJECT
33+public:
34+ explicit GlobalFunctions(QObject *parent = 0);
35+
36+ static Q_INVOKABLE QQuickItem* itemAt(QQuickItem* parent,
37+ int x,
38+ int y,
39+ QJSValue matcher);
40+};
41+
42+#endif // GLOBALFUNCTIONS_H
plugins/Utils/plugin.cpp+11−2
@@ -30,7 +30,7 @@
3030#include "qlimitproxymodelqml.h"
3131#include "unitysortfilterproxymodelqml.h"
3232#include "unitymenumodelpaths.h"
33−#include "windowkeysfilter.h"
33+#include "windowinputfilter.h"
3434#include "windowscreenshotprovider.h"
3535#include "windowstatestorage.h"
3636#include "constants.h"
@@ -38,6 +38,7 @@
3838#include "applicationsfiltermodel.h"
3939#include "inputeventgenerator.h"
4040#include "deviceconfigparser.h"
41+#include "globalfunctions.h"
4142
4243static QObject *createWindowStateStorage(QQmlEngine *engine, QJSEngine *scriptEngine)
4344{
@@ -53,6 +54,13 @@ static QObject *createConstants(QQmlEngine *engine, QJSEngine *scriptEngine)
5354 return new Constants();
5455}
5556
57+static QObject *createGlobalFunctions(QQmlEngine *engine, QJSEngine *scriptEngine)
58+{
59+ Q_UNUSED(engine)
60+ Q_UNUSED(scriptEngine)
61+ return new GlobalFunctions();
62+}
63+
5664void UtilsPlugin::registerTypes(const char *uri)
5765{
5866 Q_ASSERT(uri == QLatin1String("Utils"));
@@ -61,7 +69,7 @@ void UtilsPlugin::registerTypes(const char *uri)
6169 qmlRegisterType<QLimitProxyModelQML>(uri, 0, 1, "LimitProxyModel");
6270 qmlRegisterType<UnitySortFilterProxyModelQML>(uri, 0, 1, "UnitySortFilterProxyModel");
6371 qmlRegisterType<UnityMenuModelPaths>(uri, 0, 1, "UnityMenuModelPaths");
64− qmlRegisterType<WindowKeysFilter>(uri, 0, 1, "WindowKeysFilter");
72+ qmlRegisterType<WindowInputFilter>(uri, 0, 1, "WindowInputFilter");
6573 qmlRegisterType<EasingCurve>(uri, 0, 1, "EasingCurve");
6674 qmlRegisterSingletonType<WindowStateStorage>(uri, 0, 1, "WindowStateStorage", createWindowStateStorage);
6775 qmlRegisterType<InputWatcher>(uri, 0, 1, "InputWatcher");
@@ -72,6 +80,7 @@ void UtilsPlugin::registerTypes(const char *uri)
7280 qmlRegisterType<ApplicationsFilterModel>(uri, 0, 1, "ApplicationsFilterModel");
7381 qmlRegisterType<InputEventGenerator>(uri, 0, 1, "InputEventGenerator");
7482 qmlRegisterType<DeviceConfigParser>(uri, 0, 1, "DeviceConfigParser");
83+ qmlRegisterSingletonType<GlobalFunctions>(uri, 0, 1, "Functions", createGlobalFunctions);
7584}
7685
7786void UtilsPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
plugins/Utils/timezoneFormatter.cpp+10
@@ -33,3 +33,13 @@ QString TimezoneFormatter::currentTimeInTimezone(const QVariant &tzId) const
3333 }
3434 return QString();
3535}
36+
37+QString TimezoneFormatter::currentTimeInTimezoneWithAbbrev(const QVariant &tzId) const
38+{
39+ QTimeZone tz(tzId.toByteArray());
40+ if (tz.isValid()) {
41+ const QDateTime now = QDateTime::currentDateTime().toTimeZone(tz);
42+ return QStringLiteral("%1 %2").arg(now.time().toString(QStringLiteral("h:mm"))).arg(tz.abbreviation(now));
43+ }
44+ return QString();
45+}
plugins/Utils/timezoneFormatter.h+1
@@ -27,6 +27,7 @@ public:
2727 ~TimezoneFormatter() = default;
2828
2929 Q_INVOKABLE QString currentTimeInTimezone(const QVariant &tzId) const;
30+ Q_INVOKABLE QString currentTimeInTimezoneWithAbbrev(const QVariant &tzId) const;
3031};
3132
3233#endif
plugins/Utils/windowkeysfilter.cpp → plugins/Utils/windowinputfilter.cpprenamed+14−16
@@ -16,36 +16,34 @@
1616 * Author: Daniel d'Andrada <daniel.dandrada@canonical.com>
1717 */
1818
19−#include "windowkeysfilter.h"
19+#include "windowinputfilter.h"
2020
2121#include <QQuickWindow>
2222
23−WindowKeysFilter::WindowKeysFilter(QQuickItem *parent)
23+WindowInputFilter::WindowInputFilter(QQuickItem *parent)
2424 : QQuickItem(parent),
25− m_currentEventTimestamp(0)
25+ m_lastInputTimestamp(0)
2626{
2727 connect(this, &QQuickItem::windowChanged,
28− this, &WindowKeysFilter::setupFilterOnWindow);
28+ this, &WindowInputFilter::setupFilterOnWindow);
2929}
3030
31−bool WindowKeysFilter::eventFilter(QObject *watched, QEvent *event)
31+bool WindowInputFilter::eventFilter(QObject *watched, QEvent *event)
3232{
3333 Q_ASSERT(!m_filteredWindow.isNull());
3434 Q_ASSERT(watched == static_cast<QObject*>(m_filteredWindow.data()));
3535 Q_UNUSED(watched);
3636
37+ QInputEvent *inputEvent = dynamic_cast<QInputEvent*>(event);
38+ if (inputEvent) {
39+ m_lastInputTimestamp = inputEvent->timestamp();
40+ Q_EMIT lastInputTimestampChanged();
41+ }
42+
3743 if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) {
3844 // Let QML see this event and decide if it does not want it
3945 event->accept();
40−
41− m_currentEventTimestamp = static_cast<QInputEvent*>(event)->timestamp();
42− Q_EMIT currentEventTimestampChanged();
43−
4446 QCoreApplication::sendEvent(this, event);
45−
46− m_currentEventTimestamp = 0;
47− Q_EMIT currentEventTimestampChanged();
48−
4947 return event->isAccepted();
5048 } else {
5149 // Not interested
@@ -53,7 +51,7 @@ bool WindowKeysFilter::eventFilter(QObject *watched, QEvent *event)
5351 }
5452}
5553
56−void WindowKeysFilter::setupFilterOnWindow(QQuickWindow *window)
54+void WindowInputFilter::setupFilterOnWindow(QQuickWindow *window)
5755{
5856 if (!m_filteredWindow.isNull()) {
5957 m_filteredWindow->removeEventFilter(this);
@@ -66,7 +64,7 @@ void WindowKeysFilter::setupFilterOnWindow(QQuickWindow *window)
6664 }
6765}
6866
69−ulong WindowKeysFilter::currentEventTimestamp() const
67+ulong WindowInputFilter::lastInputTimestamp() const
7068{
71− return m_currentEventTimestamp;
69+ return m_lastInputTimestamp;
7270}
plugins/Utils/windowkeysfilter.h → plugins/Utils/windowinputfilter.hrenamed+11−11
@@ -16,8 +16,8 @@
1616 * Author: Daniel d'Andrada <daniel.dandrada@canonical.com>
1717 */
1818
19−#ifndef UNITY_WINDOWKEYSFILTER_H
20−#define UNITY_WINDOWKEYSFILTER_H
19+#ifndef UNITY_WINDOWINPUTFILTER_H
20+#define UNITY_WINDOWINPUTFILTER_H
2121
2222#include <QQuickItem>
2323#include <QPointer>
@@ -29,30 +29,30 @@
2929 accepted ones will be filtered out. Events are accepted by default, so make sure you reject
3030 the keys you're not interested in.
3131
32− If more than one WindowKeysFilter exist in the same QML scene (and thus in the same QQuickWindow)
32+ If more than one WindowInputFilter exist in the same QML scene (and thus in the same QQuickWindow)
3333 they will be called in the order of creation, which can be tricky to assess. So the best practice
34− is to have at most one WindowKeysFilter per QML scene.
34+ is to have at most one WindowInputFilter per QML scene.
3535 */
36−class WindowKeysFilter : public QQuickItem
36+class WindowInputFilter : public QQuickItem
3737{
3838 Q_OBJECT
39− Q_PROPERTY(ulong currentEventTimestamp READ currentEventTimestamp NOTIFY currentEventTimestampChanged)
39+ Q_PROPERTY(ulong lastInputTimestamp READ lastInputTimestamp NOTIFY lastInputTimestampChanged)
4040public:
41− WindowKeysFilter(QQuickItem *parent = 0);
41+ WindowInputFilter(QQuickItem *parent = 0);
4242
4343 bool eventFilter(QObject *watched, QEvent *event) override;
4444
45− ulong currentEventTimestamp() const;
45+ ulong lastInputTimestamp() const;
4646
4747Q_SIGNALS:
48− void currentEventTimestampChanged();
48+ void lastInputTimestampChanged();
4949
5050private Q_SLOTS:
5151 void setupFilterOnWindow(QQuickWindow *window);
5252
5353private:
5454 QPointer<QQuickWindow> m_filteredWindow;
55− ulong m_currentEventTimestamp;
55+ ulong m_lastInputTimestamp;
5656};
5757
58−#endif // UNITY_WINDOWKEYSFILTER_H
58+#endif // UNITY_WINDOWINPUTFILTER_H
plugins/Utils/windowstatestorage.cpp+36−4
@@ -23,9 +23,14 @@
2323#include <QSqlError>
2424#include <QSqlResult>
2525#include <QRect>
26+#include <unity/shell/application/ApplicationInfoInterface.h>
2627
2728QMutex WindowStateStorage::s_mutex;
2829
30+inline QString sanitiseString(QString string) {
31+ return string.remove("\"").remove("'").remove("\\");
32+}
33+
2934WindowStateStorage::WindowStateStorage(QObject *parent):
3035 QObject(parent)
3136{
@@ -50,7 +55,7 @@ WindowStateStorage::~WindowStateStorage()
5055void WindowStateStorage::saveState(const QString &windowId, WindowStateStorage::WindowState state)
5156{
5257 const QString queryString = QStringLiteral("INSERT OR REPLACE INTO state (windowId, state) values ('%1', '%2');")
53− .arg(windowId)
58+ .arg(sanitiseString(windowId))
5459 .arg((int)state);
5560
5661 saveValue(queryString);
@@ -59,7 +64,7 @@ void WindowStateStorage::saveState(const QString &windowId, WindowStateStorage::
5964WindowStateStorage::WindowState WindowStateStorage::getState(const QString &windowId, WindowStateStorage::WindowState defaultValue) const
6065{
6166 const QString queryString = QStringLiteral("SELECT * FROM state WHERE windowId = '%1';")
62− .arg(windowId);
67+ .arg(sanitiseString(windowId));
6368
6469 QSqlQuery query = getValue(queryString);
6570
@@ -72,7 +77,7 @@ WindowStateStorage::WindowState WindowStateStorage::getState(const QString &wind
7277void WindowStateStorage::saveGeometry(const QString &windowId, const QRect rect)
7378{
7479 const QString queryString = QStringLiteral("INSERT OR REPLACE INTO geometry (windowId, x, y, width, height) values ('%1', '%2', '%3', '%4', '%5');")
75− .arg(windowId)
80+ .arg(sanitiseString(windowId))
7681 .arg(rect.x())
7782 .arg(rect.y())
7883 .arg(rect.width())
@@ -81,6 +86,28 @@ void WindowStateStorage::saveGeometry(const QString &windowId, const QRect rect)
8186 saveValue(queryString);
8287}
8388
89+void WindowStateStorage::saveStage(const QString &appId, int stage)
90+{
91+ const QString queryString = QStringLiteral("INSERT OR REPLACE INTO stage (appId, stage) values ('%1', '%2');")
92+ .arg(sanitiseString(appId))
93+ .arg((int)stage);
94+
95+ saveValue(queryString);
96+}
97+
98+int WindowStateStorage::getStage(const QString &appId) const
99+{
100+ const QString queryString = QStringLiteral("SELECT * FROM stage WHERE appId = '%1';")
101+ .arg(sanitiseString(appId));
102+
103+ QSqlQuery query = getValue(queryString);
104+
105+ if (!query.first()) {
106+ return unity::shell::application::ApplicationInfoInterface::MainStage;
107+ }
108+ return query.value("stage").toInt();
109+}
110+
84111void WindowStateStorage::executeAsyncQuery(const QString &queryString)
85112{
86113 QMutexLocker l(&s_mutex);
@@ -97,7 +124,7 @@ void WindowStateStorage::executeAsyncQuery(const QString &queryString)
97124QRect WindowStateStorage::getGeometry(const QString &windowId, const QRect defaultValue) const
98125{
99126 QString queryString = QStringLiteral("SELECT * FROM geometry WHERE windowId = '%1';")
100− .arg(windowId);
127+ .arg(sanitiseString(windowId));
101128
102129 QSqlQuery query = getValue(queryString);
103130
@@ -124,6 +151,11 @@ void WindowStateStorage::initdb()
124151 QSqlQuery query;
125152 query.exec(QStringLiteral("CREATE TABLE state(windowId TEXT UNIQUE, state INTEGER);"));
126153 }
154+
155+ if (!m_db.tables().contains(QStringLiteral("stage"))) {
156+ QSqlQuery query;
157+ query.exec(QStringLiteral("CREATE TABLE stage(appId TEXT UNIQUE, stage INTEGER);"));
158+ }
127159}
128160
129161void WindowStateStorage::saveValue(const QString &queryString)
plugins/Utils/windowstatestorage.h+3
@@ -38,6 +38,9 @@ public:
3838 Q_INVOKABLE void saveGeometry(const QString &windowId, const QRect rect);
3939 Q_INVOKABLE QRect getGeometry(const QString &windowId, const QRect defaultValue) const;
4040
41+ Q_INVOKABLE void saveStage(const QString &appId, int stage);
42+ Q_INVOKABLE int getStage(const QString &appId) const;
43+
4144private:
4245 void initdb();
4346
plugins/Wizard/CMakeLists.txt+7−1
@@ -1,10 +1,16 @@
1+include_directories(${GLIB_INCLUDE_DIRS} ${GEONAMES_INCLUDE_DIRS})
2+
13add_library(Wizard-qml MODULE
24 plugin.cpp
35 PageList.cpp
46 System.cpp
7+ LocalePlugin.cpp
8+ timezonemodel.cpp
9+ Status.cpp
510)
611
7−qt5_use_modules(Wizard-qml DBus Qml)
12+qt5_use_modules(Wizard-qml DBus Qml Concurrent)
13+target_link_libraries(Wizard-qml ${GLIB_LDFLAGS} ${GEONAMES_LDFLAGS})
814add_unity8_plugin(Wizard 0.1 Wizard TARGETS Wizard-qml)
915
1016set(POLKIT_LIB_DIR "${CMAKE_INSTALL_LOCALSTATEDIR}/lib/polkit-1")
plugins/Wizard/LocalePlugin.cppnew file+291
@@ -0,0 +1,291 @@
1+/*
2+ * Copyright (C) 2015 Canonical Ltd.
3+ *
4+ * This program is free software: you can redistribute it and/or modify it
5+ * under the terms of the GNU General Public License version 3, as published
6+ * by the Free Software Foundation.
7+ *
8+ * This program is distributed in the hope that it will be useful, but
9+ * WITHOUT ANY WARRANTY; without even the implied warranties of
10+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11+ * PURPOSE. See the GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License along
14+ * with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include <QLocale>
18+#include <QStringList>
19+
20+#include <libintl.h>
21+
22+#include "LocalePlugin.h"
23+
24+class LocalePrivate {
25+public:
26+ LocalePrivate() {
27+ m_mccCodes = QHash<int, QString>
28+ ({{202,"gr"},
29+ {204,"nl"},
30+ {206,"be"},
31+ {208,"fr"},
32+ {212,"mc"},
33+ {213,"ad"},
34+ {214,"es"},
35+ {216,"hu"},
36+ {218,"ba"},
37+ {219,"hr"},
38+ {220,"rs"},
39+ {222,"it"},
40+ {226,"ro"},
41+ {228,"ch"},
42+ {230,"cz"},
43+ {231,"sk"},
44+ {232,"at"},
45+ {234,"gb"},
46+ {235,"gb"},
47+ {238,"dk"},
48+ {240,"se"},
49+ {242,"no"},
50+ {244,"fi"},
51+ {246,"lt"},
52+ {247,"lv"},
53+ {248,"ee"},
54+ {250,"ru"},
55+ {255,"ua"},
56+ {257,"by"},
57+ {259,"md"},
58+ {260,"pl"},
59+ {262,"de"},
60+ {266,"gi"},
61+ {268,"pt"},
62+ {270,"lu"},
63+ {272,"ie"},
64+ {274,"is"},
65+ {276,"al"},
66+ {278,"mt"},
67+ {280,"cy"},
68+ {282,"ge"},
69+ {283,"am"},
70+ {284,"bg"},
71+ {286,"tr"},
72+ {288,"fo"},
73+ {289,"ge"},
74+ {290,"gl"},
75+ {292,"sm"},
76+ {293,"si"},
77+ {294,"mk"},
78+ {295,"li"},
79+ {297,"me"},
80+ {302,"ca"},
81+ {308,"pm"},
82+ {310,"gu"},
83+ {310,"us"},
84+ {311,"gu"},
85+ {311,"us"},
86+ {312,"us"},
87+ {316,"us"},
88+ {330,"pr"},
89+ {334,"mx"},
90+ {338,"jm"},
91+ {340,"fg"},
92+ {340,"gp"},
93+ {340,"mq"},
94+ {342,"bb"},
95+ {344,"ag"},
96+ {346,"ky"},
97+ {348,"vg"},
98+ {350,"bm"},
99+ {352,"gd"},
100+ {354,"ms"},
101+ {356,"kn"},
102+ {358,"lc"},
103+ {360,"vc"},
104+ {362,"an"},
105+ {362,"cw"},
106+ {363,"aw"},
107+ {364,"bs"},
108+ {365,"ai"},
109+ {366,"dm"},
110+ {368,"cu"},
111+ {370,"do"},
112+ {372,"ht"},
113+ {374,"tt"},
114+ {376,"tc"},
115+ {376,"vi"},
116+ {400,"az"},
117+ {401,"kz"},
118+ {402,"bt"},
119+ {404,"in"},
120+ {405,"in"},
121+ {410,"pk"},
122+ {412,"af"},
123+ {413,"lk"},
124+ {414,"mm"},
125+ {415,"lb"},
126+ {416,"jo"},
127+ {417,"sy"},
128+ {418,"iq"},
129+ {419,"kw"},
130+ {420,"sa"},
131+ {421,"ye"},
132+ {422,"om"},
133+ {424,"ae"},
134+ {425,"il"},
135+ {425,"ps"},
136+ {426,"bh"},
137+ {427,"qa"},
138+ {428,"mn"},
139+ {429,"np"},
140+ {430,"ae"},
141+ {431,"ae"},
142+ {432,"ir"},
143+ {434,"uz"},
144+ {436,"tk"},
145+ {437,"kg"},
146+ {438,"tm"},
147+ {440,"jp"},
148+ {441,"jp"},
149+ {450,"kr"},
150+ {452,"vn"},
151+ {454,"hk"},
152+ {455,"mo"},
153+ {456,"kh"},
154+ {457,"la"},
155+ {460,"cn"},
156+ {466,"tw"},
157+ {467,"kp"},
158+ {470,"bd"},
159+ {472,"mv"},
160+ {502,"my"},
161+ {505,"au"},
162+ {510,"id"},
163+ {514,"tp"},
164+ {515,"ph"},
165+ {520,"th"},
166+ {525,"sg"},
167+ {528,"bn"},
168+ {530,"nz"},
169+ {537,"pg"},
170+ {539,"to"},
171+ {540,"sb"},
172+ {541,"vu"},
173+ {542,"fj"},
174+ {544,"as"},
175+ {545,"ki"},
176+ {546,"nc"},
177+ {547,"pf"},
178+ {548,"ck"},
179+ {549,"ws"},
180+ {550,"fm"},
181+ {552,"pw"},
182+ {553,"tv"},
183+ {555,"nu"},
184+ {602,"eg"},
185+ {603,"dz"},
186+ {604,"ma"},
187+ {605,"tn"},
188+ {606,"ly"},
189+ {607,"gm"},
190+ {608,"sn"},
191+ {609,"mr"},
192+ {610,"ml"},
193+ {611,"gn"},
194+ {612,"ci"},
195+ {613,"bf"},
196+ {614,"ne"},
197+ {615,"tg"},
198+ {616,"bj"},
199+ {617,"mu"},
200+ {618,"lr"},
201+ {619,"sl"},
202+ {620,"gh"},
203+ {621,"ng"},
204+ {622,"td"},
205+ {623,"cf"},
206+ {624,"cm"},
207+ {625,"cv"},
208+ {626,"st"},
209+ {627,"gq"},
210+ {628,"ga"},
211+ {629,"cg"},
212+ {630,"cd"},
213+ {631,"ao"},
214+ {632,"gw"},
215+ {633,"sc"},
216+ {634,"sd"},
217+ {635,"rw"},
218+ {636,"et"},
219+ {637,"so"},
220+ {638,"dj"},
221+ {639,"ke"},
222+ {640,"tz"},
223+ {641,"ug"},
224+ {642,"bi"},
225+ {643,"mz"},
226+ {645,"zm"},
227+ {646,"mg"},
228+ {647,"re"},
229+ {648,"zw"},
230+ {649,"na"},
231+ {650,"mw"},
232+ {651,"ls"},
233+ {652,"bw"},
234+ {653,"sz"},
235+ {654,"km"},
236+ {655,"za"},
237+ {657,"er"},
238+ {659,"ss"},
239+ {702,"bz"},
240+ {704,"gt"},
241+ {706,"sv"},
242+ {708,"hn"},
243+ {710,"ni"},
244+ {712,"cr"},
245+ {714,"pa"},
246+ {716,"pe"},
247+ {722,"ar"},
248+ {724,"br"},
249+ {730,"cl"},
250+ {732,"co"},
251+ {734,"ve"},
252+ {736,"bo"},
253+ {738,"gy"},
254+ {740,"ec"},
255+ {744,"py"},
256+ {746,"sr"},
257+ {748,"uy"},
258+ {750,"fk"}}
259+ );
260+ }
261+
262+ QString mccToCountryCode(int mcc) const {
263+ return m_mccCodes.value(mcc, "us").toUpper();
264+ }
265+
266+private:
267+ // MCC = Mobile Country Code, see https://en.wikipedia.org/wiki/Mobile_country_code
268+ QHash<int,QString> m_mccCodes;
269+};
270+
271+Q_GLOBAL_STATIC(LocalePrivate, d)
272+
273+LocaleAttached::LocaleAttached(QObject* parent)
274+ : QObject(parent)
275+{
276+}
277+
278+QString LocaleAttached::mccToCountryCode(int mcc) const
279+{
280+ return d->mccToCountryCode(mcc);
281+}
282+
283+LocalePlugin::LocalePlugin(QObject* parent)
284+ : QObject(parent)
285+{
286+}
287+
288+LocaleAttached* LocalePlugin::qmlAttachedProperties(QObject* parent)
289+{
290+ return new LocaleAttached(parent);
291+}
plugins/Wizard/LocalePlugin.hnew file+58
@@ -0,0 +1,58 @@
1+/*
2+ * Copyright (C) 2015 Canonical Ltd.
3+ *
4+ * This program is free software: you can redistribute it and/or modify it
5+ * under the terms of the GNU General Public License version 3, as published
6+ * by the Free Software Foundation.
7+ *
8+ * This program is distributed in the hope that it will be useful, but
9+ * WITHOUT ANY WARRANTY; without even the implied warranties of
10+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11+ * PURPOSE. See the GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License along
14+ * with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef LOCALE_PLUGIN_H
18+#define LOCALE_PLUGIN_H
19+
20+#include <QObject>
21+#include <QString>
22+#include <QJsonObject>
23+#include <QtQml>
24+
25+class LocaleAttached: public QObject
26+{
27+ Q_OBJECT
28+protected:
29+ explicit LocaleAttached(QObject *parent = 0);
30+
31+public:
32+ Q_INVOKABLE QString mccToCountryCode(int mcc) const;
33+
34+ friend class LocalePlugin;
35+};
36+
37+/**
38+ * A simplified wrapper around QLocale.
39+ *
40+ * The wrapper is implemented as an attached property, which makes it possible
41+ * to use it as if its methods were static:
42+ *
43+ * @code
44+ * var langs = LocalePlugin.languages();
45+ * @endcode
46+ */
47+class LocalePlugin: public QObject
48+{
49+ Q_OBJECT
50+public:
51+ explicit LocalePlugin(QObject *parent = 0);
52+
53+ static LocaleAttached* qmlAttachedProperties(QObject *parent);
54+};
55+
56+QML_DECLARE_TYPEINFO(LocalePlugin, QML_HAS_ATTACHED_PROPERTIES)
57+
58+#endif // LOCALE_PLUGIN_H
plugins/Wizard/PageList.cpp+4−4
@@ -40,20 +40,20 @@ PageList::PageList(QObject *parent)
4040 m_index(-1),
4141 m_pages()
4242{
43− QString qmlSuffix = QStringLiteral(".qml");
44− QString disabledSuffix = QStringLiteral(".disabled");
43+ const QString qmlSuffix = QStringLiteral(".qml");
44+ const QString disabledSuffix = QStringLiteral(".disabled");
4545 QSet<QString> disabledPages;
4646 QStringList dataDirs;
4747
4848 if (!isRunningInstalled() && getenv("WIZARD_TESTING") == nullptr) {
49− dataDirs = QStringList() << qmlDirectory();
49+ dataDirs << qmlDirectory();
5050 } else {
5151 dataDirs = shellDataDirs();
5252 }
5353
5454 Q_FOREACH(const QString &dataDir, dataDirs) {
5555 QDir dir(dataDir + "/Wizard/Pages");
56− QStringList entries = dir.entryList(QStringList(QStringLiteral("[0-9]*")), QDir::Files | QDir::Readable);
56+ const QStringList entries = dir.entryList(QStringList(QStringLiteral("[0-9]*")), QDir::Files | QDir::Readable);
5757 Q_FOREACH(const QString &entry, entries) {
5858 if (!m_pages.contains(entry) && entry.endsWith(qmlSuffix))
5959 m_pages.insert(entry, dir.absoluteFilePath(entry));
plugins/Wizard/Status.cppnew file+143
@@ -0,0 +1,143 @@
1+/*
2+ * Copyright (C) 2015 Canonical Ltd.
3+ *
4+ * This program is free software: you can redistribute it and/or modify it
5+ * under the terms of the GNU General Public License version 3, as published
6+ * by the Free Software Foundation.
7+ *
8+ * This program is distributed in the hope that it will be useful, but
9+ * WITHOUT ANY WARRANTY; without even the implied warranties of
10+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11+ * PURPOSE. See the GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License along
14+ * with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include <QDebug>
18+#include <QDBusConnection>
19+
20+#include "Status.h"
21+
22+Status::Status()
23+{
24+ initNM();
25+ initUPower();
26+}
27+
28+void Status::initNM()
29+{
30+ m_nmIface = new QDBusInterface("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager", "org.freedesktop.NetworkManager",
31+ QDBusConnection::systemBus(), this);
32+
33+ QDBusConnection::systemBus().connect("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager", "org.freedesktop.NetworkManager", "PropertiesChanged",
34+ this, SLOT(onNMPropertiesChanged(QVariantMap)));
35+}
36+
37+void Status::onNMPropertiesChanged(const QVariantMap &changedProps)
38+{
39+ if (changedProps.contains("State") || changedProps.contains("Connectivity")) {
40+ Q_EMIT onlineChanged();
41+ Q_EMIT networkIconChanged();
42+ }
43+
44+ if (changedProps.contains("PrimaryConnection") || changedProps.contains("SpecificObject") || changedProps.contains("Strength")) {
45+ Q_EMIT networkIconChanged();
46+ }
47+}
48+
49+bool Status::online() const
50+{
51+ if (!m_nmIface->isValid())
52+ return false;
53+
54+ return m_nmIface->property("State").toUInt() == 70;
55+}
56+
57+QString Status::networkIcon()
58+{
59+ QString iconName = QStringLiteral("nm-no-connection");
60+
61+ if (!online()) {
62+ return iconName;
63+ }
64+
65+ const QString primaryConn = m_nmIface->property("PrimaryConnection").value<QDBusObjectPath>().path();
66+ const QString primaryConnType = m_nmIface->property("PrimaryConnectionType").toString();
67+
68+ if (primaryConn.isEmpty()) {
69+ qWarning() << "Empty primary connection";
70+ return iconName;
71+ }
72+
73+ if (primaryConnType == "802-11-wireless") {
74+ QDBusInterface activeConn("org.freedesktop.NetworkManager", primaryConn, "org.freedesktop.NetworkManager.Connection.Active", QDBusConnection::systemBus());
75+
76+ if (activeConn.isValid()) {
77+ const QString apPath = activeConn.property("SpecificObject").value<QDBusObjectPath>().path();
78+
79+ if (apPath.isEmpty()) {
80+ qWarning() << "No AP path";
81+ return iconName;
82+ }
83+
84+ QDBusConnection::systemBus().connect("org.freedesktop.NetworkManager", primaryConn, "org.freedesktop.NetworkManager.Connection.Active", "PropertiesChanged",
85+ this, SLOT(onNMPropertiesChanged(QVariantMap)));
86+
87+ QDBusInterface ap("org.freedesktop.NetworkManager", apPath, "org.freedesktop.NetworkManager.AccessPoint", QDBusConnection::systemBus());
88+
89+ if (!ap.isValid()) {
90+ qWarning() << "Invalid AP";
91+ return iconName;
92+ }
93+
94+ QDBusConnection::systemBus().connect("org.freedesktop.NetworkManager", apPath, "org.freedesktop.NetworkManager.AccessPoint", "PropertiesChanged",
95+ this, SLOT(onNMPropertiesChanged(QVariantMap)));
96+
97+ const uint strength = ap.property("Strength").toUInt();
98+ const uint flags = ap.property("Flags").toUInt();
99+
100+ if (strength == 0) {
101+ iconName = "nm-signal-00";
102+ } else if (strength <= 25) {
103+ iconName = "nm-signal-25";
104+ } else if (strength <= 50) {
105+ iconName = "nm-signal-50";
106+ } else if (strength <= 75) {
107+ iconName = "nm-signal-75";
108+ } else if (strength <= 100) {
109+ iconName = "nm-signal-100";
110+ }
111+
112+ if (flags >= 1) {
113+ iconName += "-secure";
114+ }
115+ }
116+ }
117+
118+ return iconName;
119+}
120+
121+void Status::initUPower()
122+{
123+ m_upowerIface = new QDBusInterface("org.freedesktop.UPower", "/org/freedesktop/UPower/devices/DisplayDevice", "org.freedesktop.UPower.Device",
124+ QDBusConnection::systemBus(), this);
125+ QDBusConnection::systemBus().connect("org.freedesktop.UPower", "/org/freedesktop/UPower/devices/DisplayDevice", "org.freedesktop.DBus.Properties",
126+ "PropertiesChanged", this, SLOT(onUPowerPropertiesChanged(QString,QVariantMap,QStringList)));
127+}
128+
129+void Status::onUPowerPropertiesChanged(const QString &iface, const QVariantMap &changedProps, const QStringList &invalidatedProps)
130+{
131+ Q_UNUSED(iface)
132+ Q_UNUSED(invalidatedProps)
133+
134+ if (changedProps.contains("IconName")) {
135+ Q_EMIT batteryIconChanged();
136+ }
137+}
138+
139+QString Status::batteryIcon() const
140+{
141+ const QString iconName = m_upowerIface->property("IconName").toString();
142+ return iconName;
143+}
plugins/Wizard/Status.hnew file+60
@@ -0,0 +1,60 @@
1+/*
2+ * Copyright (C) 2015 Canonical Ltd.
3+ *
4+ * This program is free software: you can redistribute it and/or modify it
5+ * under the terms of the GNU General Public License version 3, as published
6+ * by the Free Software Foundation.
7+ *
8+ * This program is distributed in the hope that it will be useful, but
9+ * WITHOUT ANY WARRANTY; without even the implied warranties of
10+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11+ * PURPOSE. See the GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License along
14+ * with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef WIZARD_STATUS_H
18+#define WIZARD_STATUS_H
19+
20+#include <QObject>
21+#include <QString>
22+#include <QDBusInterface>
23+
24+class Status: public QObject
25+{
26+ Q_OBJECT
27+ Q_PROPERTY(bool online READ online NOTIFY onlineChanged)
28+ Q_PROPERTY(QString networkIcon READ networkIcon NOTIFY networkIconChanged)
29+ Q_PROPERTY(QString batteryIcon READ batteryIcon NOTIFY batteryIconChanged)
30+public:
31+ Status();
32+ ~Status() = default;
33+
34+ bool online() const;
35+ QString networkIcon();
36+
37+ QString batteryIcon() const;
38+
39+Q_SIGNALS:
40+ void networkIconChanged();
41+ void onlineChanged();
42+ void batteryIconChanged();
43+
44+private Q_SLOTS:
45+ void onNMPropertiesChanged(const QVariantMap &changedProps);
46+ void onUPowerPropertiesChanged(const QString &iface, const QVariantMap &changedProps, const QStringList &invalidatedProps);
47+
48+private:
49+ Q_DISABLE_COPY(Status)
50+
51+ // network status
52+ void initNM();
53+ QDBusInterface * m_nmIface = nullptr;
54+
55+ // battery status
56+ void initUPower();
57+ QDBusInterface * m_upowerIface = nullptr;
58+};
59+
60+#endif
plugins/Wizard/System.cpp+5−4
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2014 Canonical Ltd.
2+ * Copyright (C) 2014-2015 Canonical Ltd.
33 *
44 * This program is free software: you can redistribute it and/or modify it
55 * under the terms of the GNU General Public License version 3, as published
@@ -25,10 +25,10 @@
2525#include <QLocale>
2626#include <QMap>
2727#include <QProcess>
28+#include <QDebug>
2829
2930System::System()
30− : QObject(),
31− m_fsWatcher()
31+ : QObject()
3232{
3333 // Register the argument needed for UpdateActivationEnvironment below
3434 qDBusRegisterMetaType<QMap<QString,QString>>();
@@ -88,7 +88,7 @@ void System::setSessionVariable(const QString &variable, const QString &value)
8888 QDBusConnection::sessionBus().asyncCall(msg);
8989}
9090
91−void System::updateSessionLanguage(const QString &locale)
91+void System::updateSessionLocale(const QString &locale)
9292{
9393 const QString language = locale.split(QStringLiteral("."))[0];
9494
@@ -106,5 +106,6 @@ void System::updateSessionLanguage(const QString &locale)
106106 initctl stop smart-scopes-proxy; \
107107 initctl emit --no-wait indicator-services-start; \
108108 initctl restart --no-wait maliit-server; \
109+ initctl restart --no-wait indicator-messages; \
109110 initctl restart --no-wait unity8-dash\""));
110111}
plugins/Wizard/System.h+3−2
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2014 Canonical Ltd.
2+ * Copyright (C) 2014-2015 Canonical Ltd.
33 *
44 * This program is free software: you can redistribute it and/or modify it
55 * under the terms of the GNU General Public License version 3, as published
@@ -28,12 +28,13 @@ class System : public QObject
2828
2929public:
3030 System();
31+ ~System() = default;
3132
3233 bool wizardEnabled() const;
3334 void setWizardEnabled(bool enabled);
3435
3536public Q_SLOTS:
36− void updateSessionLanguage(const QString &locale);
37+ void updateSessionLocale(const QString &locale);
3738
3839Q_SIGNALS:
3940 void wizardEnabledChanged();
plugins/Wizard/plugin.cpp+7−8
@@ -17,19 +17,18 @@
1717#include "plugin.h"
1818#include "PageList.h"
1919#include "System.h"
20+#include "timezonemodel.h"
21+#include "LocalePlugin.h"
22+#include "Status.h"
2023
2124#include <QtQml/qqml.h>
2225
23−static QObject *system_provider(QQmlEngine *engine, QJSEngine *scriptEngine)
24−{
25− Q_UNUSED(engine)
26− Q_UNUSED(scriptEngine)
27− return new System();
28−}
29−
3026void WizardPlugin::registerTypes(const char *uri)
3127{
3228 Q_ASSERT(uri == QLatin1String("Wizard"));
3329 qmlRegisterType<PageList>(uri, 0, 1, "PageList");
34− qmlRegisterSingletonType<System>(uri, 0, 1, "System", system_provider);
30+ qmlRegisterSingletonType<System>(uri, 0, 1, "System", [](QQmlEngine*, QJSEngine*) -> QObject* { return new System; });
31+ qmlRegisterSingletonType<Status>(uri, 0, 1, "Status", [](QQmlEngine*, QJSEngine*) -> QObject* { return new Status; });
32+ qmlRegisterType<TimeZoneLocationModel>(uri, 0, 1, "TimeZoneModel");
33+ qmlRegisterType<LocalePlugin>(uri, 0, 1, "LocalePlugin");
3534}
plugins/Wizard/plugin.h+1−2
@@ -17,13 +17,12 @@
1717#ifndef WIZARD_PLUGIN_H
1818#define WIZARD_PLUGIN_H
1919
20−#include <QtQml/QQmlEngine>
2120#include <QtQml/QQmlExtensionPlugin>
2221
2322class WizardPlugin : public QQmlExtensionPlugin
2423{
2524 Q_OBJECT
26− Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface")
25+ Q_PLUGIN_METADATA(IID QQmlExtensionInterface_iid)
2726
2827public:
2928 void registerTypes(const char *uri) override;
plugins/Wizard/timezonemodel.cppnew file+244
@@ -0,0 +1,244 @@
1+/*
2+ * Copyright (C) 2016 Canonical Ltd.
3+ *
4+ * This program is free software: you can redistribute it and/or modify it
5+ * under the terms of the GNU General Public License version 3, as published
6+ * by the Free Software Foundation.
7+ *
8+ * This program is distributed in the hope that it will be useful, but
9+ * WITHOUT ANY WARRANTY; without even the implied warranties of
10+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11+ * PURPOSE. See the GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License along
14+ * with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include <QDebug>
18+
19+#include <glib.h>
20+#include <glib-object.h>
21+
22+#include "LocalePlugin.h"
23+#include "timezonemodel.h"
24+
25+TimeZoneLocationModel::TimeZoneLocationModel(QObject *parent):
26+ QAbstractListModel(parent),
27+ m_listUpdating(false),
28+ m_cancellable(nullptr)
29+{
30+ m_roleNames[Qt::DisplayRole] = "displayName";
31+ m_roleNames[TimeZoneRole] = "timeZone";
32+ m_roleNames[CityRole] = "city";
33+ m_roleNames[CountryRole] = "country";
34+ m_roleNames[OffsetRole] = "offset";
35+ m_roleNames[LatitudeRole] = "latitude";
36+ m_roleNames[LongitudeRole] = "longitude";
37+}
38+
39+int TimeZoneLocationModel::rowCount(const QModelIndex &parent) const
40+{
41+ if (parent.isValid()) {
42+ return 0;
43+ } else if (m_filter.isEmpty()) {
44+ return m_countryLocations.count();
45+ } else {
46+ return m_locations.count();
47+ }
48+}
49+
50+QVariant TimeZoneLocationModel::data(const QModelIndex &index, int role) const
51+{
52+ GeonamesCity *city;
53+ if (m_filter.isEmpty()) {
54+ city = m_countryLocations.value(index.row());
55+ } else {
56+ city = m_locations.value(index.row());
57+ }
58+ if (!city)
59+ return QVariant();
60+
61+ switch (role) {
62+ case Qt::DisplayRole:
63+ return QStringLiteral("%1, %2, %3").arg(geonames_city_get_name(city))
64+ .arg(geonames_city_get_state(city))
65+ .arg(geonames_city_get_country(city));
66+ case SimpleRole:
67+ return QStringLiteral("%1, %2").arg(geonames_city_get_name(city))
68+ .arg(geonames_city_get_country(city));
69+ case TimeZoneRole:
70+ return geonames_city_get_timezone(city);
71+ case CountryRole:
72+ return geonames_city_get_country(city);
73+ case CityRole:
74+ return geonames_city_get_name(city);
75+ case OffsetRole: {
76+ QTimeZone tmp(geonames_city_get_timezone(city));
77+ return static_cast<double>(tmp.standardTimeOffset(QDateTime::currentDateTime())) / 3600;
78+ }
79+ case LatitudeRole:
80+ return geonames_city_get_latitude(city);
81+ case LongitudeRole:
82+ return geonames_city_get_longitude(city);
83+ default:
84+ qWarning() << Q_FUNC_INFO << "Unknown role";
85+ return QVariant();
86+ }
87+}
88+
89+QHash<int, QByteArray> TimeZoneLocationModel::roleNames() const
90+{
91+ return m_roleNames;
92+}
93+
94+void TimeZoneLocationModel::setModel(const QList<GeonamesCity *> &locations)
95+{
96+ beginResetModel();
97+
98+ Q_FOREACH(GeonamesCity *city, m_locations) {
99+ geonames_city_free(city);
100+ }
101+
102+ m_locations = locations;
103+ endResetModel();
104+}
105+
106+void TimeZoneLocationModel::filterFinished(GObject *source_object,
107+ GAsyncResult *res,
108+ gpointer user_data)
109+{
110+ Q_UNUSED(source_object);
111+
112+ g_autofree gint *cities = nullptr;
113+ guint cities_len = 0;
114+ g_autoptr(GError) error = nullptr;
115+
116+ cities = geonames_query_cities_finish(res, &cities_len, &error);
117+ if (error) {
118+ if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
119+ TimeZoneLocationModel *model = static_cast<TimeZoneLocationModel *>(user_data);
120+ g_clear_object(&model->m_cancellable);
121+ model->setListUpdating(false);
122+ qWarning() << "Could not filter timezones:" << error->message;
123+ }
124+ return;
125+ }
126+
127+ QList<GeonamesCity *> locations;
128+
129+ for (guint i = 0; i < cities_len; ++i) {
130+ GeonamesCity *city = geonames_get_city(cities[i]);
131+ if (city) {
132+ locations.append(city);
133+ }
134+ }
135+
136+ TimeZoneLocationModel *model = static_cast<TimeZoneLocationModel *>(user_data);
137+
138+ g_clear_object(&model->m_cancellable);
139+
140+ model->setModel(locations);
141+ model->setListUpdating(false);
142+}
143+
144+bool TimeZoneLocationModel::listUpdating() const
145+{
146+ return m_listUpdating;
147+}
148+
149+void TimeZoneLocationModel::setListUpdating(bool listUpdating)
150+{
151+ if (m_listUpdating != listUpdating) {
152+ m_listUpdating = listUpdating;
153+ Q_EMIT listUpdatingChanged();
154+ }
155+}
156+
157+QString TimeZoneLocationModel::filter() const
158+{
159+ return m_filter;
160+}
161+
162+void TimeZoneLocationModel::setFilter(const QString &filter)
163+{
164+ if (filter != m_filter) {
165+ m_filter = filter;
166+ Q_EMIT filterChanged();
167+ }
168+
169+ setListUpdating(true);
170+
171+ if (m_cancellable) {
172+ g_cancellable_cancel(m_cancellable);
173+ g_clear_object(&m_cancellable);
174+ }
175+
176+ setModel(QList<GeonamesCity *>());
177+
178+ if (filter.isEmpty()) {
179+ setListUpdating(false);
180+ return;
181+ }
182+
183+ m_cancellable = g_cancellable_new();
184+ geonames_query_cities(filter.toUtf8().data(),
185+ GEONAMES_QUERY_DEFAULT,
186+ m_cancellable,
187+ filterFinished,
188+ this);
189+}
190+
191+QString TimeZoneLocationModel::country() const
192+{
193+ return m_country;
194+}
195+
196+static bool citycmp(GeonamesCity *a, GeonamesCity *b)
197+{
198+ return geonames_city_get_population(b) < geonames_city_get_population(a);
199+}
200+
201+void TimeZoneLocationModel::setCountry(const QString &country)
202+{
203+ if (m_country == country)
204+ return;
205+
206+ beginResetModel();
207+
208+ m_country = country;
209+
210+ Q_FOREACH(GeonamesCity *city, m_countryLocations) {
211+ geonames_city_free(city);
212+ }
213+ m_countryLocations.clear();
214+
215+ gint num_cities = geonames_get_n_cities();
216+ for (gint i = 0; i < num_cities; i++) {
217+ GeonamesCity *city = geonames_get_city(i);
218+ if (city && m_country == geonames_city_get_country_code(city)) {
219+ m_countryLocations.append(city);
220+ }
221+ }
222+
223+ std::sort(m_countryLocations.begin(), m_countryLocations.end(), citycmp);
224+
225+ endResetModel();
226+
227+ Q_EMIT countryChanged(country);
228+}
229+
230+TimeZoneLocationModel::~TimeZoneLocationModel()
231+{
232+ if (m_cancellable) {
233+ g_cancellable_cancel(m_cancellable);
234+ g_clear_object(&m_cancellable);
235+ }
236+
237+ Q_FOREACH(GeonamesCity *city, m_countryLocations) {
238+ geonames_city_free(city);
239+ }
240+
241+ Q_FOREACH(GeonamesCity *city, m_locations) {
242+ geonames_city_free(city);
243+ }
244+}
plugins/Wizard/timezonemodel.hnew file+80
@@ -0,0 +1,80 @@
1+/*
2+ * Copyright (C) 2016 Canonical Ltd.
3+ *
4+ * This program is free software: you can redistribute it and/or modify it
5+ * under the terms of the GNU General Public License version 3, as published
6+ * by the Free Software Foundation.
7+ *
8+ * This program is distributed in the hope that it will be useful, but
9+ * WITHOUT ANY WARRANTY; without even the implied warranties of
10+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11+ * PURPOSE. See the GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License along
14+ * with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef TIMEZONEMODEL_H
18+#define TIMEZONEMODEL_H
19+
20+#include <geonames.h>
21+#include <glib.h>
22+#include <QAbstractListModel>
23+
24+class TimeZoneLocationModel: public QAbstractListModel
25+{
26+ Q_OBJECT
27+ Q_PROPERTY(bool listUpdating READ listUpdating NOTIFY listUpdatingChanged)
28+ Q_PROPERTY(QString filter READ filter WRITE setFilter NOTIFY filterChanged)
29+ Q_PROPERTY(QString country READ country WRITE setCountry NOTIFY countryChanged)
30+ Q_ENUMS(Roles)
31+
32+public:
33+ explicit TimeZoneLocationModel(QObject *parent = nullptr);
34+ ~TimeZoneLocationModel();
35+
36+ enum Roles {
37+ TimeZoneRole = Qt::UserRole + 1,
38+ CityRole,
39+ CountryRole,
40+ SimpleRole,
41+ OffsetRole,
42+ LatitudeRole,
43+ LongitudeRole
44+ };
45+
46+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
47+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
48+ QHash<int, QByteArray> roleNames() const override;
49+
50+ bool listUpdating() const;
51+
52+ QString filter() const;
53+ void setFilter(const QString &filter);
54+
55+ QString country() const;
56+ void setCountry(const QString &country);
57+
58+Q_SIGNALS:
59+ void listUpdatingChanged();
60+ void filterChanged();
61+ void countryChanged(const QString &country);
62+
63+private:
64+ void setModel(const QList<GeonamesCity *> &locations);
65+ void setListUpdating(bool listUpdating);
66+ static void filterFinished(GObject *source_object,
67+ GAsyncResult *res,
68+ gpointer user_data);
69+
70+
71+ bool m_listUpdating;
72+ QString m_filter;
73+ QString m_country;
74+ GCancellable *m_cancellable;
75+ QHash<int, QByteArray> m_roleNames;
76+ QList<GeonamesCity *> m_locations;
77+ QList<GeonamesCity *> m_countryLocations;
78+};
79+
80+#endif
po/unity8.pot+256−178
@@ -8,7 +8,7 @@ msgid ""
88msgstr ""
99"Project-Id-Version: unity8\n"
1010"Report-Msgid-Bugs-To: \n"
11−"POT-Creation-Date: 2016-03-18 17:18+0000\n"
11+"POT-Creation-Date: 2016-03-23 10:01+0000\n"
1212"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
1313"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
1414"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -73,64 +73,46 @@ msgctxt "Button: Log out from the system"
7373msgid "Log Out"
7474msgstr ""
7575
76−#: qml/Components/Dialogs.qml:161 qml/Components/Dialogs.qml:194
77−#: qml/Components/Dialogs.qml:253
76+#: qml/Components/Dialogs.qml:161 qml/Components/Dialogs.qml:219
77+#: qml/Dash/DashPageHeader.qml:298 qml/Wizard/Pages/passcode-confirm.qml:32
78+#: qml/Wizard/Pages/passcode-set.qml:32
7879msgid "Cancel"
7980msgstr ""
8081
8182#: qml/Components/Dialogs.qml:173
82−msgctxt "Title: Reboot/Shut down dialog"
83−msgid "Shut down"
84−msgstr ""
85−
86−#: qml/Components/Dialogs.qml:174
87−msgid "Are you sure you want to shut down?"
88−msgstr ""
89−
90−#: qml/Components/Dialogs.qml:176
91−msgctxt "Button: Reboot the system"
92−msgid "Reboot"
93−msgstr ""
94−
95−#: qml/Components/Dialogs.qml:185
96−msgctxt "Button: Shut down the system"
97−msgid "Shut down"
98−msgstr ""
99−
100−#: qml/Components/Dialogs.qml:207
10183msgctxt "Title: Reboot dialog"
10284msgid "Reboot"
10385msgstr ""
10486
105−#: qml/Components/Dialogs.qml:208
87+#: qml/Components/Dialogs.qml:174
10688msgid "Are you sure you want to reboot?"
10789msgstr ""
10890
109−#: qml/Components/Dialogs.qml:210
91+#: qml/Components/Dialogs.qml:176
11092msgid "No"
11193msgstr ""
11294
113−#: qml/Components/Dialogs.qml:217
95+#: qml/Components/Dialogs.qml:183
11496msgid "Yes"
11597msgstr ""
11698
117−#: qml/Components/Dialogs.qml:232
99+#: qml/Components/Dialogs.qml:198
118100msgctxt "Title: Power off/Restart dialog"
119101msgid "Power"
120102msgstr ""
121103
122−#: qml/Components/Dialogs.qml:233
104+#: qml/Components/Dialogs.qml:199
123105msgid ""
124106"Are you sure you would like\n"
125107"to power off?"
126108msgstr ""
127109
128−#: qml/Components/Dialogs.qml:235
110+#: qml/Components/Dialogs.qml:201
129111msgctxt "Button: Power off the system"
130112msgid "Power off"
131113msgstr ""
132114
133−#: qml/Components/Dialogs.qml:244
115+#: qml/Components/Dialogs.qml:210
134116msgctxt "Button: Restart the system"
135117msgid "Restart"
136118msgstr ""
@@ -173,42 +155,58 @@ msgstr ""
173155msgid "Close all"
174156msgstr ""
175157
176−#: qml/Dash/DashPageHeader.qml:270
158+#: qml/Dash/DashPageHeader.qml:337
177159msgctxt "Button: Open the Ubuntu Store"
178160msgid "Store"
179161msgstr ""
180162
181−#: qml/Dash/DashPageHeader.qml:277
163+#: qml/Dash/DashPageHeader.qml:344
182164msgctxt "Button: Start a search in the current dash scope"
183165msgid "Search"
184166msgstr ""
185167
186−#: qml/Dash/DashPageHeader.qml:287
168+#: qml/Dash/DashPageHeader.qml:354
187169msgctxt "Button: Show the current dash scope settings"
188170msgid "Settings"
189171msgstr ""
190172
191−#: qml/Dash/DashPageHeader.qml:294
173+#: qml/Dash/DashPageHeader.qml:361
192174msgid "Remove from Favorites"
193175msgstr ""
194176
195−#: qml/Dash/DashPageHeader.qml:294
177+#: qml/Dash/DashPageHeader.qml:361
196178msgid "Add to Favorites"
197179msgstr ""
198180
199−#: qml/Dash/GenericScopeView.qml:567 qml/Dash/GenericScopeView.qml:725
181+#: qml/Dash/FiltersPopover.qml:60
182+msgid "Refine your results"
183+msgstr ""
184+
185+#: qml/Dash/FiltersPopover.qml:69
186+msgid "Reset"
187+msgstr ""
188+
189+#: qml/Dash/GenericScopeView.qml:570 qml/Dash/GenericScopeView.qml:755
200190msgid "See less"
201191msgstr ""
202192
203−#: qml/Dash/GenericScopeView.qml:567
193+#: qml/Dash/GenericScopeView.qml:570
204194msgid "See all"
205195msgstr ""
206196
207−#: qml/Dash/GenericScopeView.qml:629
197+#: qml/Dash/GenericScopeView.qml:627
208198msgctxt "Label: Hint for dash search line edit"
209199msgid "Search"
210200msgstr ""
211201
202+#: qml/Dash/PageHeaderExtraPanel.qml:55
203+msgid "Recent Searches"
204+msgstr ""
205+
206+#: qml/Dash/PageHeaderExtraPanel.qml:66
207+msgid "Clear All"
208+msgstr ""
209+
212210#: qml/Dash/Previews/PreviewActionCombo.qml:35
213211msgid "More..."
214212msgstr ""
@@ -276,11 +274,11 @@ msgstr ""
276274msgid "Tap to unlock"
277275msgstr ""
278276
279−#: qml/Greeter/NarrowView.qml:54 qml/Wizard/Pages/passwd-confirm.qml:53
277+#: qml/Greeter/NarrowView.qml:54
280278msgid "Sorry, incorrect passphrase."
281279msgstr ""
282280
283−#: qml/Greeter/NarrowView.qml:55 qml/Wizard/Pages/passwd-confirm.qml:54
281+#: qml/Greeter/NarrowView.qml:55
284282msgid "Sorry, incorrect passcode."
285283msgstr ""
286284
@@ -305,7 +303,7 @@ msgstr ""
305303msgid "Enter %1"
306304msgstr ""
307305
308−#: qml/Greeter/NarrowView.qml:117 qml/Wizard/Pages/passwd-set.qml:60
306+#: qml/Greeter/NarrowView.qml:117
309307msgid "Enter passphrase"
310308msgstr ""
311309
@@ -343,62 +341,62 @@ msgstr ""
343341msgid "Conference"
344342msgstr ""
345343
346−#: qml/Panel/Indicators/MenuItemFactory.qml:757
344+#: qml/Panel/Indicators/MenuItemFactory.qml:761
347345msgid "Nothing is playing"
348346msgstr ""
349347
350−#: qml/Panel/Indicators/MenuItemFactory.qml:886
348+#: qml/Panel/Indicators/MenuItemFactory.qml:890
351349#, qt-format
352350msgid "%1 hour"
353351msgid_plural "%1 hours"
354352msgstr[0] ""
355353msgstr[1] ""
356354
357−#: qml/Panel/Indicators/MenuItemFactory.qml:890
355+#: qml/Panel/Indicators/MenuItemFactory.qml:894
358356#, qt-format
359357msgid "%1 minute"
360358msgid_plural "%1 minutes"
361359msgstr[0] ""
362360msgstr[1] ""
363361
364−#: qml/Panel/Indicators/MenuItemFactory.qml:895
362+#: qml/Panel/Indicators/MenuItemFactory.qml:899
365363#, qt-format
366364msgid "%1 second"
367365msgid_plural "%1 seconds"
368366msgstr[0] ""
369367msgstr[1] ""
370368
371−#: qml/Panel/Indicators/MenuItemFactory.qml:898
369+#: qml/Panel/Indicators/MenuItemFactory.qml:902
372370msgid "0 seconds"
373371msgstr ""
374372
375373#. Translators: String like "1 hour, 2 minutes, 3 seconds remaining"
376−#: qml/Panel/Indicators/MenuItemFactory.qml:900
374+#: qml/Panel/Indicators/MenuItemFactory.qml:904
377375#, qt-format
378376msgid "%1 remaining"
379377msgstr ""
380378
381−#: qml/Panel/Indicators/MenuItemFactory.qml:906
379+#: qml/Panel/Indicators/MenuItemFactory.qml:910
382380msgid "In queue…"
383381msgstr ""
384382
385−#: qml/Panel/Indicators/MenuItemFactory.qml:910
383+#: qml/Panel/Indicators/MenuItemFactory.qml:914
386384msgid "Downloading"
387385msgstr ""
388386
389−#: qml/Panel/Indicators/MenuItemFactory.qml:912
387+#: qml/Panel/Indicators/MenuItemFactory.qml:916
390388msgid "Paused, tap to resume"
391389msgstr ""
392390
393−#: qml/Panel/Indicators/MenuItemFactory.qml:914
391+#: qml/Panel/Indicators/MenuItemFactory.qml:918
394392msgid "Canceled"
395393msgstr ""
396394
397−#: qml/Panel/Indicators/MenuItemFactory.qml:916
395+#: qml/Panel/Indicators/MenuItemFactory.qml:920
398396msgid "Finished"
399397msgstr ""
400398
401−#: qml/Panel/Indicators/MenuItemFactory.qml:918
399+#: qml/Panel/Indicators/MenuItemFactory.qml:922
402400msgid "Failed, tap to retry"
403401msgstr ""
404402
@@ -418,241 +416,321 @@ msgctxt "Button: Call back on phone"
418416msgid "Call back"
419417msgstr ""
420418
421−#: qml/Tutorial/TutorialBottom.qml:28
422−msgid "Open special menus"
419+#: qml/Stages/SideStage.qml:76
420+msgid "Drag using 3 fingers any application from one window to the other"
423421msgstr ""
424422
425−#: qml/Tutorial/TutorialBottom.qml:29
426−msgid "Swipe up from the bottom edge."
423+#: qml/Tutorial/TutorialBottom.qml:78
424+msgid "Swipe up to add a contact"
427425msgstr ""
428426
429−#: qml/Tutorial/TutorialBottomFinish.qml:24
430−msgid "This action does different things for different apps"
427+#: qml/Tutorial/TutorialBottom.qml:80
428+msgid "Swipe up for favorite calculations"
431429msgstr ""
432430
433−#: qml/Tutorial/TutorialBottomFinish.qml:25
434−msgid "Tap here to finish."
431+#: qml/Tutorial/TutorialBottom.qml:82
432+msgid "Swipe up for recent calls"
435433msgstr ""
436434
437−#: qml/Tutorial/TutorialContent.qml:80 qml/Tutorial/TutorialLeftFinish.qml:25
438−#: qml/Tutorial/TutorialRight.qml:144
439−msgid "Tap here to continue."
435+#: qml/Tutorial/TutorialBottom.qml:84
436+msgid "Swipe up to create a message"
440437msgstr ""
441438
442−#: qml/Tutorial/TutorialLeft.qml:26
443−msgid "Open the launcher"
439+#: qml/Tutorial/TutorialBottom.qml:85
440+msgid "Swipe up to manage the app"
444441msgstr ""
445442
446−#: qml/Tutorial/TutorialLeft.qml:27
447−msgid "Short swipe from the left edge."
443+#: qml/Tutorial/TutorialLeft.qml:47
444+msgid "Swipe from the left edge to open the launcher"
448445msgstr ""
449446
450−#: qml/Tutorial/TutorialLeftFinish.qml:24
451−msgid "These are the shortcuts to favorite apps"
447+#: qml/Tutorial/TutorialLeftLong.qml:47
448+msgid "Long swipe from the left edge to open the Today scope"
452449msgstr ""
453450
454−#: qml/Tutorial/TutorialPage.qml:178
455−msgid "You almost got it!"
451+#: qml/Tutorial/TutorialRight.qml:54
452+msgid "Hover your mouse on the right edge to view your open apps"
456453msgstr ""
457454
458−#: qml/Tutorial/TutorialPage.qml:195
459−msgid "Try again."
455+#: qml/Tutorial/TutorialRight.qml:55
456+msgid "Short or long swipe from the right edge to view your open apps"
460457msgstr ""
461458
462−#: qml/Tutorial/TutorialRight.qml:31
463−msgid "To view open apps"
459+#: qml/Tutorial/TutorialTop.qml:53
460+msgid "Swipe from the top right edge to open the notification bar"
464461msgstr ""
465462
466−#: qml/Tutorial/TutorialRight.qml:32
467−msgid "Long swipe from the right edge."
463+#: qml/Tutorial/TutorialTop.qml:54
464+msgid "Swipe from the top edge to open the notification bar"
468465msgstr ""
469466
470−#: qml/Tutorial/TutorialRight.qml:122
471−msgid "View all your running tasks."
472−msgstr ""
473−
474−#: qml/Wizard/Page.qml:89
467+#: qml/Wizard/Page.qml:54
475468msgctxt "Button: Go back one page in the Wizard"
476469msgid "Back"
477470msgstr ""
478471
479472#: qml/Wizard/Pages/10-welcome.qml:27
480−msgid "Hi!"
473+msgid "Language"
481474msgstr ""
482475
483−#: qml/Wizard/Pages/10-welcome.qml:44
484−msgid "Welcome to your Ubuntu device."
476+#: qml/Wizard/Pages/10-welcome.qml:156 qml/Wizard/Pages/30-wifi.qml:206
477+#: qml/Wizard/Pages/40-location.qml:271 qml/Wizard/Pages/50-timezone.qml:267
478+#: qml/Wizard/Pages/60-account.qml:65 qml/Wizard/Pages/70-passwd-type.qml:144
479+#: qml/Wizard/Pages/75-report-check.qml:84
480+#: qml/Wizard/Pages/passcode-desktop.qml:124
481+#: qml/Wizard/Pages/password-set.qml:141
482+msgid "Next"
485483msgstr ""
486484
487−#: qml/Wizard/Pages/10-welcome.qml:52
488−msgid "Let’s get started."
485+#: qml/Wizard/Pages/30-wifi.qml:29
486+msgid "Connect to Wi‑Fi"
489487msgstr ""
490488
491−#: qml/Wizard/Pages/10-welcome.qml:88 qml/Wizard/Pages/30-passwd-type.qml:133
492−#: qml/Wizard/Pages/40-wifi.qml:215 qml/Wizard/Pages/50-location.qml:131
493−#: qml/Wizard/Pages/60-reporting.qml:60 qml/Wizard/Pages/passwd-confirm.qml:82
494−#: qml/Wizard/Pages/passwd-set.qml:90
495−msgid "Continue"
489+#: qml/Wizard/Pages/30-wifi.qml:130
490+msgid "Connected"
496491msgstr ""
497492
498−#: qml/Wizard/Pages/20-sim.qml:25
499−msgid "Add a SIM card and restart your device"
493+#: qml/Wizard/Pages/30-wifi.qml:163
494+msgid "Available Wi-Fi networks"
500495msgstr ""
501496
502−#: qml/Wizard/Pages/20-sim.qml:59
503−msgid "Without it, you won’t be able to make calls or use text messaging."
497+#: qml/Wizard/Pages/30-wifi.qml:164
498+msgid "No available Wi-Fi networks"
504499msgstr ""
505500
506−#: qml/Wizard/Pages/20-sim.qml:73 qml/Wizard/Pages/40-wifi.qml:215
501+#: qml/Wizard/Pages/30-wifi.qml:206 qml/Wizard/Pages/60-account.qml:65
502+#: qml/Wizard/Pages/sim.qml:101
507503msgid "Skip"
508504msgstr ""
509505
510−#: qml/Wizard/Pages/30-passwd-type.qml:39
511−msgid "Lock security"
506+#: qml/Wizard/Pages/40-location.qml:27
507+msgid "Location Services"
512508msgstr ""
513509
514−#: qml/Wizard/Pages/30-passwd-type.qml:79
515−msgid "Please select how you’d like to unlock your device."
510+#: qml/Wizard/Pages/40-location.qml:83
511+msgid ""
512+"Use GPS, Wi-Fi hotspots and mobile network anonymously to detect location "
513+"(recommended)"
516514msgstr ""
517515
518−#: qml/Wizard/Pages/30-passwd-type.qml:102
519−msgctxt "Label: Type of security method"
520−msgid "Swipe"
516+#: qml/Wizard/Pages/40-location.qml:105
517+#, qt-format
518+msgid "By selecting this option you agree to the Nokia HERE %1."
519+msgstr ""
520+
521+#: qml/Wizard/Pages/40-location.qml:106
522+msgctxt "part of: Nokia HERE terms and conditions"
523+msgid "terms and conditions"
524+msgstr ""
525+
526+#: qml/Wizard/Pages/40-location.qml:160
527+msgid "GPS only"
528+msgstr ""
529+
530+#: qml/Wizard/Pages/40-location.qml:214
531+msgid "Don't use my location"
532+msgstr ""
533+
534+#: qml/Wizard/Pages/40-location.qml:260
535+msgid "You can change it later in System Settings."
521536msgstr ""
522537
523−#: qml/Wizard/Pages/30-passwd-type.qml:103
524−msgctxt "Label: Description of security method"
525−msgid "No security"
538+#: qml/Wizard/Pages/50-timezone.qml:29
539+msgid "Time Zone"
526540msgstr ""
527541
528−#: qml/Wizard/Pages/30-passwd-type.qml:105
542+#: qml/Wizard/Pages/50-timezone.qml:178
543+msgid "Enter your city"
544+msgstr ""
545+
546+#: qml/Wizard/Pages/60-account.qml:24
547+msgid "Personalize Your Device"
548+msgstr ""
549+
550+#: qml/Wizard/Pages/60-account.qml:48
551+msgid "Preferred Name"
552+msgstr ""
553+
554+#: qml/Wizard/Pages/70-passwd-type.qml:39
555+msgid "Lock Screen"
556+msgstr ""
557+
558+#: qml/Wizard/Pages/70-passwd-type.qml:101
529559msgctxt "Label: Type of security method"
530−msgid "Passcode"
560+msgid "Create new password"
531561msgstr ""
532562
533−#: qml/Wizard/Pages/30-passwd-type.qml:106
534−msgctxt "Label: Description of security method"
535−msgid "4 digits only"
563+#: qml/Wizard/Pages/70-passwd-type.qml:103
564+msgctxt "Label: Type of security method"
565+msgid "Create passcode (numbers only)"
536566msgstr ""
537567
538−#: qml/Wizard/Pages/30-passwd-type.qml:108
568+#: qml/Wizard/Pages/70-passwd-type.qml:105
539569msgctxt "Label: Type of security method"
540−msgid "Passphrase"
570+msgid "No lock code"
541571msgstr ""
542572
543−#: qml/Wizard/Pages/30-passwd-type.qml:109
544−msgctxt "Label: Description of security method"
545−msgid "Numbers and letters"
573+#: qml/Wizard/Pages/75-report-check.qml:26 qml/Wizard/Pages/here-terms.qml:108
574+msgid "Privacy Policy"
546575msgstr ""
547576
548−#: qml/Wizard/Pages/40-wifi.qml:29
549−msgid "Connect to Wi‑Fi"
577+#: qml/Wizard/Pages/75-report-check.qml:26
578+msgid "Help Us Improve"
579+msgstr ""
580+
581+#: qml/Wizard/Pages/75-report-check.qml:59
582+msgid "Improve system performance by sending us crashes and error reports."
550583msgstr ""
551584
552−#: qml/Wizard/Pages/40-wifi.qml:168
553−msgid "Available networks…"
585+#: qml/Wizard/Pages/75-report-check.qml:60
586+msgid "Privacy policy"
554587msgstr ""
555588
556−#: qml/Wizard/Pages/40-wifi.qml:169
557−msgid "No available networks."
589+#: qml/Wizard/Pages/80-finished.qml:89
590+msgid "Welcome to Ubuntu"
558591msgstr ""
559592
560−#: qml/Wizard/Pages/50-location.qml:27
561−msgid "Location"
593+#: qml/Wizard/Pages/80-finished.qml:104
594+msgid "You are ready to use your device now"
562595msgstr ""
563596
564−#: qml/Wizard/Pages/50-location.qml:62
565−msgid "Let the phone detect your location:"
597+#: qml/Wizard/Pages/80-finished.qml:124
598+msgid "Get Started"
566599msgstr ""
567600
568−#: qml/Wizard/Pages/50-location.qml:69
569−msgid "Using GPS only (less accurate)"
601+#: qml/Wizard/Pages/here-terms.qml:25
602+msgid "Terms & Conditions"
570603msgstr ""
571604
572−#: qml/Wizard/Pages/50-location.qml:86
573−msgid "Using GPS, anonymized Wi-Fi and cellular network info (recommended)"
605+#: qml/Wizard/Pages/here-terms.qml:69
606+msgid "Your device uses positioning technologies provided by HERE."
574607msgstr ""
575608
576−#. Translators: HERE is a trademark for Nokia's location service, you probably shouldn't translate it
577−#: qml/Wizard/Pages/50-location.qml:103
609+#: qml/Wizard/Pages/here-terms.qml:81
578610msgid ""
579−"By selecting this option you agree to the Nokia HERE <a href='#'>terms and "
580−"conditions</a>."
611+"To provide you with positioning services and to improve their quality, HERE "
612+"collects information about nearby cell towers and Wi-Fi hotspots around your "
613+"current location whenever your position is being found."
581614msgstr ""
582615
583−#: qml/Wizard/Pages/50-location.qml:112
584−msgid "Not at all"
616+#: qml/Wizard/Pages/here-terms.qml:93
617+msgid ""
618+"The information collected is used to analyze the service and to improve the "
619+"use of service, but not to identify you personally."
585620msgstr ""
586621
587−#: qml/Wizard/Pages/50-location.qml:124
588−msgid "You can change your mind later in <b>System Settings</b>."
622+#: qml/Wizard/Pages/here-terms.qml:106
623+#, qt-format
624+msgid "By continuing, you agree to the HERE platform %1 and %2."
589625msgstr ""
590626
591−#: qml/Wizard/Pages/60-reporting.qml:26
592−msgid "Improving your experience"
627+#: qml/Wizard/Pages/here-terms.qml:107
628+msgid "Service Terms"
593629msgstr ""
594630
595−#: qml/Wizard/Pages/60-reporting.qml:46
596−msgid ""
597−"Your phone is set up to automatically report errors to Canonical and its "
598−"partners, the makers of the operating system."
631+#: qml/Wizard/Pages/passcode-confirm.qml:43
632+#: qml/Wizard/Pages/passcode-desktop.qml:80
633+msgid "Confirm passcode"
599634msgstr ""
600635
601−#: qml/Wizard/Pages/60-reporting.qml:53
602−msgid ""
603−"This can be disabled in <b>System Settings</b> under <b>Security &amp; "
604−"Privacy</b>"
636+#: qml/Wizard/Pages/passcode-confirm.qml:45
637+msgid "Incorrect passcode."
605638msgstr ""
606639
607−#: qml/Wizard/Pages/80-finished.qml:24
608−msgid "All done"
640+#: qml/Wizard/Pages/passcode-confirm.qml:45
641+msgctxt "Enter the passcode again"
642+msgid "Please re-enter."
609643msgstr ""
610644
611−#: qml/Wizard/Pages/80-finished.qml:39
612−msgid "Nice work!"
645+#: qml/Wizard/Pages/passcode-desktop.qml:30
646+msgid "Lock Screen Passcode"
613647msgstr ""
614648
615−#: qml/Wizard/Pages/80-finished.qml:46
616−msgid "Your device is now ready to use."
649+#: qml/Wizard/Pages/passcode-desktop.qml:56
650+msgid "Enter 4 numbers to setup your passcode"
617651msgstr ""
618652
619−#: qml/Wizard/Pages/80-finished.qml:53
620−msgid "Finish"
653+#: qml/Wizard/Pages/passcode-desktop.qml:65
654+#: qml/Wizard/Pages/passcode-set.qml:54
655+msgid "Choose passcode"
621656msgstr ""
622657
623−#: qml/Wizard/Pages/here-terms.qml:27
624−msgid "Terms & Conditions"
658+#: qml/Wizard/Pages/passcode-desktop.qml:105
659+msgid "Passcode too short"
625660msgstr ""
626661
627−#: qml/Wizard/Pages/passwd-confirm.qml:49
628−msgid "Confirm passphrase"
662+#: qml/Wizard/Pages/passcode-desktop.qml:107
663+msgid "Passcodes match"
629664msgstr ""
630665
631−#: qml/Wizard/Pages/passwd-confirm.qml:50
632−msgid "Confirm passcode"
666+#: qml/Wizard/Pages/passcode-desktop.qml:109
667+msgid "Passcodes do not match"
633668msgstr ""
634669
635−#: qml/Wizard/Pages/passwd-confirm.qml:53
636−#: qml/Wizard/Pages/passwd-confirm.qml:54
637−msgid "Please try again."
670+#: qml/Wizard/Pages/passcode-set.qml:62
671+msgid "Passcode must be 4 characters long"
638672msgstr ""
639673
640−#: qml/Wizard/Pages/passwd-set.qml:61
641−msgid "Choose your passcode"
674+#: qml/Wizard/Pages/password-set.qml:30
675+msgid "Lock Screen Password"
642676msgstr ""
643677
644−#: qml/Wizard/Pages/passwd-set.qml:68
645−msgid "Passphrase must be 4 characters long"
678+#: qml/Wizard/Pages/password-set.qml:61
679+msgid "Enter at least 8 characters"
646680msgstr ""
647681
648−#. Translators: This is the arrow for "Back" buttons
649−#: qml/Wizard/StackButton.qml:39
650−#, qt-format
651−msgid "〈 %1"
682+#: qml/Wizard/Pages/password-set.qml:73
683+msgid "Choose password"
652684msgstr ""
653685
654−#. Translators: This is the arrow for "Forward" buttons
655−#: qml/Wizard/StackButton.qml:42
656−#, qt-format
657−msgid "%1 〉"
686+#: qml/Wizard/Pages/password-set.qml:103
687+msgid "Confirm password"
688+msgstr ""
689+
690+#: qml/Wizard/Pages/sim.qml:27
691+msgid "No SIM card installed"
692+msgstr ""
693+
694+#: qml/Wizard/Pages/sim.qml:54
695+msgid "SIM card added"
696+msgstr ""
697+
698+#: qml/Wizard/Pages/sim.qml:55
699+msgid "You must restart the device to access the mobile network."
700+msgstr ""
701+
702+#: qml/Wizard/Pages/sim.qml:59
703+msgid "Restart"
704+msgstr ""
705+
706+#: qml/Wizard/Pages/sim.qml:78
707+msgid "You won’t be able to make calls or use text messaging without a SIM."
708+msgstr ""
709+
710+#: qml/Wizard/Pages/sim.qml:90
711+msgid "To proceed with no SIM tap Skip."
712+msgstr ""
713+
714+#: qml/Wizard/PasswordMeter.qml:87
715+msgid "Passwords match"
716+msgstr ""
717+
718+#: qml/Wizard/PasswordMeter.qml:89
719+msgid "Passwords do not match"
720+msgstr ""
721+
722+#: qml/Wizard/PasswordMeter.qml:93
723+msgid "Strong password"
724+msgstr ""
725+
726+#: qml/Wizard/PasswordMeter.qml:95
727+msgid "Fair password"
728+msgstr ""
729+
730+#: qml/Wizard/PasswordMeter.qml:97
731+msgid "Weak password"
732+msgstr ""
733+
734+#: qml/Wizard/PasswordMeter.qml:99
735+msgid "Very weak password"
658736msgstr ""
qml/Components/Dialogs.qml+6−47
@@ -166,40 +166,6 @@ Item {
166166 }
167167 }
168168
169− Component {
170− id: shutdownDialogComponent
171− ShellDialog {
172− id: shutdownDialog
173− title: i18n.ctr("Title: Reboot/Shut down dialog", "Shut down")
174− text: i18n.tr("Are you sure you want to shut down?")
175− Button {
176− text: i18n.ctr("Button: Reboot the system", "Reboot")
177− onClicked: {
178− root.closeAllApps();
179− unitySessionService.reboot();
180− shutdownDialog.hide();
181− }
182− color: UbuntuColors.lightGrey
183− }
184− Button {
185− text: i18n.ctr("Button: Shut down the system", "Shut down")
186− onClicked: {
187− root.closeAllApps();
188− unitySessionService.shutdown();
189− shutdownDialog.hide();
190− }
191− color: UbuntuColors.red
192− }
193− Button {
194− text: i18n.tr("Cancel")
195− onClicked: {
196− shutdownDialog.hide();
197− }
198− color: UbuntuColors.lightGrey
199− }
200− }
201− }
202−
203169 Component {
204170 id: rebootDialogComponent
205171 ShellDialog {
@@ -273,23 +239,16 @@ Item {
273239
274240 onShutdownRequested: {
275241 // Display a dialog to ask the user to confirm.
276− if (!dialogLoader.active) {
277− dialogLoader.sourceComponent = shutdownDialogComponent;
278− dialogLoader.focus = true;
279− dialogLoader.active = true;
280− }
242+ showPowerDialog();
281243 }
282244
283245 onRebootRequested: {
284246 // Display a dialog to ask the user to confirm.
285− if (!dialogLoader.active) {
286− // display a combined reboot/shutdown dialog, sadly the session indicator calls rather the "Reboot()" method
287− // than shutdown when clicking on the "Shutdown..." menu item
288− // FIXME: when/if session indicator is fixed, put the rebootDialogComponent here
289− dialogLoader.sourceComponent = shutdownDialogComponent;
290− dialogLoader.focus = true;
291− dialogLoader.active = true;
292− }
247+
248+ // display a combined reboot/shutdown dialog, sadly the session indicator calls rather the "Reboot()" method
249+ // than shutdown when clicking on the "Shutdown..." menu item
250+ // FIXME: when/if session indicator is fixed, put the rebootDialogComponent here
251+ showPowerDialog();
293252 }
294253
295254 onLogoutReady: {
qml/Components/InputMethod.qml+4−1
@@ -51,7 +51,10 @@ Item {
5151 }
5252
5353 state: {
54− if (surfaceItem.surface && surfaceItem.surfaceState != Mir.MinimizedState && root.enabled) {
54+ if (surfaceItem.surface &&
55+ surfaceItem.surfaceState != Mir.HiddenState &&
56+ surfaceItem.surfaceState != Mir.MinimizedState &&
57+ root.enabled) {
5558 return "shown";
5659 } else {
5760 return "hidden";
qml/Components/Showable.qml+10
@@ -31,6 +31,7 @@ Item {
3131 property bool required
3232 property bool __shouldShow: false
3333 property bool __skipShowAnimation: false
34+ property bool __skipHideAnimation: false
3435
3536 property list<QtObject> hides
3637 property var showAnimation
@@ -120,12 +121,21 @@ Item {
120121 if (!hideAnimation.running) {
121122 hideAnimation.restart()
122123 }
124+ if (__skipHideAnimation) {
125+ hideAnimation.complete();
126+ }
123127 } else {
124128 visible = false
125129 required = false
126130 }
127131
128132 shown = false
133+ __skipHideAnimation = false;
134+ }
135+
136+ function hideNow() {
137+ __skipHideAnimation = true;
138+ hide();
129139 }
130140
131141 Connections {
qml/Components/Unity8Settings.qmldeleted−50
@@ -1,50 +0,0 @@
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.4
18−import GSettings 1.0
19−
20−QtObject {
21− id: root
22−
23− property string usageMode: "Staged"
24−
25− // FIXME: Works around a bug where if we change a Loader's source in response to a GSettings
26− // property change in the same event loop, the Loader's previously loaded component does not
27− // get destroyed and its bindings continue to operate!
28− //
29− // Shouldn't be needed after
30− // https://code.launchpad.net/~lukas-kde/gsettings-qt/queued-processing/+merge/259883 gets
31− // merged.
32− property var timer: Timer {
33− interval: 1
34− onTriggered: { root.usageMode = root.wrapped.usageMode; }
35− }
36− property var wrappedConnections: Connections {
37− target: root.wrapped
38− ignoreUnknownSignals: true // don't spam us
39− onUsageModeChanged: { root.timer.start(); }
40− }
41− property var wrapped: GSettings {
42− schema.id: "com.canonical.Unity8"
43− Component.onCompleted: {
44− // init the value. it's a dynamic prop, so we have to check first
45− if (root.usageMode) {
46− root.usageMode = root.wrapped.usageMode;
47− }
48− }
49− }
50−}
qml/Dash/DashContent.qml+2−2
@@ -108,7 +108,7 @@ Item {
108108 objectName: "dashContentList"
109109
110110 interactive: !dashContent.forceNonInteractive && dashContent.scopes.loaded && currentItem
111− && !currentItem.moving && !currentItem.navigationDisableParentInteractive && !currentItem.subPageShown
111+ && !currentItem.moving && !currentItem.subPageShown && !currentItem.extraPanelShown
112112 anchors.fill: parent
113113 orientation: ListView.Horizontal
114114 boundsBehavior: Flickable.DragAndOvershootBounds
@@ -170,7 +170,7 @@ Item {
170170 objectName: "scopeLoader" + index
171171
172172 readonly property bool moving: item ? item.moving : false
173− readonly property bool navigationDisableParentInteractive: item ? item.navigationDisableParentInteractive : false
173+ readonly property bool extraPanelShown: item ? item.extraPanelShown : false
174174 readonly property bool subPageShown: item ? item.subPageShown : false
175175 readonly property var categoryView: item ? item.categoryView : null
176176 readonly property var theScope: scope
qml/Dash/DashNavigation.qml+159−78
@@ -17,101 +17,182 @@
1717import QtQuick 2.4
1818import Ubuntu.Components 1.3
1919import Dash 0.1
20−import "../Components"
2120
2221Item {
2322 id: root
2423 objectName: "dashNavigation"
2524
25+ // set by parent
2626 property var scope: null
27− property var scopeStyle: null
28−
29− property alias windowWidth: blackRect.width
30− property alias windowHeight: blackRect.height
31− readonly property var openList: {
32− if (navigationButton.showList) return navigationButton.listView;
33− if (altNavigationButton.showList) return altNavigationButton.listView;
34− return null;
35− }
36− readonly property bool disableParentInteractive: {
37− return navigationButton.showList || altNavigationButton.showList ||
38− navigationButton.inverseMousePressed || altNavigationButton.inverseMousePressed;
39− }
27+ property real availableHeight
28+
29+ signal leafClicked()
4030
41− // FIXME this is only here for highlight purposes (see Background.qml, too)
42− readonly property var background: backgroundItem
31+ // internal
32+ readonly property var currentNavigation: scope && scope.hasNavigation ? scope.getNavigation(scope.currentNavigationId) : null
33+ // Are we drilling down the tree or up?
34+ property bool isEnteringChildren: false
4335
4436 visible: height != 0
45− height: navigationButton.currentNavigation || altNavigationButton.currentNavigation ? units.gu(5) : 0
46−
47− QtObject {
48− id: d
49− readonly property color foregroundColor: root.scopeStyle
50− ? root.scopeStyle.getTextColor(backgroundItem.luminance)
51− : theme.palette.normal.baseText
52− readonly property bool bothVisible: altNavigationButton.visible && navigationButton.visible
53− readonly property real navigationWidth: root.width >= units.gu(60) ? units.gu(40) : root.width
54− readonly property real buttonWidth: navigationWidth / (bothVisible ? 2 : 1)
55− }
37+ implicitHeight: scope && scope.hasNavigation ? navigationListView.y + navigationListView.height : 0
5638
57− Rectangle {
58− id: blackRect
59− objectName: "blackRect"
60− color: "black"
61− opacity: openList && openList.currentItem && openList.currentItem.visible ? 0.5 : 0
62− Behavior on opacity { UbuntuNumberAnimation { duration: UbuntuAnimation.SnapDuration } }
63− anchors { left: parent.left; right: parent.right }
64− visible: opacity > 0
39+ function resetNavigation() {
40+ if (navigationModel.count > 1) {
41+ clear();
42+ }
6543 }
6644
67− Background {
68− id: backgroundItem
69− anchors.fill: parent
70− style: scopeStyle ? scopeStyle.navigationBackground : "color:///#f5f5f5"
71− }
45+ Column {
46+ id: headersColumn
47+ anchors {
48+ top: parent.top
49+ left: parent.left
50+ right: parent.right
51+ }
7252
73− Image {
74− fillMode: Image.Stretch
75− source: scopeStyle.backgroundLuminance > 0.2 ? "graphics/navigation_shadow.png" : "graphics/navigation_shadow_light.png"
76− anchors { top: parent.bottom; left: parent.left; right: parent.right }
77− }
53+ function pop(popsNeeded) {
54+ if (popsNeeded == 0)
55+ return;
56+ isEnteringChildren = false;
57+ navigationListView.currentIndex = navigationListView.currentIndex - popsNeeded;
58+ navigationModel.setProperty(navigationListView.currentIndex, "nullifyNavigation", false);
59+ navigationModel.remove(navigationModel.count - popsNeeded, popsNeeded);
60+
61+ popsNeeded = Math.min(headersModel.count, popsNeeded);
62+ // This is effectively deleting ourselves, so needs to be the last thing of the function
63+ headersModel.remove(headersModel.count - popsNeeded, popsNeeded);
64+ }
65+
66+ Repeater {
67+ model: ListModel {
68+ id: headersModel
69+ // Roles
70+ // headerText: the text to show
71+ // navigationId: the navigation Id that represents
72+ // parentNavigationId: the parent navigation Id
73+ }
74+ delegate: DashNavigationHeader {
75+ objectName: "dashNavigationHeader" + index
76+ height: index == 0 && headersModel.count > 1 ? 0 : units.gu(5)
77+ width: headersColumn.width
78+
79+ backVisible: index != 0
80+ text: headerText
7881
79− DashNavigationButton {
80− id: altNavigationButton
81− objectName: "altNavigationButton"
82− height: root.height
83− width: d.buttonWidth
84− scope: root.scope
85− scopeStyle: root.scopeStyle
86− foregroundColor: d.foregroundColor
87− listView.width: d.navigationWidth
88− isAltNavigation: true
89− showDivider: navigationButton.visible || root.width > d.navigationWidth
90− // needed so that InverseMouseArea is above navigationButton
91− z: listView.height > 0 ? 1 : 0
82+ onBackClicked: {
83+ scope.setNavigationState(parentNavigationId);
84+
85+ var popsNeeded = headersModel.count - index;
86+ headersColumn.pop(popsNeeded);
87+ }
88+
89+ onTextClicked: {
90+ scope.setNavigationState(navigationId);
91+
92+ var popsNeeded = headersModel.count - index - 1;
93+ headersColumn.pop(popsNeeded);
94+
95+ root.leafClicked();
96+ }
97+ }
98+ }
9299 }
93100
94− DashNavigationButton {
95− id: navigationButton
96− objectName: "navigationButton"
97− height: root.height
98− width: altNavigationButton.visible ? d.buttonWidth : d.navigationWidth
99− x: altNavigationButton.visible ? d.buttonWidth : 0
100− scope: root.scope
101− scopeStyle: root.scopeStyle
102− foregroundColor: d.foregroundColor
103− listView.width: d.navigationWidth
104− listView.x: -x
105− showDivider: root.width > d.navigationWidth
101+ ListView {
102+ id: navigationListView
103+ objectName: "navigationListView"
104+ orientation: ListView.Horizontal
105+ interactive: false
106+ model: ListModel {
107+ id: navigationModel
108+ // We have two roles
109+ // navigationId: the navigation id of the navigation the list represents
110+ // nullifyNavigation: overrides navigationId to be null
111+ // This is used to "clear" the delegate when going "right" on the tree
112+ }
113+ anchors {
114+ top: headersColumn.bottom
115+ left: parent.left
116+ right: parent.right
117+ }
118+ readonly property int maxHeight: root.availableHeight - navigationListView.y
119+ property int prevHeight: maxHeight
120+ height: currentItem ? currentItem.height : maxHeight
121+
122+ onHeightChanged: {
123+ if (currentItem) {
124+ prevHeight = currentItem.desiredHeight;
125+ }
126+ }
127+ highlightMoveDuration: UbuntuAnimation.FastDuration
128+ delegate: DashNavigationList {
129+ objectName: "navigation" + index
130+ visible: height > 0
131+ width: navigationListView.width
132+ property real desiredHeight: {
133+ if (navigation && navigation.loaded && x == navigationListView.contentX)
134+ {
135+ return Math.min(implicitHeight, navigationListView.maxHeight);
136+ } else {
137+ return navigationListView.prevHeight;
138+ }
139+ }
140+ height: desiredHeight
141+ navigation: (nullifyNavigation || !scope) ? null : scope.getNavigation(navigationId)
142+ currentNavigation: root.currentNavigation
143+ onEnterNavigation: { // var newNavigationId, string newNavigationLabel, bool hasChildren
144+ scope.setNavigationState(newNavigationId);
145+ // We only need to add a new item to the model
146+ // if we have children, otherwise just load it
147+ if (hasChildren) {
148+ isEnteringChildren = true;
149+ navigationModel.append({"navigationId": newNavigationId, "nullifyNavigation": false});
150+ headersModel.append({"headerText": newNavigationLabel,
151+ "navigationId": newNavigationId,
152+ "parentNavigationId": navigationId
153+ });
154+ navigationListView.currentIndex++;
155+ } else {
156+ root.leafClicked();
157+ }
158+ }
159+ }
160+ onContentXChanged: {
161+ if (navigationListView.highlightMoveDuration == 0)
162+ return;
163+
164+ if (contentX == width * navigationListView.currentIndex) {
165+ if (isEnteringChildren) {
166+ navigationModel.setProperty(navigationListView.currentIndex - 1, "nullifyNavigation", true);
167+ }
168+ }
169+ }
106170 }
107171
108− Image {
109− fillMode: Image.Stretch
110− source: backgroundItem.luminance > 0.7 ? "graphics/navigation_shadow.png" : "graphics/navigation_shadow_light.png"
111− x: navigationButton.listView.height > 0 ? altNavigationButton.x : navigationButton.x
112− width: d.buttonWidth
113− rotation: 180
114− anchors.bottom: parent.bottom
115− visible: d.bothVisible && (navigationButton.listView.height > 0 || altNavigationButton.listView.height > 0)
172+ property bool isFirstLoad: false
173+ onScopeChanged: clear();
174+ function clear() {
175+ navigationModel.clear();
176+ headersModel.clear();
177+ isFirstLoad = true;
178+ }
179+ function setNewNavigation() {
180+ if (isFirstLoad && currentNavigation && currentNavigation.loaded) {
181+ isFirstLoad = false;
182+ if (currentNavigation.count > 0) {
183+ navigationModel.append({"navigationId": scope.currentNavigationId, "nullifyNavigation": false});
184+ } else {
185+ navigationModel.append({"navigationId": currentNavigation.parentNavigationId, "nullifyNavigation": false});
186+ }
187+ headersModel.append({"headerText": currentNavigation.allLabel != "" ? currentNavigation.allLabel : currentNavigation.label,
188+ "navigationId": currentNavigation.navigationId,
189+ "parentNavigationId": currentNavigation.parentNavigationId
190+ });
191+ }
192+ }
193+ Connections {
194+ target: currentNavigation
195+ onLoadedChanged: setNewNavigation();
116196 }
197+ onCurrentNavigationChanged: setNewNavigation();
117198}
qml/Dash/DashNavigationButton.qmldeleted−233
@@ -1,233 +0,0 @@
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−import QtQuick 2.4
18−import Ubuntu.Components 1.3
19−
20−AbstractButton {
21− id: root
22− objectName: "dashNavigation"
23−
24− // Set by parent
25− property var scope: null
26− property var scopeStyle: null
27− property color foregroundColor: theme.palette.normal.baseText
28− property bool isAltNavigation: false
29− property bool showDivider: false
30−
31− // Used by parent
32− readonly property var currentNavigation: scope && scope[hasNavigation] ? getNavigation(scope[currentNavigationId]) : null
33− readonly property alias listView: navigationListView
34− readonly property bool inverseMousePressed: inverseMouseArea.pressed
35− property bool showList: false
36−
37− // Internal
38− // Are we drilling down the tree or up?
39− property bool isGoingBack: false
40− readonly property string hasNavigation: isAltNavigation ? "hasAltNavigation" : "hasNavigation"
41− readonly property string currentNavigationId: isAltNavigation ? "currentAltNavigationId" : "currentNavigationId"
42− function getNavigation(navId) {
43− if (isAltNavigation) {
44− return scope.getAltNavigation(navId);
45− } else {
46− return scope.getNavigation(navId);
47− }
48− }
49−
50− visible: root.currentNavigation != null
51−
52− onClicked: {
53− navigationListView.updateMaxHeight();
54− root.showList = !root.showList;
55− }
56−
57− Label {
58− anchors.fill: parent
59− anchors.margins: units.gu(2)
60− anchors.rightMargin: units.gu(5)
61− verticalAlignment: Text.AlignVCenter
62− text: root.currentNavigation ? root.currentNavigation.label : ""
63− color: root.foregroundColor
64− elide: Text.ElideRight
65− maximumLineCount: 1
66− }
67−
68− Icon {
69− anchors.verticalCenter: parent.verticalCenter
70− anchors.right: parent.right
71− anchors.rightMargin: units.gu(2)
72− name: showList ? "up" : "down"
73− height: units.gu(2)
74− width: height
75− color: root.foregroundColor
76− }
77−
78− // navigationListView is outside root
79− ListView {
80− id: navigationListView
81− objectName: "navigationListView"
82− visible: height > 0
83− orientation: ListView.Horizontal
84− interactive: false
85− clip: root.width != windowWidth
86− model: ListModel {
87− id: navigationModel
88− // We have two roles
89− // navigationId: the navigation id of the navigation the list represents
90− // nullifyNavigation: overrides navigationId to be null
91− // This is used to "clear" the delegate when going "right" on the tree
92− }
93− anchors.top: root.bottom
94− property int maxHeight: -1
95− Component.onCompleted: updateMaxHeight();
96− function updateMaxHeight()
97− {
98− maxHeight = (windowHeight - mapToItem(null, 0, 0).y) - units.gu(8);
99− }
100− property int prevHeight: maxHeight
101− height: currentItem ? currentItem.height : maxHeight
102− onHeightChanged: {
103− if (root.showList) {
104− prevHeight = currentItem.desiredHeight;
105− }
106− }
107− highlightMoveDuration: UbuntuAnimation.FastDuration
108− delegate: DashNavigationList {
109− objectName: "navigation" + index
110− visible: height > 0
111− width: navigationListView.width
112− scopeStyle: root.scopeStyle
113− foregroundColor: root.foregroundColor
114− property real desiredHeight: {
115− if (root.showList) {
116− if (navigation && navigation.loaded && x == navigationListView.contentX)
117− {
118− navigationListView.updateMaxHeight();
119− return Math.min(implicitHeight, navigationListView.maxHeight);
120− } else {
121− return navigationListView.prevHeight;
122− }
123− } else {
124− return 0;
125− }
126− }
127− height: desiredHeight
128− navigation: (nullifyNavigation || !scope) ? null : getNavigation(navigationId)
129− currentNavigation: root.currentNavigation
130− onEnterNavigation: {
131− scope.setNavigationState(newNavigationId, isAltNavigation);
132− // We only need to add a new item to the model
133− // if we have children, otherwise just load it
134− if (hasChildren) {
135− isGoingBack = false;
136− navigationModel.append({"navigationId": newNavigationId, "nullifyNavigation": false});
137− navigationListView.currentIndex++;
138− } else {
139− showList = false;
140− }
141− }
142− onGoBackToParentClicked: {
143− if (navigationListView.currentIndex == 0) {
144− // This can happen if we jumped to the non root of a deep tree and the user
145− // is now going back, create space in the list for the list to move "left"
146− var aux = navigationListView.highlightMoveDuration;
147− navigationListView.highlightMoveDuration = 0;
148− navigationModel.insert(0, {"navigationId": navigation.parentNavigationId, "nullifyNavigation": false});
149− navigationListView.currentIndex = navigationListView.currentIndex + 1;
150− navigationListView.contentX = width * navigationListView.currentIndex;
151− navigationListView.highlightMoveDuration = aux;
152− }
153−
154− scope.setNavigationState(navigation.parentNavigationId, isAltNavigation);
155− isGoingBack = true;
156− navigationModel.setProperty(navigationListView.currentIndex - 1, "nullifyNavigation", false);
157− navigationListView.currentIndex--;
158− }
159− onAllNavigationClicked: {
160− showList = false;
161− if (root.currentNavigation.parentNavigationId == navigation.navigationId) {
162− // For leaves we have to go to the parent too
163− scope.setNavigationState(root.currentNavigation.parentNavigationId, isAltNavigation);
164− }
165− }
166− }
167− onContentXChanged: {
168− if (navigationListView.highlightMoveDuration == 0)
169− return;
170−
171− if (contentX == width * navigationListView.currentIndex) {
172− if (isGoingBack) {
173− navigationModel.remove(navigationListView.currentIndex + 1);
174− } else {
175− navigationModel.setProperty(navigationListView.currentIndex - 1, "nullifyNavigation", true);
176− }
177− }
178− }
179− }
180−
181− Image {
182− anchors {
183− top: navigationListView.bottom
184− left: navigationListView.left
185− right: navigationListView.right
186− }
187− fillMode: Image.Stretch
188− source: "graphics/navigation_shadow.png"
189− visible: root.showList
190− }
191−
192− property bool isFirstLoad: false
193− onScopeChanged: {
194− navigationModel.clear();
195− isFirstLoad = true;
196− }
197− function setNewNavigation() {
198− if (isFirstLoad && currentNavigation && currentNavigation.loaded) {
199− isFirstLoad = false;
200− if (currentNavigation.count > 0) {
201− navigationModel.append({"navigationId": scope[currentNavigationId], "nullifyNavigation": false});
202− } else {
203− navigationModel.append({"navigationId": currentNavigation.parentNavigationId, "nullifyNavigation": false});
204− }
205− }
206− }
207− Connections {
208− target: currentNavigation
209− onLoadedChanged: setNewNavigation();
210− }
211− onCurrentNavigationChanged: setNewNavigation();
212−
213− InverseMouseArea {
214− id: inverseMouseArea
215− anchors.fill: navigationListView
216− enabled: root.showList
217− onPressed: root.showList = false
218− }
219−
220− Rectangle {
221− visible: root.showDivider
222− anchors {
223− top: parent.top
224− topMargin: units.dp(1)
225− bottom: parent.bottom
226− left: parent.right
227− leftMargin: -units.dp(0.5)
228− }
229− width: units.dp(1)
230− color: root.foregroundColor
231− opacity: 0.2
232− }
233−}
qml/Dash/DashNavigationHeader.qmlnew file+73
@@ -0,0 +1,73 @@
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.4
18+import Ubuntu.Components 1.3
19+import Ubuntu.Components.ListItems 1.3 as ListItem
20+
21+Row {
22+ id: root
23+
24+ property alias backVisible: backImageItem.visible
25+ property alias textEnabled: textItem.enabled
26+ property alias text: textLabel.text
27+
28+ readonly property alias backButtonWidth: backImageItem.width
29+
30+ signal backClicked()
31+ signal textClicked()
32+
33+ ListItem.Standard {
34+ id: backImageItem
35+ objectName: "backButton"
36+ height: parent.height
37+ width: height
38+ showDivider: false
39+
40+ Icon {
41+ id: backImage
42+ anchors.centerIn: parent
43+ name: "back"
44+ height: units.gu(2)
45+ width: height
46+ }
47+
48+ onClicked: root.backClicked();
49+ }
50+
51+ ListItem.Standard {
52+ id: textItem
53+ height: parent.height
54+ showDivider: false
55+
56+ Label {
57+ id: textLabel
58+ anchors {
59+ verticalCenter: parent.verticalCenter
60+ left: parent.left
61+ right: parent.right
62+ leftMargin: backImageItem.visible ? 0 : units.gu(2)
63+ rightMargin: units.gu(2)
64+ }
65+ color: "#5D5D5D"
66+ wrapMode: Text.Wrap
67+ maximumLineCount: 2
68+ elide: Text.ElideMiddle
69+ }
70+
71+ onClicked: root.textClicked();
72+ }
73+}
qml/Dash/DashNavigationList.qml+33−85
@@ -16,27 +16,18 @@
1616
1717import QtQuick 2.4
1818import Ubuntu.Components 1.3
19−import Ubuntu.Components.ListItems 1.3 as ListItem
19+import Ubuntu.Components.ListItems 1.3 as ListItems
2020import "../Components"
2121
2222Item {
2323 id: root
2424 property var navigation: null
2525 property var currentNavigation: null
26− property var scopeStyle: null
27− property color foregroundColor: theme.palette.normal.baseText
28− signal enterNavigation(var newNavigationId, bool hasChildren)
29− signal goBackToParentClicked()
30− signal allNavigationClicked()
26+ signal enterNavigation(var newNavigationId, string newNavigationLabel, bool hasChildren)
3127
3228 readonly property int itemHeight: units.gu(5)
3329 implicitHeight: flickable.contentHeight
3430
35− Background {
36− style: root.scopeStyle ? root.scopeStyle.navigationBackground : "color:///#f5f5f5"
37− anchors.fill: parent
38− }
39−
4031 clip: true
4132
4233 Behavior on height {
@@ -59,89 +50,46 @@ Item {
5950 id: column
6051 width: parent.width
6152
62− ListItem.Standard {
63− id: backButton
64− objectName: "backButton"
65− visible: navigation && !navigation.isRoot || false
66− height: itemHeight
67−
68− onClicked: root.goBackToParentClicked();
69−
70− Icon {
71− id: backImage
53+ Repeater {
54+ model: navigation && navigation.loaded ? navigation : null
55+ clip: true
56+ // FIXME Move to ListItem (and remove import) once 1556971 is fixed
57+ delegate: ListItems.Empty {
58+ objectName: root.objectName + "child" + index
59+ height: root.itemHeight
60+ width: column.width
7261 anchors {
73− verticalCenter: parent.verticalCenter
74− left: parent.left
62+ left: column.left
63+ right: column.right
7564 leftMargin: units.gu(2)
76− }
77− name: "back"
78− height: units.gu(2)
79− width: height
80− color: root.foregroundColor
81− }
82−
83− Label {
84− anchors {
85− verticalCenter: parent.verticalCenter
86− left: backImage.right
87− right: parent.right
88− leftMargin: units.gu(0.5)
8965 rightMargin: units.gu(2)
9066 }
91− text: navigation ? navigation.parentLabel : ""
92− color: root.foregroundColor
93− wrapMode: Text.Wrap
94− maximumLineCount: 2
95− elide: Text.ElideMiddle
96− }
97− }
9867
99− ListItem.Standard {
100− id: allButton
101− objectName: "allButton"
102− visible: navigation && (!navigation.isRoot || (!navigation.hidden && root.currentNavigation && !root.currentNavigation.isRoot && root.currentNavigation.parentNavigationId == navigation.navigationId)) || false
103− height: itemHeight
68+ onClicked: root.enterNavigation(navigationId, allLabel != "" ? allLabel : label, hasChildren)
10469
105− Label {
106− anchors {
107− verticalCenter: parent.verticalCenter
108− left: parent.left
109− right: parent.right
110− leftMargin: units.gu(2)
111− rightMargin: units.gu(2)
70+ Icon {
71+ id: leftIcon
72+ anchors {
73+ verticalCenter: parent.verticalCenter
74+ left: parent.left
75+ }
76+ height: units.gu(2)
77+ width: height
78+ name: "tick"
79+ color: "#3EB34F"
80+ visible: isActive
11281 }
113− text: navigation ? (navigation.allLabel != "" ? navigation.allLabel : navigation.label) : ""
114− font.bold: true
115− color: root.foregroundColor
116− wrapMode: Text.Wrap
117− maximumLineCount: 2
118− elide: Text.ElideMiddle
119− }
120−
121− onClicked: root.allNavigationClicked();
122− }
123−
124− Repeater {
125− model: navigation && navigation.loaded ? navigation : null
126− clip: true
127− delegate: ListItem.Standard {
128− objectName: root.objectName + "child" + index
129− height: root.itemHeight
130− showDivider: index != navigation.count - 1
131− selected: isActive
132−
133− onClicked: root.enterNavigation(navigationId, hasChildren)
13482
13583 Label {
13684 anchors {
13785 verticalCenter: parent.verticalCenter
138− left: parent.left
139− leftMargin: units.gu(2)
140− right: rightIcon.visible ? rightIcon.left : parent.right
141− rightMargin: rightIcon.visible ? units.gu(0.5) : units.gu(2)
86+ left: leftIcon.right
87+ leftMargin: units.gu(1)
88+ right: rightIcon.left
89+ rightMargin: units.gu(2)
14290 }
14391 text: label
144− color: root.foregroundColor
92+ color: isActive ? "#333333" : "#888888"
14593 wrapMode: Text.Wrap
14694 maximumLineCount: 2
14795 elide: Text.ElideMiddle
@@ -152,14 +100,14 @@ Item {
152100 anchors {
153101 verticalCenter: parent.verticalCenter
154102 right: parent.right
155− rightMargin: units.gu(2)
156103 }
157104 height: units.gu(2)
158105 width: height
159− name: hasChildren ? "go-next" : "tick"
160− color: root.foregroundColor
161− visible: hasChildren || isActive
106+ name: "go-next"
107+ visible: hasChildren
162108 }
109+
110+ divider.visible: index != navigation.count - 1
163111 }
164112 }
165113 }
qml/Dash/DashPageHeader.qml+140−131
@@ -17,41 +17,45 @@
1717import QtQuick 2.4
1818import Ubuntu.Components 1.3
1919import Ubuntu.Components.Themes.Ambiance 1.3
20−import Ubuntu.Components.Popups 1.3
2120import Ubuntu.Components.ListItems 1.3
2221import "../Components"
23−import "../Components/SearchHistoryModel"
2422
2523Item {
2624 id: root
2725 objectName: "pageHeader"
28− implicitHeight: headerContainer.height + bottomContainer.height + (showSignatureLine ? units.gu(2) : 0)
26+ implicitHeight: headerContainer.height + signatureLineHeight
27+ readonly property real signatureLineHeight: showSignatureLine ? units.gu(2) : 0
2928
29+ property int activeFiltersCount: 0
30+ property bool scopeHasFilters: false
3031 property bool showBackButton: false
3132 property bool backIsClose: false
3233 property string title
34+ property var extraPanel
35+ property string navigationTag
3336
3437 property bool storeEntryEnabled: false
3538 property bool searchEntryEnabled: false
3639 property bool settingsEnabled: false
3740 property bool favoriteEnabled: false
3841 property bool favorite: false
39− property ListModel searchHistory: SearchHistoryModel
42+ property ListModel searchHistory
4043 property alias searchQuery: searchTextField.text
4144 property alias searchHint: searchTextField.placeholderText
4245 property bool showSignatureLine: true
4346
44− property alias bottomItem: bottomContainer.children
4547 property int paginationCount: 0
4648 property int paginationIndex: -1
4749
4850 property var scopeStyle: null
4951
52+ signal clearSearch(bool keepPanelOpen)
5053 signal backClicked()
5154 signal storeClicked()
5255 signal settingsClicked()
5356 signal favoriteClicked()
5457 signal searchTextFieldFocused()
58+ signal showFiltersPopup(var item)
5559
5660 onScopeStyleChanged: refreshLogo()
5761 onSearchQueryChanged: {
@@ -60,6 +64,12 @@ Item {
6064 headerContainer.showSearch = true;
6165 }
6266 }
67+ onNavigationTagChanged: {
68+ // Make sure we are at the search page if the navigation tag changes behind our feet
69+ if (navigationTag) {
70+ headerContainer.showSearch = true;
71+ }
72+ }
6373
6474 function triggerSearch() {
6575 if (searchEntryEnabled) {
@@ -69,12 +79,14 @@ Item {
6979 }
7080
7181 function closePopup(keepFocus) {
72− if (headerContainer.popover != null) {
73− headerContainer.popover.unfocusOnDestruction = !keepFocus;
74− PopupUtils.close(headerContainer.popover);
82+ if (extraPanel.visible) {
83+ extraPanel.visible = false;
7584 } else if (!keepFocus) {
7685 unfocus();
7786 }
87+ if (!searchTextField.text && !root.navigationTag && searchHistory.count == 0) {
88+ headerContainer.showSearch = false;
89+ }
7890 }
7991
8092 function resetSearch(keepFocus) {
@@ -87,22 +99,17 @@ Item {
8799
88100 function unfocus() {
89101 searchTextField.focus = false;
90− if (!searchTextField.text) {
102+ if (!searchTextField.text && !root.navigationTag) {
91103 headerContainer.showSearch = false;
92104 }
93105 }
94106
95− function openSearchHistory() {
107+ function openPopup() {
96108 if (openSearchAnimation.running) {
97− openSearchAnimation.openSearchHistory = true;
98− } else if (root.searchHistory.count > 0 && headerContainer.popover == null) {
99− headerContainer.popover = PopupUtils.open(popoverComponent, searchTextField,
100− {
101− "contentWidth": searchTextField.width,
102− "edgeMargins": units.gu(1)
103− }
104− );
105− searchTextField.forceActiveFocus();
109+ openSearchAnimation.openPopup = true;
110+ } else if (extraPanel.hasContents) {
111+ // Show extraPanel
112+ extraPanel.visible = true;
106113 }
107114 }
108115
@@ -121,13 +128,11 @@ Item {
121128 }
122129
123130 InverseMouseArea {
124− anchors { fill: parent; margins: units.gu(1); bottomMargin: units.gu(3) + bottomContainer.height }
131+ anchors { fill: parent; margins: units.gu(1); bottomMargin: units.gu(3) + (extraPanel ? extraPanel.height : 0) }
125132 visible: headerContainer.showSearch
126133 onPressed: {
134+ extraPanel.visible = false;
127135 closePopup(/* keepFocus */false);
128− if (!searchTextField.text) {
129− headerContainer.showSearch = false;
130− }
131136 mouse.accepted = false;
132137 }
133138 }
@@ -143,7 +148,6 @@ Item {
143148 contentY: showSearch ? 0 : height
144149
145150 property bool showSearch: false
146− property var popover: null
147151
148152 Background {
149153 id: background
@@ -154,12 +158,12 @@ Item {
154158 Behavior on contentY {
155159 UbuntuNumberAnimation {
156160 id: openSearchAnimation
157− property bool openSearchHistory: false
161+ property bool openPopup: false
158162
159163 onRunningChanged: {
160− if (!running && openSearchAnimation.openSearchHistory) {
161− openSearchAnimation.openSearchHistory = false;
162− root.openSearchHistory();
164+ if (!running && openSearchAnimation.openPopup) {
165+ openSearchAnimation.openPopup = false;
166+ root.openPopup();
163167 }
164168 }
165169 }
@@ -183,60 +187,123 @@ Item {
183187 backgroundColor: "transparent"
184188 config: PageHeadConfiguration {
185189 foregroundColor: root.scopeStyle ? root.scopeStyle.headerForeground : theme.palette.normal.baseText
186− backAction: Action {
187− iconName: "back"
188− onTriggered: {
189− root.resetSearch();
190− headerContainer.showSearch = false;
191− }
192− }
193190 }
194− property var contents: TextField {
195− id: searchTextField
196− objectName: "searchTextField"
197− inputMethodHints: Qt.ImhNoPredictiveText
198− hasClearButton: false
199− anchors {
200− fill: parent
201− leftMargin: units.gu(1)
202− topMargin: units.gu(1)
203− bottomMargin: units.gu(1)
204− rightMargin: root.width > units.gu(60) ? root.width - units.gu(40) : units.gu(1)
205− }
191+ property var contents: Item {
192+ anchors.fill: parent
193+
194+ TextField {
195+ id: searchTextField
196+ objectName: "searchTextField"
197+ inputMethodHints: Qt.ImhNoPredictiveText
198+ hasClearButton: false
199+ anchors {
200+ top: parent.top
201+ topMargin: units.gu(1)
202+ left: parent.left
203+ bottom: parent.bottom
204+ bottomMargin: units.gu(1)
205+ right: settingsButton.left
206+ rightMargin: settingsButton.visible ? 0 : units.gu(2)
207+ }
206208
207− secondaryItem: AbstractButton {
208− height: searchTextField.height
209− width: height
210− enabled: searchTextField.text.length > 0
209+ primaryItem: Rectangle {
210+ color: "#F5F4F5"
211+ width: root.navigationTag != "" ? tagLabel.width + units.gu(2) : 0
212+ height: root.navigationTag != "" ? tagLabel.height + units.gu(1) : 0
213+ radius: units.gu(0.5)
214+ Label {
215+ id: tagLabel
216+ text: root.navigationTag
217+ anchors.centerIn: parent
218+ color: "#333333"
219+ }
220+ }
211221
212− Image {
213− objectName: "clearIcon"
214− anchors.fill: parent
215− anchors.margins: units.gu(.75)
216− source: "image://theme/clear"
217− opacity: searchTextField.text.length > 0
218− visible: opacity > 0
219− Behavior on opacity {
220− UbuntuNumberAnimation { duration: UbuntuAnimation.FastDuration }
222+ secondaryItem: AbstractButton {
223+ id: clearButton
224+ height: searchTextField.height
225+ width: height
226+ enabled: searchTextField.text.length > 0 || root.navigationTag != ""
227+
228+ Image {
229+ objectName: "clearIcon"
230+ anchors.fill: parent
231+ anchors.margins: units.gu(1)
232+ source: "image://theme/clear"
233+ opacity: parent.enabled
234+ visible: opacity > 0
235+ Behavior on opacity {
236+ UbuntuNumberAnimation { duration: UbuntuAnimation.FastDuration }
237+ }
238+ }
239+
240+ onClicked: {
241+ root.clearSearch(true);
221242 }
222243 }
223244
224− onClicked: {
225− root.resetSearch(true);
226− root.openSearchHistory();
245+ onActiveFocusChanged: {
246+ if (activeFocus) {
247+ root.searchTextFieldFocused();
248+ root.openPopup();
249+ }
250+ }
251+
252+ onTextChanged: {
253+ if (text != "") {
254+ closePopup(/* keepFocus */true);
255+ }
227256 }
228257 }
229258
230− onActiveFocusChanged: {
231− if (activeFocus) {
232− root.searchTextFieldFocused();
233− root.openSearchHistory();
259+ AbstractButton {
260+ id: settingsButton
261+ objectName: "settingsButton"
262+
263+ width: root.scopeHasFilters ? height : 0
264+ visible: width > 0
265+ anchors {
266+ top: parent.top
267+ right: cancelButton.left
268+ bottom: parent.bottom
269+ rightMargin: units.gu(-1)
270+ }
271+
272+ Icon {
273+ anchors.fill: parent
274+ anchors.margins: units.gu(2)
275+ name: "filters"
276+ color: root.activeFiltersCount > 0 ? UbuntuColors.orange : Qt.rgba(0.0, 0.0, 0.0, 0.0)
277+ }
278+
279+ onClicked: {
280+ root.showFiltersPopup(settingsButton);
234281 }
235282 }
236283
237− onTextChanged: {
238− if (text != "") {
239− closePopup(/* keepFocus */true);
284+ AbstractButton {
285+ id: cancelButton
286+ width: cancelLabel.width + cancelLabel.anchors.rightMargin + cancelLabel.anchors.leftMargin
287+ anchors {
288+ top: parent.top
289+ right: parent.right
290+ bottom: parent.bottom
291+ }
292+ onClicked: {
293+ root.clearSearch(false);
294+ headerContainer.showSearch = false;
295+ }
296+ Label {
297+ id: cancelLabel
298+ text: i18n.tr("Cancel")
299+ color: header.panelForegroundColor
300+ verticalAlignment: Text.AlignVCenter
301+ anchors {
302+ verticalCenter: parent.verticalCenter
303+ right: parent.right
304+ rightMargin: units.gu(2)
305+ leftMargin: units.gu(1)
306+ }
240307 }
241308 }
242309 }
@@ -322,47 +389,6 @@ Item {
322389 }
323390 }
324391
325− Component {
326− id: popoverComponent
327− Popover {
328− id: popover
329− autoClose: false
330−
331− property bool unfocusOnDestruction: false
332−
333− Component.onDestruction: {
334− headerContainer.popover = null;
335− if (unfocusOnDestruction) {
336− root.unfocus();
337− }
338− }
339−
340− Column {
341− anchors {
342− top: parent.top
343− left: parent.left
344− right: parent.right
345− }
346−
347− Repeater {
348− id: recentSearches
349− objectName: "recentSearches"
350− model: searchHistory
351−
352− delegate: Standard {
353− showDivider: index < recentSearches.count - 1
354− text: query
355− onClicked: {
356− searchHistory.addQuery(text);
357− searchTextField.text = text;
358− closePopup(/* keepFocus */false);
359− }
360− }
361− }
362− }
363− }
364− }
365−
366392 Rectangle {
367393 id: bottomBorder
368394 visible: showSignatureLine
@@ -370,7 +396,7 @@ Item {
370396 top: headerContainer.bottom
371397 left: parent.left
372398 right: parent.right
373− bottom: bottomContainer.top
399+ bottom: parent.bottom
374400 }
375401
376402 color: root.scopeStyle ? root.scopeStyle.headerDividerColor : "#e0e0e0"
@@ -411,7 +437,7 @@ Item {
411437 id: bottomHighlight
412438 visible: bottomBorder.visible
413439 anchors {
414− top: bottomContainer.top
440+ top: parent.bottom
415441 left: parent.left
416442 right: parent.right
417443 }
@@ -419,30 +445,13 @@ Item {
419445 height: units.dp(1)
420446 opacity: 0.6
421447
422− // FIXME this should be a shader when bottomItem exists
423− // to support image backgrounds
424448 Rectangle {
425449 anchors.fill: parent
426− color: if (bottomItem && bottomItem.background) {
427− Qt.lighter(Qt.rgba(bottomItem.background.topColor.r,
428− bottomItem.background.topColor.g,
429− bottomItem.background.topColor.b, 1.0), 1.2);
430− } else if (!bottomItem && root.scopeStyle) {
450+ color: if (root.scopeStyle) {
431451 Qt.lighter(Qt.rgba(root.scopeStyle.background.r,
432452 root.scopeStyle.background.g,
433453 root.scopeStyle.background.b, 1.0), 1.2);
434454 } else "#CCFFFFFF"
435455 }
436456 }
437−
438− Item {
439− id: bottomContainer
440−
441− anchors {
442− left: parent.left
443− right: parent.right
444− bottom: parent.bottom
445− }
446− height: childrenRect.height
447− }
448457}
qml/Dash/Filters/FilterOptionSelector.qmlnew file+91
@@ -0,0 +1,91 @@
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.4
18+import Ubuntu.Components 1.3
19+import Ubuntu.Components.ListItems 1.3 as ListItems
20+
21+/*! Option Selector Filter Widget. */
22+
23+FilterWidget {
24+ id: root
25+
26+ implicitHeight: expandingItem.height
27+
28+ ListItems.Expandable {
29+ id: expandingItem
30+ objectName: "expandingItem"
31+
32+ expandedHeight: collapsedHeight + column.height
33+ width: parent.width
34+ showDivider: false
35+
36+ onClicked: {
37+ expanded = !expanded;
38+ forceActiveFocus();
39+ }
40+
41+ Item {
42+ id: holder
43+ anchors.top: parent.top
44+ height: expandingItem.collapsedHeight
45+ width: parent.width
46+
47+ Label {
48+ anchors.left: parent.left
49+ anchors.right: dropDown.left
50+ anchors.verticalCenter: parent.verticalCenter
51+ text: widgetData.label || ""
52+ }
53+
54+ Image {
55+ id: dropDown
56+ height: units.gu(3)
57+ fillMode: Image.PreserveAspectFit
58+ anchors.right: parent.right
59+ anchors.verticalCenter: parent.verticalCenter
60+ source: expandingItem.expanded ? "image://theme/up" : "image://theme/down"
61+ }
62+ }
63+
64+ Column {
65+ id: column
66+ anchors.top: holder.bottom
67+ width: parent.width
68+ Repeater {
69+ model: widgetData.options
70+
71+ ListItems.Standard {
72+ text: label
73+ objectName: root.objectName + "label" + index;
74+
75+ Image {
76+ height: units.gu(3)
77+ fillMode: Image.PreserveAspectFit
78+ anchors.right: parent.right
79+ anchors.verticalCenter: parent.verticalCenter
80+ source: "image://theme/tick"
81+ visible: checked
82+ }
83+
84+ onClicked: {
85+ widgetData.options.setChecked(index, !checked);
86+ }
87+ }
88+ }
89+ }
90+ }
91+}
qml/Dash/Filters/FilterRangeInput.qmlnew file+136
@@ -0,0 +1,136 @@
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.4
18+import QtQuick.Layouts 1.1
19+import Ubuntu.Components 1.3
20+
21+/*! Range Input Filter Widget. */
22+
23+FilterWidget {
24+ id: root
25+
26+ implicitHeight: field1.height + units.gu(2)
27+
28+ function setFieldValue(field, hasValue, value) {
29+ if (hasValue) {
30+ // Need this othewise if we are on 4.5 and backspace instead of
31+ // having 4. in the text field we end up with 4 which is confusing
32+ if (field.text != value) {
33+ field.text = value;
34+ }
35+ } else {
36+ field.text = "";
37+ }
38+ }
39+
40+ Connections {
41+ target: widgetData
42+ onStartValueChanged: root.setFieldValue(field1, widgetData.hasStartValue, widgetData.startValue);
43+ onHasStartValueChanged: root.setFieldValue(field1, widgetData.hasStartValue, widgetData.startValue);
44+ onEndValueChanged: root.setFieldValue(field2, widgetData.hasEndValue, widgetData.endValue);
45+ onHasEndValueChanged: root.setFieldValue(field2, widgetData.hasEndValue, widgetData.endValue);
46+ }
47+
48+ onWidgetDataChanged: {
49+ if (widgetData) {
50+ root.setFieldValue(field1, widgetData.hasStartValue, widgetData.startValue);
51+ root.setFieldValue(field2, widgetData.hasEndValue, widgetData.endValue);
52+ } else {
53+ root.setFieldValue(field1, false, -1);
54+ root.setFieldValue(field2, false, -1);
55+ }
56+ }
57+
58+ RowLayout {
59+ anchors {
60+ fill: parent
61+ topMargin: units.gu(1)
62+ bottomMargin: units.gu(1)
63+ }
64+
65+ Item {
66+ Layout.fillWidth: true
67+ }
68+
69+ Label {
70+ text: widgetData.startPrefixLabel
71+ verticalAlignment: Text.AlignVCenter
72+ }
73+
74+ TextField {
75+ id: field1
76+ objectName: "startValueField"
77+ implicitWidth: units.gu(9)
78+ verticalAlignment: Text.AlignVCenter
79+ inputMethodHints: Qt.ImhFormattedNumbersOnly
80+ validator: DoubleValidator {
81+ notation: DoubleValidator.StandardNotation
82+ }
83+ onTextChanged: {
84+ if (text === "") widgetData.eraseStartValue();
85+ else widgetData.startValue = text;
86+ }
87+ }
88+
89+ Label {
90+ text: widgetData.startPostfixLabel
91+ verticalAlignment: Text.AlignVCenter
92+ }
93+
94+ Item {
95+ Layout.fillWidth: true
96+ }
97+
98+ Label {
99+ text: widgetData.centralLabel
100+ verticalAlignment: Text.AlignVCenter
101+ }
102+
103+ Item {
104+ Layout.fillWidth: true
105+ }
106+
107+ Label {
108+ text: widgetData.endPrefixLabel
109+ verticalAlignment: Text.AlignVCenter
110+ }
111+
112+ TextField {
113+ id: field2
114+ objectName: "endValueField"
115+ implicitWidth: units.gu(9)
116+ verticalAlignment: Text.AlignVCenter
117+ inputMethodHints: Qt.ImhFormattedNumbersOnly
118+ validator: DoubleValidator {
119+ notation: DoubleValidator.StandardNotation
120+ }
121+ onTextChanged: {
122+ if (text === "") widgetData.eraseEndValue();
123+ else widgetData.endValue = text;
124+ }
125+ }
126+
127+ Label {
128+ text: widgetData.endPostfixLabel
129+ verticalAlignment: Text.AlignVCenter
130+ }
131+
132+ Item {
133+ Layout.fillWidth: true
134+ }
135+ }
136+}
qml/Dash/Filters/FilterValueSlider.qmlnew file+87
@@ -0,0 +1,87 @@
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.4
18+import QtQuick.Layouts 1.1
19+import Ubuntu.Components 1.3
20+
21+/*! Value Slider Filter Widget. */
22+
23+FilterWidget {
24+ id: root
25+
26+ implicitHeight: childrenRect.height + units.gu(2)
27+
28+ Connections {
29+ target: widgetData
30+ // One would think that this is not needed since
31+ // we have value: widgetData.value on the slider
32+ // but it is, otherwise reset doesn't seem to work
33+ onValueChanged: {
34+ if (widgetData.value !== slider.value) {
35+ slider.value = widgetData.value;
36+ }
37+ }
38+ }
39+
40+ Slider {
41+ id: slider
42+ objectName: "slider"
43+
44+ anchors {
45+ top: parent.top
46+ topMargin: units.gu(1)
47+ left: parent.left
48+ leftMargin: units.gu(2)
49+ right: parent.right
50+ rightMargin: units.gu(2)
51+ }
52+
53+ minimumValue: widgetData.minValue
54+ maximumValue: widgetData.maxValue
55+ value: widgetData.value
56+ onValueChanged: {
57+ widgetData.value = value;
58+ }
59+ onPressedChanged: {
60+ if (pressed) forceActiveFocus();
61+ }
62+
63+ readonly property Item thumb: __internals.thumb
64+ readonly property real barMinusThumb: __internals.barMinusThumb
65+ }
66+
67+ Repeater {
68+ objectName: "repeater"
69+ model: widgetData.values
70+ delegate: Label {
71+ anchors {
72+ top: slider.bottom
73+ // The slider is too tall, so move a bit up
74+ topMargin: -units.gu(1)
75+ }
76+ text: label
77+ visible: value <= widgetData.maxValue && value >= widgetData.minValue
78+ x: {
79+ var halfThumbDifference = (width - slider.thumb.width) / 2;
80+ var result = (value - widgetData.minValue) * slider.barMinusThumb / (widgetData.maxValue - widgetData.minValue) - halfThumbDifference;
81+ result = Math.max(result, 0);
82+ result = Math.min(result, slider.width - width);
83+ return units.gu(2) + result;
84+ }
85+ }
86+ }
87+}
qml/Dash/Filters/FilterWidget.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.4
18+
19+/*! Interface for filter widgets. */
20+
21+Item {
22+ //! The widget identifier
23+ property string widgetId
24+
25+ //! Variable used to contain widget's data
26+ property var widgetData: null
27+
28+ objectName: widgetId
29+}
qml/Dash/Filters/FilterWidgetFactory.qmlnew file+80
@@ -0,0 +1,80 @@
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.4
18+import Ubuntu.Components 1.3
19+import Unity 0.2
20+
21+//! \brief This component loads the widgets based on widgetType.
22+
23+Item {
24+ id: root
25+ //! Identifier of the widget.
26+ property string widgetId: ""
27+
28+ //! Type of the widget to display.
29+ property int widgetType
30+
31+ //! Widget data, forwarded to the widget as is.
32+ property var widgetData: null
33+
34+ implicitHeight: title.height + title.anchors.topMargin + loader.height
35+
36+ Label {
37+ id: title
38+ text: widgetData ? widgetData.title : ""
39+ height: text != "" ? implicitHeight : 0
40+
41+ anchors {
42+ top: parent.top
43+ left: parent.left
44+ right: parent.right
45+
46+ topMargin: height > 0 ? units.gu(1) : 0
47+ leftMargin: units.gu(2)
48+ rightMargin: anchors.leftMargin
49+ }
50+ }
51+
52+ property alias active: loader.active
53+
54+ Loader {
55+ id: loader
56+ objectName: "loader"
57+
58+ anchors {
59+ top: title.bottom
60+ left: parent.left
61+ right: parent.right
62+ }
63+
64+ source: widgetSource
65+
66+ readonly property url widgetSource: {
67+ switch (widgetType) {
68+ case Filters.OptionSelectorFilter: return "FilterOptionSelector.qml";
69+ case Filters.RangeInputFilter: return "FilterRangeInput.qml";
70+ case Filters.ValueSliderFilter: return "FilterValueSlider.qml";
71+ default: return "";
72+ }
73+ }
74+
75+ onLoaded: {
76+ item.widgetId = Qt.binding(function() { return root.widgetId } )
77+ item.widgetData = Qt.binding(function() { return root.widgetData } )
78+ }
79+ }
80+}
qml/Dash/FiltersPopover.qmlnew file+105
@@ -0,0 +1,105 @@
1+/*
2+ * Copyright (C) 2013-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.4
18+import Ubuntu.Components 1.3
19+import Ubuntu.Components.Popups 1.3
20+import Ubuntu.Components.ListItems 1.3 as ListItems
21+import "Filters" as Filters
22+
23+Popover {
24+ id: root
25+ objectName: "filtersPopover"
26+
27+ Flickable {
28+ id: flickable
29+ anchors {
30+ top: parent.top
31+ left: parent.left
32+ right: parent.right
33+ }
34+ height: {
35+ // Popover doesn't like being 75% or bigger than the screen (counting the "empty" part on top)
36+ var posToRootParent = flickable.mapToItem(null, 0, 0).y;
37+ var threeQuartersParent = root.parent.height * 3 / 4 - posToRootParent - 1;
38+ var parentAndKeyboard = root.parent.height - posToRootParent - (Qt.inputMethod.visible ? Qt.inputMethod.keyboardRectangle.height + units.gu(3) : 0)
39+ return Math.min(parentAndKeyboard, Math.min(threeQuartersParent, column.height));
40+ }
41+ clip: true
42+ contentHeight: column.height
43+ contentWidth: width
44+
45+ Column {
46+ id: column
47+ width: parent.width
48+
49+ Item {
50+ width: parent.width
51+ height: resetLabel.height + units.gu(3)
52+
53+ Label {
54+ anchors {
55+ left: parent.left
56+ right: resetLabel.left
57+ margins: units.gu(2)
58+ verticalCenter: parent.verticalCenter
59+ }
60+ text: i18n.tr("Refine your results")
61+ }
62+ Label {
63+ id: resetLabel
64+ anchors {
65+ right: parent.right
66+ rightMargin: units.gu(2)
67+ verticalCenter: parent.verticalCenter
68+ }
69+ text: i18n.tr("Reset")
70+
71+ AbstractButton {
72+ anchors {
73+ fill: parent
74+ rightMargin: units.gu(-2)
75+ leftMargin: units.gu(-2)
76+ topMargin: units.gu(-1)
77+ bottomMargin: units.gu(-1)
78+ }
79+ onClicked: {
80+ scopeView.scope.resetFilters();
81+ }
82+ }
83+ }
84+ }
85+
86+ Repeater {
87+ id: repeater
88+ model: scopeView.scope.filters
89+
90+ delegate: Filters.FilterWidgetFactory {
91+ width: parent.width
92+
93+ widgetId: id
94+ widgetType: type
95+ widgetData: filter
96+
97+ ListItems.ThinDivider {
98+ anchors.bottom: parent.bottom
99+ visible: index != repeater.count - 1
100+ }
101+ }
102+ }
103+ }
104+ }
105+}
qml/Dash/GenericScopeView.qml+77−47
@@ -16,6 +16,8 @@
1616
1717import QtQuick 2.4
1818import Ubuntu.Components 1.3
19+import Ubuntu.Components.Popups 1.3
20+import "../Components/SearchHistoryModel"
1921import Utils 0.1
2022import Unity 0.2
2123import Dash 0.1
@@ -25,7 +27,6 @@ import "../Components/ListItems" as ListItems
2527FocusScope {
2628 id: scopeView
2729
28− readonly property bool navigationDisableParentInteractive: pageHeaderLoader.item ? pageHeaderLoader.item.bottomItem[0].disableParentInteractive : false
2930 property bool forceNonInteractive: false
3031 property var scope: null
3132 property UnitySortFilterProxyModel categories: categoryFilter
@@ -34,14 +35,15 @@ FocusScope {
3435 property bool hasBackAction: false
3536 property bool enableHeightBehaviorOnNextCreation: false
3637 property var categoryView: categoryView
37− property bool showPageHeader: true
3838 readonly property alias subPageShown: subPageLoader.subPageShown
39+ readonly property alias extraPanelShown: peExtraPanel.visible
3940 property int paginationCount: 0
4041 property int paginationIndex: 0
4142 property bool visibleToParent: false
4243 property alias pageHeaderTotallyVisible: categoryView.pageHeaderTotallyVisible
4344 property var holdingList: null
4445 property bool wasCurrentOnMoveStart: false
46+ property var filtersPopover: null
4547
4648 property var scopeStyle: ScopeStyle {
4749 style: scope ? scope.customizations : {}
@@ -68,8 +70,7 @@ FocusScope {
6870 }
6971
7072 function resetSearch() {
71− if(pageHeaderLoader.item && showPageHeader)
72− pageHeaderLoader.item.resetSearch()
73+ categoryView.pageHeader.resetSearch()
7374 }
7475
7576 property var maybePreviewResult;
@@ -120,24 +121,26 @@ FocusScope {
120121 if (!holdingList || !holdingList.moving) {
121122 wasCurrentOnMoveStart = scopeView.isCurrent;
122123 }
123− if (pageHeaderLoader.item && showPageHeader) {
124− pageHeaderLoader.item.resetSearch();
125− }
124+ categoryView.pageHeader.resetSearch();
126125 subPageLoader.closeSubPage();
126+ if (filtersPopover) {
127+ PopupUtils.close(filtersPopover)
128+ scopeView.filtersPopover = null;
129+ }
127130 }
128131
129132 Binding {
130133 target: scopeView.scope
131134 property: "searchQuery"
132− value: pageHeaderLoader.item ? pageHeaderLoader.item.searchQuery : ""
133− when: isCurrent && showPageHeader
135+ value: categoryView.pageHeader.searchQuery
136+ when: isCurrent
134137 }
135138
136139 Binding {
137− target: pageHeaderLoader.item
140+ target: categoryView.pageHeader
138141 property: "searchQuery"
139142 value: scopeView.scope ? scopeView.scope.searchQuery : ""
140− when: isCurrent && showPageHeader
143+ when: isCurrent
141144 }
142145
143146 Connections {
@@ -189,8 +192,8 @@ FocusScope {
189192 property string expandedCategoryId: ""
190193 property int runMaximizeAfterSizeChanges: 0
191194
192− readonly property bool pageHeaderTotallyVisible: scopeView.showPageHeader &&
193− ((headerItemShownHeight == 0 && categoryView.contentY <= categoryView.originY) || (headerItemShownHeight == pageHeaderLoader.item.height))
195+ readonly property bool pageHeaderTotallyVisible:
196+ ((headerItemShownHeight == 0 && categoryView.contentY <= categoryView.originY) || (headerItemShownHeight == categoryView.pageHeader.height))
194197
195198 onExpandedCategoryIdChanged: {
196199 var firstCreated = firstCreatedIndex();
@@ -615,41 +618,68 @@ FocusScope {
615618 }
616619 }
617620
618− pageHeader: scopeView.showPageHeader ? pageHeaderLoader : null
619− Loader {
620− id: pageHeaderLoader
621+ pageHeader: DashPageHeader {
622+ objectName: "scopePageHeader"
621623 width: parent.width
622− sourceComponent: scopeView.showPageHeader ? pageHeaderComponent : undefined
623− Component {
624− id: pageHeaderComponent
625− DashPageHeader {
626− objectName: "scopePageHeader"
627− width: parent.width
628− title: scopeView.scope ? scopeView.scope.name : ""
629− searchHint: scopeView.scope && scopeView.scope.searchHint || i18n.ctr("Label: Hint for dash search line edit", "Search")
630− showBackButton: scopeView.hasBackAction
631− searchEntryEnabled: true
632− settingsEnabled: scopeView.scope && scopeView.scope.settings && scopeView.scope.settings.count > 0 || false
633− favoriteEnabled: scopeView.scope && scopeView.scope.id !== "clickscope"
634− favorite: scopeView.scope && scopeView.scope.favorite
635− scopeStyle: scopeView.scopeStyle
636− paginationCount: scopeView.paginationCount
637− paginationIndex: scopeView.paginationIndex
638−
639− bottomItem: DashNavigation {
640− scope: scopeView.scope
641− anchors { left: parent.left; right: parent.right }
642− windowHeight: scopeView.height
643− windowWidth: scopeView.width
644− scopeStyle: scopeView.scopeStyle
645− }
646−
647− onBackClicked: scopeView.backClicked()
648− onSettingsClicked: subPageLoader.openSubPage("settings")
649− onFavoriteClicked: scopeView.scope.favorite = !scopeView.scope.favorite
650− onSearchTextFieldFocused: scopeView.showHeader()
624+ title: scopeView.scope ? scopeView.scope.name : ""
625+ extraPanel: peExtraPanel
626+ searchHistory: SearchHistoryModel
627+ searchHint: scopeView.scope && scopeView.scope.searchHint || i18n.ctr("Label: Hint for dash search line edit", "Search")
628+ scopeHasFilters: scopeView.scope.filters != null
629+ activeFiltersCount: scopeView.scope.activeFiltersCount
630+ showBackButton: scopeView.hasBackAction
631+ searchEntryEnabled: true
632+ settingsEnabled: scopeView.scope && scopeView.scope.settings && scopeView.scope.settings.count > 0 || false
633+ favoriteEnabled: scopeView.scope && scopeView.scope.id !== "clickscope"
634+ favorite: scopeView.scope && scopeView.scope.favorite
635+ navigationTag: scopeView.scope ? scopeView.scope.primaryNavigationTag : ""
636+ scopeStyle: scopeView.scopeStyle
637+ paginationCount: scopeView.paginationCount
638+ paginationIndex: scopeView.paginationIndex
639+
640+ onBackClicked: scopeView.backClicked()
641+ onSettingsClicked: subPageLoader.openSubPage("settings")
642+ onFavoriteClicked: scopeView.scope.favorite = !scopeView.scope.favorite
643+ onSearchTextFieldFocused: scopeView.showHeader()
644+ onClearSearch: { // keepPanelOpen
645+ var panelOpen = peExtraPanel.visible;
646+ resetSearch(keepPanelOpen);
647+ scopeView.scope.resetPrimaryNavigationTag();
648+ peExtraPanel.resetNavigation();
649+ if ((panelOpen || searchHistory.count > 0) && keepPanelOpen) {
650+ openPopup();
651651 }
652652 }
653+ onShowFiltersPopup: { // item
654+ extraPanel.visible = false;
655+ scopeView.filtersPopover = PopupUtils.open(Qt.resolvedUrl("FiltersPopover.qml"), item, { "contentWidth": scopeView.width - units.gu(2) } );
656+ }
657+ }
658+
659+ PageHeaderExtraPanel {
660+ id: peExtraPanel
661+ objectName: "peExtraPanel"
662+ width: parent.width >= units.gu(60) ? units.gu(40) : parent.width
663+ anchors {
664+ top: categoryView.pageHeader.bottom
665+ topMargin: -categoryView.pageHeader.signatureLineHeight
666+ }
667+ z: 1
668+ visible: false
669+
670+ searchHistory: SearchHistoryModel
671+ scope: scopeView.scope
672+ windowHeight: scopeView.height
673+
674+ onHistoryItemClicked: {
675+ SearchHistoryModel.addQuery(text);
676+ categoryView.pageHeader.searchQuery = text;
677+ }
678+
679+ onDashNavigationLeafClicked: {
680+ categoryView.pageHeader.closePopup();
681+ categoryView.pageHeader.unfocus();
682+ }
653683 }
654684 }
655685
@@ -658,7 +688,7 @@ FocusScope {
658688 anchors.left: parent.left
659689 anchors.right: parent.right
660690 anchors.bottom: parent.bottom
661− height: parent.height - pullToRefresh.contentY + (pageHeaderLoader.item ? pageHeaderLoader.item.bottomItem[0].height - pageHeaderLoader.item.height : 0)
691+ height: parent.height - pullToRefresh.contentY - categoryView.pageHeader.height
662692 clip: true
663693
664694 PullToRefresh {
@@ -793,7 +823,7 @@ FocusScope {
793823 open = true;
794824 }
795825
796− onOpenChanged: pageHeaderLoader.item.unfocus()
826+ onOpenChanged: categoryView.pageHeader.unfocus()
797827
798828 onVisibleChanged: if (!visible) subPage = ""
799829
qml/Dash/PageHeaderExtraPanel.qmlnew file+176
@@ -0,0 +1,176 @@
1+/*
2+ * Copyright (C) 2013-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.4
18+import Ubuntu.Components 1.3
19+import Ubuntu.Components.ListItems 1.3 as ListItems
20+import "Filters" as Filters
21+
22+Item {
23+ id: root
24+
25+ readonly property real searchesHeight: recentSearchesRepeater.count > 0 ? searchColumn.height + recentSearchesLabels.height + recentSearchesLabels.anchors.topMargin : 0
26+
27+ implicitHeight: searchesHeight + dashNavigation.implicitHeight + dashNavigation.anchors.topMargin + primaryFilter.height + primaryFilter.anchors.topMargin
28+
29+ // Set by parent
30+ property ListModel searchHistory
31+ property var scope: null
32+ property real windowHeight
33+
34+ // Used by PageHeader
35+ readonly property bool hasContents: searchHistory.count > 0 || scope && scope.hasNavigation || scope && scope.primaryNavigationFilter
36+
37+ signal historyItemClicked(string text)
38+ signal dashNavigationLeafClicked()
39+
40+ function resetNavigation() {
41+ dashNavigation.resetNavigation();
42+ }
43+
44+ Rectangle {
45+ color: "white"
46+ anchors.fill: parent
47+ }
48+
49+ ListItems.ThinDivider {
50+ anchors.top: parent.top
51+ }
52+
53+ Label {
54+ id: recentSearchesLabels
55+ text: i18n.tr("Recent Searches")
56+ visible: recentSearchesRepeater.count > 0
57+ anchors {
58+ top: parent.top
59+ left: parent.left
60+ margins: units.gu(2)
61+ topMargin: units.gu(3)
62+ }
63+ }
64+
65+ Label {
66+ text: i18n.tr("Clear All")
67+ fontSize: "small"
68+ visible: recentSearchesRepeater.count > 0
69+ anchors {
70+ top: parent.top
71+ right: parent.right
72+ margins: units.gu(2)
73+ topMargin: units.gu(3)
74+ }
75+
76+ AbstractButton {
77+ anchors.fill: parent
78+ onClicked: searchHistory.clear();
79+ }
80+ }
81+
82+ Column {
83+ id: searchColumn
84+ anchors {
85+ top: recentSearchesLabels.bottom
86+ left: parent.left
87+ right: parent.right
88+ }
89+
90+ Repeater {
91+ id: recentSearchesRepeater
92+ objectName: "recentSearchesRepeater"
93+ model: searchHistory
94+
95+ // FIXME Move to ListItem once 1556971 is fixed
96+ delegate: ListItems.Empty {
97+ anchors {
98+ left: parent.left
99+ right: parent.right
100+ leftMargin: units.gu(2)
101+ rightMargin: units.gu(2)
102+ }
103+ height: units.gu(5)
104+
105+ Icon {
106+ id: searchIcon
107+ anchors {
108+ verticalCenter: parent.verticalCenter
109+ left: parent.left
110+ }
111+ height: units.gu(1.5)
112+ width: height
113+ name: "search"
114+ }
115+
116+ Label {
117+ anchors {
118+ verticalCenter: parent.verticalCenter
119+ left: searchIcon.right
120+ leftMargin: units.gu(1)
121+ right: parent.right
122+ }
123+ text: query
124+ color: "#888888"
125+ }
126+
127+ divider.visible: index != repeater.count - 1 || (scope && scope.hasNavigation) || primaryFilter.active
128+
129+ onClicked: root.historyItemClicked(query)
130+ }
131+ }
132+ }
133+
134+ DashNavigation {
135+ id: dashNavigation
136+ scope: root.scope
137+ anchors {
138+ top: recentSearchesRepeater.count > 0 ? searchColumn.bottom : parent.top
139+ topMargin: implicitHeight && recentSearchesRepeater.count > 0 ? units.gu(2) : 0
140+ left: parent.left
141+ right: parent.right
142+ }
143+ availableHeight: windowHeight * 4 / 6 - searchesHeight
144+
145+ onLeafClicked: root.dashNavigationLeafClicked();
146+ }
147+
148+ Filters.FilterWidgetFactory {
149+ id: primaryFilter
150+ active: scope && !scope.hasNavigation
151+
152+ property var filter: active ? scope.primaryNavigationFilter : null
153+
154+ anchors {
155+ top: recentSearchesRepeater.count > 0 ? searchColumn.bottom : parent.top
156+ topMargin: active && recentSearchesRepeater.count > 0 ? units.gu(2) : 0
157+ left: parent.left
158+ right: parent.right
159+ }
160+
161+ widgetId: filter ? filter.filterId : ""
162+ widgetType: filter ? filter.filterType : -1
163+ widgetData: filter
164+ }
165+
166+ // This is outside the item
167+ Image {
168+ anchors {
169+ top: parent.bottom
170+ left: parent.left
171+ right: parent.right
172+ }
173+ fillMode: Image.Stretch
174+ source: "graphics/navigation_shadow.png"
175+ }
176+}
qml/Greeter/Greeter.qml+16−6
@@ -20,6 +20,7 @@ import GSettings 1.0
2020import Ubuntu.Components 1.3
2121import Ubuntu.SystemImage 0.1
2222import Unity.Launcher 0.1
23+import Unity.Session 0.1
2324import "../Components"
2425
2526Showable {
@@ -160,6 +161,14 @@ Showable {
160161 return false;
161162 }
162163 }
164+
165+ function checkForcedUnlock() {
166+ if (forcedUnlock && shown && loader.item) {
167+ // pretend we were just authenticated
168+ loader.item.notifyAuthenticationSucceeded();
169+ loader.item.hide();
170+ }
171+ }
163172 }
164173
165174 onLauncherOffsetChanged: {
@@ -168,12 +177,8 @@ Showable {
168177 }
169178 }
170179
171− onForcedUnlockChanged: {
172− if (forcedUnlock && shown) {
173− // pretend we were just authenticated
174− loader.item.notifyAuthenticationSucceeded();
175− }
176− }
180+ onForcedUnlockChanged: d.checkForcedUnlock()
181+ Component.onCompleted: d.checkForcedUnlock()
177182
178183 onRequiredChanged: {
179184 if (required) {
@@ -443,6 +448,11 @@ Showable {
443448 }
444449 }
445450
451+ Connections {
452+ target: DBusUnitySessionService
453+ onLockRequested: root.forceShow()
454+ }
455+
446456 Binding {
447457 target: lightDM.greeter
448458 property: "active"
qml/Launcher/Launcher.qml+2−3
@@ -28,7 +28,6 @@ FocusScope {
2828 property bool lockedVisible: false
2929 property bool available: true // can be used to disable all interactions
3030 property alias inverted: panel.inverted
31− property bool shadeBackground: true // can be used to disable background shade when launcher is visible
3231
3332 property int panelWidth: units.gu(10)
3433 property int dragAreaWidth: units.gu(1)
@@ -298,7 +297,7 @@ FocusScope {
298297 InverseMouseArea {
299298 id: closeMouseArea
300299 anchors.fill: panel
301− enabled: root.shadeBackground && root.state == "visible" && (!root.lockedVisible || panel.highlightIndex >= -1)
300+ enabled: root.state == "visible" && (!root.lockedVisible || panel.highlightIndex >= -1)
302301 visible: enabled
303302 onPressed: {
304303 panel.highlightIndex = -2
@@ -310,7 +309,7 @@ FocusScope {
310309 id: backgroundShade
311310 anchors.fill: parent
312311 color: "black"
313− opacity: root.shadeBackground && root.state == "visible" && !root.lockedVisible ? 0.6 : 0
312+ opacity: root.state == "visible" && !root.lockedVisible ? 0.6 : 0
314313
315314 Behavior on opacity { NumberAnimation { duration: UbuntuAnimation.BriskDuration } }
316315 }
qml/Launcher/LauncherPanel.qml+3−2
@@ -784,8 +784,9 @@ Rectangle {
784784 ListItem {
785785 objectName: "quickListEntry" + index
786786 selected: index === quickList.selectedIndex
787− divider.colorFrom: UbuntuColors.slate
788− divider.colorTo: UbuntuColors.slate
787+ // hide the divider after last item
788+ divider.colorFrom: index == popoverRepeater.count-1 ? quickList.color : UbuntuColors.slate
789+ divider.colorTo: index == popoverRepeater.count-1 ? quickList.color : UbuntuColors.slate
789790 highlightColor: !model.clickable ? quickList.color : undefined
790791
791792 Label {
qml/OrientedShell.qml+27−17
@@ -58,7 +58,7 @@ Rectangle {
5858 }
5959 }
6060 // to be overwritten by tests
61− property var unity8Settings: Unity8Settings {}
61+ property var unity8Settings: GSettings { schema.id: "com.canonical.Unity8" }
6262 property var oskSettings: GSettings { schema.id: "com.canonical.keyboard.maliit" }
6363
6464 property int physicalOrientation: Screen.orientation
@@ -68,11 +68,13 @@ Rectangle {
6868 InputDeviceModel {
6969 id: miceModel
7070 deviceFilter: InputInfo.Mouse
71+ property int oldCount: 0
7172 }
7273
7374 InputDeviceModel {
7475 id: touchPadModel
7576 deviceFilter: InputInfo.TouchPad
77+ property int oldCount: 0
7678 }
7779
7880 InputDeviceModel {
@@ -87,6 +89,29 @@ Rectangle {
8789 deviceFilter: InputInfo.TouchScreen
8890 }
8991
92+ readonly property int pointerInputDevices: miceModel.count + touchPadModel.count
93+ onPointerInputDevicesChanged: {
94+ console.log("Pointer input devices changed:", pointerInputDevices, "current mode:", root.unity8Settings.usageMode, "old device count", miceModel.oldCount + touchPadModel.oldCount)
95+ if (root.unity8Settings.usageMode === "Windowed") {
96+ if (pointerInputDevices === 0) {
97+ // All pointer devices have been unplugged. Move to staged.
98+ root.unity8Settings.usageMode = "Staged";
99+ }
100+ } else {
101+ var longEdgeWidth = Math.max(root.width, root.height)
102+ if (longEdgeWidth > units.gu(90)){
103+ if (pointerInputDevices > 0 && pointerInputDevices > miceModel.oldCount + touchPadModel.oldCount) {
104+ root.unity8Settings.usageMode = "Windowed";
105+ }
106+ } else {
107+ // Make sure we initialize to something sane
108+ root.unity8Settings.usageMode = "Staged";
109+ }
110+ }
111+ miceModel.oldCount = miceModel.count;
112+ touchPadModel.oldCount = touchPadModel.count;
113+ }
114+
90115 /* FIXME: This exposes the NameRole as a work arround for lp:1542224.
91116 * When QInputInfo exposes NameRole to QML, this should be removed.
92117 */
@@ -222,30 +247,15 @@ Rectangle {
222247 oskEnabled: (keyboardsModel.count === 0 && screens.count === 1) ||
223248 forceOSKEnabled
224249
225− // TODO: Factor in the connected input devices (eg: physical keyboard, mouse, touchscreen),
226− // what's the output device (eg: big TV, desktop monitor, phone display), etc.
227250 usageScenario: {
228251 if (root.unity8Settings.usageMode === "Windowed") {
229252 return "desktop";
230− } else if (root.unity8Settings.usageMode === "Staged") {
253+ } else {
231254 if (deviceConfiguration.category === "phone") {
232255 return "phone";
233256 } else {
234257 return "tablet";
235258 }
236− } else { // automatic
237− var longEdgeWidth = Math.max(root.width, root.height)
238− if (longEdgeWidth > units.gu(120)) {
239− if (keyboardsModel.count + miceModel.count + touchPadModel.count > 0) {
240− return "desktop";
241− }
242− } else if (longEdgeWidth > units.gu(90)){
243− if (miceModel.count + touchPadModel.count > 0) {
244− return "desktop";
245− }
246− }
247−
248− return deviceConfiguration.category;
249259 }
250260 }
251261
qml/Panel/IndicatorItemRow.qml+5−2
@@ -15,6 +15,7 @@
1515 */
1616
1717import QtQuick 2.4
18+import QtQuick.Window 2.2
1819import Ubuntu.Components 1.3
1920
2021Item {
@@ -183,7 +184,9 @@ Item {
183184
184185 property int ownIndex: index
185186 property bool overflow: row.width - x > overFlowWidth
186− property bool hidden: !expanded && (overflow || !indicatorVisible)
187+ property bool hidden: !expanded && (overflow || !indicatorVisible || hideSessionIndicator)
188+ // HACK for indicator-session
189+ readonly property bool hideSessionIndicator: identifier == "indicator-session" && Math.min(Screen.width, Screen.height) <= units.gu(60)
187190
188191 height: row.height
189192 expanded: root.expanded
@@ -199,7 +202,7 @@ Item {
199202 NumberAnimation { duration: UbuntuAnimation.SnapDuration; easing: UbuntuAnimation.StandardEasing }
200203 }
201204
202− width: (expanded || indicatorVisible) ? implicitWidth : 0
205+ width: ((expanded || indicatorVisible) && !hideSessionIndicator) ? implicitWidth : 0
203206
204207 Behavior on width {
205208 NumberAnimation { duration: UbuntuAnimation.SnapDuration; easing: UbuntuAnimation.StandardEasing }
qml/Panel/Indicators/MenuItemFactory.qml+16−3
@@ -15,6 +15,7 @@
1515 */
1616
1717import QtQuick 2.4
18+import QtQuick.Window 2.2
1819import Ubuntu.Settings.Menus 0.1 as Menus
1920import Ubuntu.Settings.Components 0.1
2021import QMenuModel 0.1
@@ -22,6 +23,7 @@ import Utils 0.1 as Utils
2223import Ubuntu.Components.ListItems 1.3 as ListItems
2324import Ubuntu.Components 1.3
2425import Unity.Session 0.1
26+import Unity.Platform 1.0
2527
2628Item {
2729 id: menuFactory
@@ -61,9 +63,11 @@ Item {
6163
6264 "com.canonical.indicator.calendar": calendarMenu,
6365 "com.canonical.indicator.location": timezoneMenu,
64−
65− "indicator.user-menu-item": userMenuItem,
66− "indicator.guest-menu-item": userMenuItem
66+ },
67+ "indicator-session": {
68+ "indicator.user-menu-item": Platform.isPC ? userMenuItem : null,
69+ "indicator.guest-menu-item": Platform.isPC ? userMenuItem : null,
70+ "com.canonical.indicator.switch": Math.min(Screen.width, Screen.height) > units.gu(60) ? switchMenu : null // Desktop mode switch
6771 },
6872 "indicator-messages" : {
6973 "com.canonical.indicator.button" : messagesButtonMenu
@@ -981,6 +985,15 @@ Item {
981985 }
982986
983987 function load(modelData, context) {
988+ // tweak indicator-session items
989+ if (context === "indicator-session") {
990+ if ((modelData.action === "indicator.logout" || modelData.action === "indicator.suspend" || modelData.action === "indicator.hibernate" ||
991+ modelData.action === "indicator.reboot")
992+ && !Platform.isPC) {
993+ return null; // logout, suspend and hibernate hidden on devices
994+ }
995+ }
996+
984997 if (modelData.type !== undefined && modelData.type !== "") {
985998 var component = undefined;
986999
qml/Panel/IndicatorsMenu.qml−2
@@ -36,7 +36,6 @@ Showable {
3636 readonly property bool partiallyOpened: unitProgress > 0 && unitProgress < 1.0
3737 readonly property bool fullyClosed: unitProgress == 0
3838 property bool enableHint: true
39− property bool contentEnabled: true
4039 property bool showOnClick: true
4140 property color panelColor: theme.palette.normal.background
4241
@@ -85,7 +84,6 @@ Showable {
8584 height: openedHeight - bar.height - handle.height
8685 indicatorsModel: root.indicatorsModel
8786 visible: root.unitProgress > 0
88− enabled: contentEnabled
8987 currentMenuIndex: bar.currentItemIndex
9088 }
9189
qml/Panel/MenuContent.qml+1
@@ -27,6 +27,7 @@ Rectangle {
2727
2828 property QtObject indicatorsModel: null
2929 property int currentMenuIndex: -1
30+ color: theme.palette.normal.background
3031
3132 width: units.gu(40)
3233 height: units.gu(42)
qml/Shell.qml+84−53
@@ -24,7 +24,7 @@ import Ubuntu.Gestures 0.1
2424import Ubuntu.Telephony 0.1 as Telephony
2525import Unity.Connectivity 0.1
2626import Unity.Launcher 0.1
27−import GlobalShortcut 1.0 // has to be before Utils, because of WindowKeysFilter
27+import GlobalShortcut 1.0 // has to be before Utils, because of WindowInputFilter
2828import GSettings 1.0
2929import Utils 0.1
3030import Powerd 0.1
@@ -102,6 +102,8 @@ Item {
102102 // internal props from here onwards
103103 readonly property var mainApp:
104104 applicationsDisplayLoader.item ? applicationsDisplayLoader.item.mainApp : null
105+ readonly property var mainAppWindow:
106+ applicationsDisplayLoader.item ? applicationsDisplayLoader.item.mainAppWindow : null
105107
106108 // Disable everything while greeter is waiting, so that the user can't swipe
107109 // the greeter or launcher until we know whether the session is locked.
@@ -120,9 +122,7 @@ Item {
120122 if (ApplicationManager.findApplication(appId)) {
121123 ApplicationManager.requestFocusApplication(appId);
122124 } else {
123− var execFlags = shell.usageScenario === "phone" ? ApplicationManager.ForceMainStage
124− : ApplicationManager.NoFlag;
125− ApplicationManager.startApplication(appId, execFlags);
125+ ApplicationManager.startApplication(appId);
126126 }
127127 }
128128
@@ -169,12 +169,13 @@ Item {
169169 }
170170
171171 GlobalShortcut {
172− // dummy shortcut to force creation of GlobalShortcutRegistry before WindowKeyFilter
172+ // dummy shortcut to force creation of GlobalShortcutRegistry before WindowInputFilter
173173 }
174174
175− WindowKeysFilter {
176− Keys.onPressed: physicalKeysMapper.onKeyPressed(event, currentEventTimestamp);
177− Keys.onReleased: physicalKeysMapper.onKeyReleased(event, currentEventTimestamp);
175+ WindowInputFilter {
176+ id: inputFilter
177+ Keys.onPressed: physicalKeysMapper.onKeyPressed(event, lastInputTimestamp);
178+ Keys.onReleased: physicalKeysMapper.onKeyReleased(event, lastInputTimestamp);
178179 }
179180
180181 WindowInputMonitor {
@@ -207,7 +208,7 @@ Item {
207208 onFocusedApplicationIdChanged: {
208209 var appId = ApplicationManager.focusedApplicationId;
209210
210− if (tutorial.running && appId != "" && appId != "unity8-dash") {
211+ if (wizard.active && appId != "" && appId != "unity8-dash") {
211212 // If this happens on first boot, we may be in edge
212213 // tutorial or wizard while receiving a call. But a call
213214 // is more important than wizard so just bail out of those.
@@ -248,7 +249,7 @@ Item {
248249 property string usageScenario: shell.usageScenario === "phone" || greeter.hasLockedApp
249250 ? "phone"
250251 : shell.usageScenario
251− source: {
252+ readonly property string qmlComponent: {
252253 if(shell.mode === "greeter") {
253254 return "Stages/ShimStage.qml"
254255 } else if (applicationsDisplayLoader.usageScenario === "phone") {
@@ -259,9 +260,12 @@ Item {
259260 return "Stages/DesktopStage.qml";
260261 }
261262 }
263+ onQmlComponentChanged: {
264+ if (item) item.stageAboutToBeUnloaded();
265+ source = qmlComponent;
266+ }
262267
263− property bool interactive: tutorial.spreadEnabled
264− && (!greeter || !greeter.shown)
268+ property bool interactive: (!greeter || !greeter.shown)
265269 && panel.indicators.fullyClosed
266270 && launcher.progress == 0
267271 && !notifications.useModal
@@ -355,30 +359,6 @@ Item {
355359 value: shell.usageScenario == "desktop" && !settings.autohideLauncher ? launcher.panelWidth: 0
356360 }
357361 }
358−
359− Tutorial {
360− id: tutorial
361− objectName: "tutorial"
362− anchors.fill: parent
363−
364− // EdgeDragAreas don't work with mice. So to avoid trapping the user,
365− // we skip the tutorial on the Desktop to avoid using them. The
366− // Desktop doesn't use the same spread design anyway. The tutorial is
367− // all a bit of a placeholder on non-phone form factors right now.
368− // When the design team gives us more guidance, we can do something
369− // more clever here.
370− active: usageScenario != "desktop" && AccountsService.demoEdges
371−
372− paused: lightDM.greeter.active
373− launcher: launcher
374− panel: panel
375− edgeSize: shell.edgeSize
376−
377− onFinished: {
378− AccountsService.demoEdges = false;
379− active = false; // for immediate response / if AS is having problems
380− }
381− }
382362 }
383363
384364 InputMethod {
@@ -389,7 +369,7 @@ Item {
389369 topMargin: panel.panelHeight
390370 leftMargin: launcher.lockedVisible ? launcher.panelWidth : 0
391371 }
392− z: notifications.useModal || panel.indicators.shown || wizard.active ? overlay.z + 1 : overlay.z - 1
372+ z: notifications.useModal || panel.indicators.shown || wizard.active || tutorial.running ? overlay.z + 1 : overlay.z - 1
393373 }
394374
395375 Connections {
@@ -420,7 +400,7 @@ Item {
420400 hides: [launcher, panel.indicators]
421401 tabletMode: shell.usageScenario != "phone"
422402 launcherOffset: launcher.progress
423− forcedUnlock: tutorial.running
403+ forcedUnlock: wizard.active
424404 background: wallpaperResolver.background
425405
426406 // avoid overlapping with Launcher's edge drag area
@@ -477,7 +457,7 @@ Item {
477457
478458 onStatusChanged: {
479459 if (Powerd.status === Powerd.Off && reason !== Powerd.Proximity &&
480− !callManager.hasCalls && !tutorial.running) {
460+ !callManager.hasCalls && !wizard.active) {
481461 // We don't want to simply call greeter.showNow() here, because
482462 // that will take too long. Qt will delay button event
483463 // handling until the greeter is done loading and may think the
@@ -493,10 +473,6 @@ Item {
493473 }
494474
495475 function showHome() {
496− if (tutorial.running) {
497− return
498− }
499−
500476 greeter.notifyAboutToFocusApp("unity8-dash");
501477
502478 var animate = !lightDM.greeter.active && !stages.shown
@@ -530,7 +506,6 @@ Item {
530506 available: tutorial.panelEnabled
531507 && ((!greeter || !greeter.locked) || AccountsService.enableIndicatorsWhileLocked)
532508 && (!greeter || !greeter.hasLockedApp)
533− contentEnabled: tutorial.panelContentEnabled
534509 width: parent.width > units.gu(60) ? units.gu(40) : parent.width
535510
536511 minimizedPanelHeight: units.gu(3)
@@ -547,9 +522,7 @@ Item {
547522 greeterShown: greeter.shown
548523 }
549524
550− readonly property bool topmostApplicationIsFullscreen:
551− ApplicationManager.focusedApplicationId &&
552− ApplicationManager.findApplication(ApplicationManager.focusedApplicationId).fullscreen
525+ readonly property bool topmostApplicationIsFullscreen: mainApp && mainApp.fullscreen
553526
554527 fullscreenMode: (topmostApplicationIsFullscreen && !lightDM.greeter.active && launcher.progress == 0)
555528 || greeter.hasLockedApp
@@ -571,7 +544,6 @@ Item {
571544 && (!greeter.locked || AccountsService.enableLauncherWhileLocked)
572545 && !greeter.hasLockedApp
573546 inverted: shell.usageScenario !== "desktop"
574− shadeBackground: !tutorial.running
575547 superPressed: physicalKeysMapper.superPressed
576548 superTabPressed: physicalKeysMapper.superTabPressed
577549 panelWidth: units.gu(settings.launcherWidth)
@@ -585,10 +557,8 @@ Item {
585557 }
586558 }
587559 onLauncherApplicationSelected: {
588− if (!tutorial.running) {
589− greeter.notifyAboutToFocusApp(appId);
590− shell.activateApplication(appId)
591− }
560+ greeter.notifyAboutToFocusApp(appId);
561+ shell.activateApplication(appId);
592562 }
593563 onShownChanged: {
594564 if (shown) {
@@ -628,11 +598,24 @@ Item {
628598 }
629599 }
630600
601+ Tutorial {
602+ id: tutorial
603+ objectName: "tutorial"
604+ anchors.fill: parent
605+
606+ paused: callManager.hasCalls || greeter.shown
607+ keyboardVisible: inputMethod.state === "shown"
608+ usageScenario: shell.usageScenario
609+ lastInputTimestamp: inputFilter.lastInputTimestamp
610+ launcher: launcher
611+ panel: panel
612+ stage: applicationsDisplayLoader.item
613+ }
614+
631615 Wizard {
632616 id: wizard
633617 objectName: "wizard"
634618 anchors.fill: parent
635− background: wallpaperResolver.background
636619
637620 function unlockWhenDoneWithWizard() {
638621 if (!active) {
@@ -738,6 +721,54 @@ Item {
738721 onMouseMoved: { cursor.opacity = 1; }
739722 }
740723
724+ // keymap switching
725+ GlobalShortcut {
726+ shortcut: Qt.MetaModifier|Qt.Key_Space
727+ onTriggered: keymapPriv.nextKeymap()
728+ active: keymapPriv.keymapCount > 1
729+ }
730+
731+ GlobalShortcut {
732+ shortcut: Qt.MetaModifier|Qt.ShiftModifier|Qt.Key_Space
733+ onTriggered: keymapPriv.previousKeymap()
734+ active: keymapPriv.keymapCount > 1
735+ }
736+
737+ QtObject {
738+ id: keymapPriv
739+
740+ readonly property var keymaps: AccountsService.keymaps
741+ readonly property int keymapCount: keymaps.length
742+ property int currentKeymapIndex: 0 // the new one that we're setting
743+ onCurrentKeymapIndexChanged: switchToKeymap();
744+
745+ function nextKeymap() {
746+ var nextIndex = 0;
747+
748+ if (currentKeymapIndex !== -1 && currentKeymapIndex < keymapCount - 1) {
749+ nextIndex = currentKeymapIndex + 1;
750+ }
751+ currentKeymapIndex = nextIndex;
752+ }
753+
754+ function previousKeymap() {
755+ var prevIndex = keymapCount - 1;
756+
757+ if (currentKeymapIndex > 0) {
758+ prevIndex = currentKeymapIndex - 1;
759+ }
760+ currentKeymapIndex = prevIndex;
761+ }
762+
763+ function switchToKeymap() {
764+ if (mainAppWindow) {
765+ mainAppWindow.switchToKeymap(keymaps[currentKeymapIndex]);
766+ }
767+ }
768+ }
769+
770+ onMainAppWindowChanged: keymapPriv.switchToKeymap()
771+
741772 Rectangle {
742773 id: shutdownFadeOutRectangle
743774 z: cursor.z + 1
qml/Stages/AbstractStage.qml+5−1
@@ -28,6 +28,7 @@ Rectangle {
2828 property url background
2929 property bool beingResized
3030 property int dragAreaWidth
31+ property real dragProgress // How far left the stage has been dragged, used externally by tutorial code
3132 property bool interactive
3233 property real inverseProgress // This is the progress for left edge drags, in pixels.
3334 property bool keepDashRunning: true
@@ -44,13 +45,16 @@ Rectangle {
4445
4546 // To be read from outside
4647 property var mainApp: null
47− property int mainAppWindowOrientationAngle
48+ property var mainAppWindow: null
49+ property int mainAppWindowOrientationAngle: 0
4850 property bool orientationChangesEnabled
4951 property int supportedOrientations: Qt.PortraitOrientation
5052 | Qt.LandscapeOrientation
5153 | Qt.InvertedPortraitOrientation
5254 | Qt.InvertedLandscapeOrientation
5355
56+ signal stageAboutToBeUnloaded
57+
5458 // Shared code for use in stage implementations
5559 GSettings {
5660 id: lifecycleExceptions
qml/Stages/ApplicationWindow.qml+7−3
@@ -24,12 +24,14 @@ FocusScope {
2424 implicitHeight: sessionContainer.implicitHeight
2525
2626 // to be read from outside
27− readonly property bool fullscreen: application ? application.fullscreen : false
2827 property alias interactive: sessionContainer.interactive
2928 property bool orientationChangesEnabled: d.supportsSurfaceResize ? d.surfaceOldEnoughToBeResized : true
3029 readonly property string title: sessionContainer.surface && sessionContainer.surface.name !== "" ?
3130 sessionContainer.surface.name : d.name
3231
32+ // overridable from outside
33+ property bool fullscreen: application ? application.fullscreen : false
34+
3335 // to be set from outside
3436 property QtObject application
3537 property int surfaceOrientationAngle
@@ -37,6 +39,10 @@ FocusScope {
3739 property int requestedWidth: -1
3840 property int requestedHeight: -1
3941
42+ function switchToKeymap(keymap) {
43+ sessionContainer.surfaceContainer.switchToKeymap(keymap);
44+ }
45+
4046 readonly property int minimumWidth: sessionContainer.surface ? sessionContainer.surface.minimumWidth : 0
4147 readonly property int minimumHeight: sessionContainer.surface ? sessionContainer.surface.minimumHeight : 0
4248 readonly property int maximumWidth: sessionContainer.surface ? sessionContainer.surface.maximumWidth : 0
@@ -58,7 +64,6 @@ FocusScope {
5864 readonly property color splashColor: root.application ? root.application.splashColor : "#00000000"
5965 readonly property color splashColorHeader: root.application ? root.application.splashColorHeader : "#00000000"
6066 readonly property color splashColorFooter: root.application ? root.application.splashColorFooter : "#00000000"
61− readonly property url defaultScreenshot: (root.application && root.application.defaultScreenshot !== undefined) ? root.application.defaultScreenshot : ""
6267
6368 // Whether the Application had a surface before but lost it.
6469 property bool hadSurface: sessionContainer.surfaceContainer.hadSurface
@@ -110,7 +115,6 @@ FocusScope {
110115 Image {
111116 id: screenshotImage
112117 objectName: "screenshotImage"
113− source: d.defaultScreenshot
114118 anchors.fill: parent
115119 antialiasing: !root.interactive
116120
qml/Stages/DesktopStage.qml+76−13
@@ -40,6 +40,8 @@ AbstractStage {
4040 ? ApplicationManager.findApplication(ApplicationManager.focusedApplicationId)
4141 : null
4242
43+ mainAppWindow: priv.focusedAppDelegate ? priv.focusedAppDelegate.appWindow : null
44+
4345 // application windows never rotate independently
4446 mainAppWindowOrientationAngle: shellOrientationAngle
4547
@@ -255,8 +257,8 @@ AbstractStage {
255257 focus: appId === priv.focusedAppId
256258 width: decoratedWindow.width
257259 height: decoratedWindow.height
258− property alias requestedWidth: decoratedWindow.requestedWidth
259− property alias requestedHeight: decoratedWindow.requestedHeight
260+ property int requestedWidth: -1
261+ property int requestedHeight: -1
260262 property alias minimumWidth: decoratedWindow.minimumWidth
261263 property alias minimumHeight: decoratedWindow.minimumHeight
262264 property alias maximumWidth: decoratedWindow.maximumWidth
@@ -284,6 +286,8 @@ AbstractStage {
284286 property bool visuallyMaximized: false
285287 property bool visuallyMinimized: false
286288
289+ readonly property alias appWindow: decoratedWindow.window
290+
287291 onFocusChanged: {
288292 if (focus && ApplicationManager.focusedApplicationId !== appId) {
289293 ApplicationManager.focusApplication(appId);
@@ -369,8 +373,10 @@ AbstractStage {
369373 name: "fullscreen"; when: decoratedWindow.fullscreen
370374 PropertyChanges {
371375 target: appDelegate;
372− x: 0; y: -PanelState.panelHeight
373− requestedWidth: appContainer.width; requestedHeight: appContainer.height;
376+ x: 0;
377+ y: -PanelState.panelHeight
378+ requestedWidth: appContainer.width;
379+ requestedHeight: appContainer.height;
374380 }
375381 },
376382 State {
@@ -387,21 +393,42 @@ AbstractStage {
387393 name: "maximized"; when: appDelegate.maximized && !appDelegate.minimized
388394 PropertyChanges {
389395 target: appDelegate;
390− x: root.leftMargin; y: 0;
391− requestedWidth: appContainer.width - root.leftMargin; requestedHeight: appContainer.height;
396+ x: root.leftMargin;
397+ y: 0;
392398 visuallyMinimized: false;
393399 visuallyMaximized: true
394400 }
401+ PropertyChanges {
402+ target: decoratedWindow
403+ requestedWidth: appContainer.width - root.leftMargin;
404+ requestedHeight: appContainer.height;
405+ }
395406 },
396407 State {
397408 name: "maximizedLeft"; when: appDelegate.maximizedLeft && !appDelegate.minimized
398− PropertyChanges { target: appDelegate; x: root.leftMargin; y: PanelState.panelHeight;
399− requestedWidth: (appContainer.width - root.leftMargin)/2; requestedHeight: appContainer.height - PanelState.panelHeight }
409+ PropertyChanges {
410+ target: appDelegate
411+ x: root.leftMargin
412+ y: PanelState.panelHeight
413+ }
414+ PropertyChanges {
415+ target: decoratedWindow
416+ requestedWidth: (appContainer.width - root.leftMargin)/2
417+ requestedHeight: appContainer.height - PanelState.panelHeight
418+ }
400419 },
401420 State {
402421 name: "maximizedRight"; when: appDelegate.maximizedRight && !appDelegate.minimized
403− PropertyChanges { target: appDelegate; x: (appContainer.width + root.leftMargin)/2; y: PanelState.panelHeight;
404− requestedWidth: (appContainer.width - root.leftMargin)/2; requestedHeight: appContainer.height - PanelState.panelHeight }
422+ PropertyChanges {
423+ target: appDelegate;
424+ x: (appContainer.width + root.leftMargin)/2
425+ y: PanelState.panelHeight
426+ }
427+ PropertyChanges {
428+ target: decoratedWindow
429+ requestedWidth: (appContainer.width - root.leftMargin)/2
430+ requestedHeight: appContainer.height - PanelState.panelHeight
431+ }
405432 },
406433 State {
407434 name: "minimized"; when: appDelegate.minimized
@@ -421,13 +448,17 @@ AbstractStage {
421448 enabled: appDelegate.animationsEnabled
422449 PropertyAction { target: appDelegate; properties: "visuallyMinimized,visuallyMaximized" }
423450 UbuntuNumberAnimation { target: appDelegate; properties: "x,y,opacity,requestedWidth,requestedHeight,scale"; duration: UbuntuAnimation.FastDuration }
451+ UbuntuNumberAnimation { target: decoratedWindow; properties: "requestedWidth,requestedHeight"; duration: UbuntuAnimation.FastDuration }
424452 },
425453 Transition {
426454 to: "minimized"
427455 enabled: appDelegate.animationsEnabled
428456 PropertyAction { target: appDelegate; property: "visuallyMaximized" }
429457 SequentialAnimation {
430− UbuntuNumberAnimation { target: appDelegate; properties: "x,y,opacity,requestedWidth,requestedHeight,scale"; duration: UbuntuAnimation.FastDuration }
458+ ParallelAnimation {
459+ UbuntuNumberAnimation { target: appDelegate; properties: "x,y,opacity,scale"; duration: UbuntuAnimation.FastDuration }
460+ UbuntuNumberAnimation { target: decoratedWindow; properties: "requestedWidth,requestedHeight"; duration: UbuntuAnimation.FastDuration }
461+ }
431462 PropertyAction { target: appDelegate; property: "visuallyMinimized" }
432463 ScriptAction {
433464 script: {
@@ -443,7 +474,10 @@ AbstractStage {
443474 enabled: appDelegate.animationsEnabled
444475 PropertyAction { target: appDelegate; property: "visuallyMinimized" }
445476 SequentialAnimation {
446− UbuntuNumberAnimation { target: appDelegate; properties: "x,y,opacity,requestedWidth,requestedHeight,scale"; duration: UbuntuAnimation.FastDuration }
477+ ParallelAnimation {
478+ UbuntuNumberAnimation { target: appDelegate; properties: "x,y,opacity,scale"; duration: UbuntuAnimation.FastDuration }
479+ UbuntuNumberAnimation { target: decoratedWindow; properties: "requestedWidth,requestedHeight"; duration: UbuntuAnimation.FastDuration }
480+ }
447481 PropertyAction { target: appDelegate; property: "visuallyMaximized" }
448482 }
449483 }
@@ -458,6 +492,7 @@ AbstractStage {
458492 }
459493
460494 WindowResizeArea {
495+ id: resizeArea
461496 objectName: "windowResizeArea"
462497 target: appDelegate
463498 minWidth: units.gu(10)
@@ -469,6 +504,25 @@ AbstractStage {
469504 leftMargin: root.leftMargin
470505
471506 onPressed: { ApplicationManager.focusApplication(model.appId) }
507+
508+ Component.onCompleted: {
509+ loadWindowState();
510+ }
511+
512+ property bool saveStateOnDestruction: true
513+ Connections {
514+ target: root
515+ onStageAboutToBeUnloaded: {
516+ resizeArea.saveWindowState();
517+ resizeArea.saveStateOnDestruction = false;
518+ fullscreenPolicy.active = false;
519+ }
520+ }
521+ Component.onDestruction: {
522+ if (saveStateOnDestruction) {
523+ saveWindowState();
524+ }
525+ }
472526 }
473527
474528 DecoratedWindow {
@@ -480,6 +534,9 @@ AbstractStage {
480534 active: ApplicationManager.focusedApplicationId === model.appId
481535 focus: true
482536
537+ requestedWidth: appDelegate.requestedWidth
538+ requestedHeight: appDelegate.requestedHeight
539+
483540 onClose: ApplicationManager.stopApplication(model.appId)
484541 onMaximize: appDelegate.maximized || appDelegate.maximizedLeft || appDelegate.maximizedRight
485542 ? appDelegate.restoreFromMaximized() : appDelegate.maximize()
@@ -500,7 +557,7 @@ AbstractStage {
500557
501558 Rectangle {
502559 id: mask
503− radius: appDelegate.maximized ? 0 : units.gu(.7) // disable rounded corners when maximized/fullscreen
560+ radius: appDelegate.maximized ? 0 : units.gu(.7) // disable rounded corners when maximized
504561 anchors.fill: decoratedWindow
505562 }
506563
@@ -543,6 +600,12 @@ AbstractStage {
543600 }
544601 "
545602 }
603+
604+ WindowedFullscreenPolicy {
605+ id: fullscreenPolicy
606+ active: true
607+ application: decoratedWindow.application
608+ }
546609 }
547610 }
548611 }
qml/Stages/PhoneStage.qml+15−6
@@ -111,6 +111,8 @@ AbstractStage {
111111 ? applicationManager.findApplication(applicationManager.focusedApplicationId)
112112 : null
113113
114+ mainAppWindow: priv.focusedAppDelegate ? priv.focusedAppDelegate.appWindow : null
115+
114116 orientationChangesEnabled: priv.focusedAppOrientationChangesEnabled
115117 && !priv.focusedAppDelegateIsDislocated
116118 && !(priv.focusedAppDelegate && priv.focusedAppDelegate.xBehavior.running)
@@ -120,14 +122,11 @@ AbstractStage {
120122 : (Qt.PortraitOrientation | Qt.LandscapeOrientation
121123 | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation)
122124
123− // How far left the stage has been dragged
124− readonly property real dragProgress: spreadRepeater.count > 0 ? -spreadRepeater.itemAt(0).xTranslate : 0
125+ // How far left the stage has been dragged, used externally by tutorial code
126+ dragProgress: spreadRepeater.count > 0 ? spreadRepeater.itemAt(0).animatedProgress : 0
125127
126128 readonly property alias dragging: spreadDragArea.dragging
127129
128− // Only used by the tutorial right now, when it is teasing the right edge
129− property real dragAreaOverlap
130−
131130 signal opened()
132131
133132 function select(appId) {
@@ -629,6 +628,16 @@ AbstractStage {
629628 property: "focusedAppOrientationChangesEnabled"
630629 value: orientationChangesEnabled
631630 }
631+
632+ StagedFullscreenPolicy {
633+ id: fullscreenPolicy
634+ application: appDelegate.application
635+ }
636+
637+ Connections {
638+ target: root
639+ onStageAboutToBeUnloaded: fullscreenPolicy.active = false
640+ }
632641 }
633642 }
634643 }
@@ -647,7 +656,7 @@ AbstractStage {
647656 direction: Direction.Leftwards
648657 enabled: (spreadView.phase != 2 && root.spreadEnabled) || dragging
649658
650− anchors { top: parent.top; right: parent.right; bottom: parent.bottom; rightMargin: -root.dragAreaOverlap }
659+ anchors { top: parent.top; right: parent.right; bottom: parent.bottom; }
651660 width: root.dragAreaWidth
652661
653662 property var gesturePoints: new Array()
qml/Stages/SideStage.qmlnew file+118
@@ -0,0 +1,118 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import QtQuick.Layouts 1.1
19+import Ubuntu.Components 1.3
20+import Ubuntu.Gestures 0.1
21+import "../Components"
22+
23+Showable {
24+ id: root
25+ property bool showHint: true
26+ property int panelWidth: units.gu(40)
27+ readonly property alias dragging: hideSideStageDragArea.dragging
28+ readonly property real progress: width / panelWidth
29+
30+ width: 0
31+ shown: false
32+
33+ Item {
34+ id: sideStageDragHandle
35+ anchors {
36+ right: root.left
37+ top: root.top
38+ bottom: root.bottom
39+ }
40+ width: units.gu(2)
41+
42+ opacity: root.shown ? 1 : 0
43+ Behavior on opacity { UbuntuNumberAnimation {} }
44+
45+ Image {
46+ anchors.centerIn: parent
47+ width: hideSideStageDragArea.pressed ? parent.width * 2 : parent.width
48+ height: parent.height
49+ source: "graphics/sidestage_handle@20.png"
50+ Behavior on width { UbuntuNumberAnimation {} }
51+ }
52+ }
53+
54+ Rectangle {
55+ anchors.fill: parent
56+ color: Qt.rgba(0,0,0,0.95)
57+ }
58+
59+ Column {
60+ anchors.verticalCenter: parent.verticalCenter
61+ width: panelWidth - units.gu(6)
62+ x: panelWidth/2 - width/2
63+ spacing: units.gu(3)
64+ opacity: 0.8
65+ visible: showHint
66+
67+ Icon {
68+ width: units.gu(30)
69+ anchors.horizontalCenter: parent.horizontalCenter
70+ source: "graphics/sidestage_drag.svg"
71+ color: enabled ? Qt.rgba(1,1,1,1) : Qt.rgba(1,0,0,1)
72+ keyColor: Qt.rgba(1,1,1,1)
73+ }
74+
75+ Label {
76+ text: i18n.tr("Drag using 3 fingers any application from one window to the other")
77+ width: parent.width
78+ wrapMode: Text.WordWrap
79+ color: enabled ? Qt.rgba(1,1,1,1) : Qt.rgba(1,0,0,1)
80+ }
81+ }
82+
83+ showAnimation: NumberAnimation {
84+ property: "width"
85+ to: panelWidth
86+ duration: UbuntuAnimation.BriskDuration
87+ easing.type: Easing.OutCubic
88+ }
89+
90+ hideAnimation: NumberAnimation {
91+ property: "width"
92+ to: 0
93+ duration: UbuntuAnimation.BriskDuration
94+ easing.type: Easing.OutCubic
95+ }
96+
97+ DragHandle {
98+ id: hideSideStageDragArea
99+ objectName: "hideSideStageDragArea"
100+
101+ direction: Direction.Leftwards
102+ rotation: 180
103+ enabled: root.shown
104+ anchors.right: root.left
105+ width: sideStageDragHandle.width
106+ height: root.height
107+ stretch: true
108+
109+ immediateRecognition: true
110+ maxTotalDragDistance: panelWidth
111+ autoCompleteDragThreshold: panelWidth / 2
112+ }
113+
114+ // SideStage mouse event eater
115+ MouseArea {
116+ anchors.fill: parent
117+ }
118+}
qml/Stages/SpreadDelegate.qml+11−1
@@ -32,6 +32,12 @@ FocusScope {
3232 readonly property alias appWindowOrientationAngle: appWindowWithShadow.orientationAngle
3333 readonly property alias appWindowRotation: appWindowWithShadow.rotation
3434 readonly property alias orientationChangesEnabled: appWindow.orientationChangesEnabled
35+ property int supportedOrientations: application ? application.supportedOrientations :
36+ Qt.PortraitOrientation
37+ | Qt.LandscapeOrientation
38+ | Qt.InvertedPortraitOrientation
39+ | Qt.InvertedLandscapeOrientation
40+ readonly property alias appWindow: appWindow
3541
3642 // to be set from outside
3743 property bool interactive: true
@@ -45,6 +51,9 @@ FocusScope {
4551 property QtObject orientations
4652 property bool highlightShown: false
4753
54+ // overrideable from outside
55+ property alias fullscreen: appWindow.fullscreen
56+
4857 function matchShellOrientation() {
4958 if (!root.application)
5059 return;
@@ -154,7 +163,8 @@ FocusScope {
154163 if (!root.application || root.application.rotatesWindowContents) {
155164 return 0;
156165 }
157− var supportedOrientations = root.application.supportedOrientations;
166+
167+ var supportedOrientations = root.supportedOrientations;
158168
159169 if (supportedOrientations === Qt.PrimaryOrientation) {
160170 supportedOrientations = root.orientations.primary;
qml/Stages/StagedFullscreenPolicy.qmlnew file+58
@@ -0,0 +1,58 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import Unity.Application 0.1
19+
20+// This component will change the state of the surface based on the surface
21+// state and shell chrome.
22+//
23+// Chrome changed to LowChrome -> server sets client window state to "fullscreen"
24+// Chrome changed to NormalChrome -> server sets client window to "restored" state.
25+// Chrome set and state change to restored -> server RESETS client window state to "fullscreen"
26+// Chrome not set and state change to restored -> client window stays "restored"
27+// Chrome not set and state change to fulscreen -> client window stays "fullscreen"
28+QtObject {
29+ property bool active: true
30+ property QtObject application: null
31+
32+ readonly property var lastSurface: application && application.session ?
33+ application.session.lastSurface : null
34+ onLastSurfaceChanged: {
35+ if (!active || !lastSurface) return;
36+ if (lastSurface.shellChrome === Mir.LowChrome) {
37+ lastSurface.state = Mir.FullscreenState;
38+ }
39+ }
40+
41+ property var _connections: Connections {
42+ target: lastSurface
43+ onShellChromeChanged: {
44+ if (!active || !lastSurface) return;
45+ if (lastSurface.shellChrome === Mir.LowChrome) {
46+ lastSurface.state = Mir.FullscreenState;
47+ } else {
48+ lastSurface.state = Mir.RestoredState;
49+ }
50+ }
51+ onStateChanged: {
52+ if (!active) return;
53+ if (lastSurface.state === Mir.RestoredState && lastSurface.shellChrome === Mir.LowChrome) {
54+ lastSurface.state = Mir.FullscreenState;
55+ }
56+ }
57+ }
58+}
qml/Stages/SurfaceContainer.qml+14
@@ -19,6 +19,7 @@ import Ubuntu.Components 1.3
1919import Ubuntu.Gestures 0.1 // For TouchGate
2020import Utils 0.1 // for InputWatcher
2121import Unity.Application 0.1 // for MirSurfaceItem
22+import AccountsService 0.1
2223
2324FocusScope {
2425 id: root
@@ -34,10 +35,22 @@ FocusScope {
3435 property int requestedWidth: -1
3536 property int requestedHeight: -1
3637
38+ property string savedKeymap: AccountsService.keymaps[0] // start with the user default
39+
3740 onSurfaceChanged: {
3841 if (surface) {
3942 surfaceItem.surface = surface;
4043 root.hadSurface = false;
44+ switchToKeymap(savedKeymap);
45+ }
46+ }
47+
48+ function switchToKeymap(keymap) {
49+ var finalKeymap = keymap.split("+");
50+ savedKeymap = keymap; // save the keymap in case the surface changes later
51+
52+ if (surface) {
53+ surface.setKeymap(finalKeymap[0], finalKeymap[1] || "");
4154 }
4255 }
4356
@@ -118,6 +131,7 @@ FocusScope {
118131
119132
120133 TouchGate {
134+ objectName: "touchGate-"+name
121135 targetItem: surfaceItem
122136 anchors.fill: root
123137 enabled: surfaceItem.enabled
qml/Stages/TabletSideStageTouchGesture.qmlnew file+154
@@ -0,0 +1,154 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import Ubuntu.Gestures 0.1
19+
20+TouchGestureArea {
21+ id: root
22+ minimumTouchPoints: 3
23+ maximumTouchPoints: 3
24+
25+ property bool enableDrag: true
26+ property Component dragComponent
27+ property var dragComponentProperties: undefined
28+
29+ readonly property bool recognisedPress: status == TouchGestureArea.Recognized &&
30+ touchPoints.length >= minimumTouchPoints &&
31+ touchPoints.length <= maximumTouchPoints
32+ readonly property bool recognisedDrag: priv.wasRecognisedPress && dragging
33+
34+ signal pressed(int x, int y)
35+ signal clicked
36+ signal dragStarted
37+ signal dropped
38+ signal cancelled
39+
40+ onEnabledChanged: {
41+ if (!enabled) {
42+ if (priv.dragObject) root.cancelled();
43+ priv.wasRecognisedDrag = false;
44+ priv.wasRecognisedPress = false;
45+ }
46+ }
47+
48+ onRecognisedPressChanged: {
49+ if (recognisedPress) {
50+ // get the app at the center of the gesture
51+ var centerX = 0;
52+ var centerY = 0;
53+ for (var i = 0; i < touchPoints.length; i++) {
54+ centerX += touchPoints[i].x;
55+ centerY += touchPoints[i].y;
56+ }
57+ centerX = centerX/touchPoints.length;
58+ centerY = centerY/touchPoints.length;
59+
60+ pressed(centerX, centerY);
61+ priv.wasRecognisedPress = true;
62+ }
63+ }
64+
65+ onStatusChanged: {
66+ if (status != TouchGestureArea.Recognized) {
67+ if (status == TouchGestureArea.Rejected) {
68+ root.cancelled();
69+ } else if (status == TouchGestureArea.WaitingForTouch) {
70+ if (priv.wasRecognisedPress) {
71+ if (!priv.wasRecognisedDrag) {
72+ root.clicked();
73+ } else {
74+ root.dropped();
75+ }
76+ }
77+ }
78+ priv.wasRecognisedDrag = false;
79+ priv.wasRecognisedPress = false;
80+ }
81+ }
82+
83+ onRecognisedDragChanged: {
84+ if (enableDrag && recognisedDrag) {
85+ priv.wasRecognisedDrag = true;
86+ root.dragStarted()
87+ }
88+ }
89+
90+ QtObject {
91+ id: priv
92+ property var dragObject: null
93+
94+ property bool wasRecognisedPress: false
95+ property bool wasRecognisedDrag: false
96+ }
97+
98+ onCancelled: {
99+ if (priv.dragObject) {
100+ var obj = priv.dragObject;
101+ priv.dragObject = null;
102+
103+ obj.Drag.cancel();
104+ obj.destroy();
105+ }
106+ }
107+
108+ onDragStarted: {
109+ if (dragComponentProperties) {
110+ priv.dragObject = dragComponent.createObject(root, dragComponentProperties);
111+ } else {
112+ priv.dragObject = dragComponent.createObject(root);
113+ }
114+ priv.dragObject.Drag.start();
115+ }
116+
117+ onDropped: {
118+ if (priv.dragObject) {
119+ var obj = priv.dragObject;
120+ priv.dragObject = null;
121+
122+ obj.Drag.drop();
123+ obj.destroy();
124+ }
125+ }
126+
127+ Binding {
128+ target: priv.dragObject
129+ when: priv.dragObject && priv.wasRecognisedDrag
130+ property: "x"
131+ value: {
132+ if (!priv.dragObject) return 0;
133+ var sum = 0;
134+ for (var i = 0; i < root.touchPoints.length; i++) {
135+ sum += root.touchPoints[i].x;
136+ }
137+ return sum/root.touchPoints.length - priv.dragObject.width/2;
138+ }
139+ }
140+
141+ Binding {
142+ target: priv.dragObject
143+ when: priv.dragObject && priv.wasRecognisedDrag
144+ property: "y"
145+ value: {
146+ if (!priv.dragObject) return 0;
147+ var sum = 0;
148+ for (var i = 0; i < root.touchPoints.length; i++) {
149+ sum += root.touchPoints[i].y;
150+ }
151+ return sum/root.touchPoints.length - priv.dragObject.height/2;
152+ }
153+ }
154+}
qml/Stages/TabletStage.qml+405−172
@@ -27,6 +27,9 @@ AbstractStage {
2727 objectName: "stages"
2828 anchors.fill: parent
2929
30+ property alias sideStageVisible: spreadView.sideStageVisible
31+ property alias sideStageWidth: spreadView.sideStageWidth
32+
3033 // Functions to be called from outside
3134 function updateFocusedAppOrientation() {
3235 var mainStageAppIndex = priv.indexOf(priv.mainStageAppId);
@@ -46,7 +49,7 @@ AbstractStage {
4649 if (delta < 0) { delta += 360; }
4750 delta = delta % 360;
4851
49− var supportedOrientations = spreadDelegate.application.supportedOrientations;
52+ var supportedOrientations = spreadDelegate.supportedOrientations;
5053 if (supportedOrientations === Qt.PrimaryOrientation) {
5154 supportedOrientations = spreadDelegate.orientations.primary;
5255 }
@@ -76,11 +79,31 @@ AbstractStage {
7679 }
7780 }
7881
82+ mainAppWindow: priv.focusedAppDelegate ? priv.focusedAppDelegate.appWindow : null
83+
7984 orientationChangesEnabled: priv.mainAppOrientationChangesEnabled
8085
81− supportedOrientations: mainApp ? mainApp.supportedOrientations
82− : (Qt.PortraitOrientation | Qt.LandscapeOrientation
83− | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation)
86+ supportedOrientations: {
87+ if (mainApp) {
88+ var orientations = mainApp.supportedOrientations;
89+ orientations |= Qt.LandscapeOrientation | Qt.InvertedLandscapeOrientation;
90+ if (priv.sideStageAppId && !spreadView.surfaceDragging) {
91+ // If we have a sidestage app, support Portrait orientation
92+ // so that it will switch the sidestage app to mainstage on rotate
93+ orientations |= Qt.PortraitOrientation|Qt.InvertedPortraitOrientation;
94+ }
95+ return orientations;
96+ } else {
97+ // we just don't care
98+ return Qt.PortraitOrientation |
99+ Qt.LandscapeOrientation |
100+ Qt.InvertedPortraitOrientation |
101+ Qt.InvertedLandscapeOrientation;
102+ }
103+ }
104+
105+ // How far left the stage has been dragged, used externally by tutorial code
106+ dragProgress: spreadRepeater.count > 0 ? spreadRepeater.itemAt(0).animatedProgress : 0
84107
85108 onWidthChanged: {
86109 spreadView.selectedIndex = -1;
@@ -88,13 +111,6 @@ AbstractStage {
88111 spreadView.contentX = -spreadView.shift;
89112 }
90113
91− onShellOrientationChanged: {
92− if (shellOrientation == Qt.PortraitOrientation || shellOrientation == Qt.InvertedPortraitOrientation) {
93− ApplicationManager.focusApplication(priv.mainStageAppId);
94− priv.sideStageAppId = "";
95− }
96− }
97−
98114 onInverseProgressChanged: {
99115 // This can't be a simple binding because that would be triggered after this handler
100116 // while we need it active before doing the anition left/right
@@ -170,20 +186,7 @@ AbstractStage {
170186
171187 property int highlightIndex: 0
172188
173− onFocusedAppIdChanged: {
174− if (priv.focusedAppId.length > 0) {
175− var focusedApp = ApplicationManager.findApplication(focusedAppId);
176− if (focusedApp.stage == ApplicationInfoInterface.SideStage) {
177− priv.sideStageAppId = focusedAppId;
178− } else {
179− priv.mainStageAppId = focusedAppId;
180− root.mainApp = focusedApp;
181− }
182− }
183−
184− appId0 = ApplicationManager.count >= 1 ? ApplicationManager.get(0).appId : "";
185− appId1 = ApplicationManager.count > 1 ? ApplicationManager.get(1).appId : "";
186− }
189+ onFocusedAppIdChanged: updateStageApps()
187190
188191 onFocusedAppDelegateChanged: {
189192 if (focusedAppDelegate) {
@@ -229,6 +232,46 @@ AbstractStage {
229232 onHighlightIndexChanged: {
230233 spreadView.contentX = highlightIndex * spreadView.contentWidth / (spreadRepeater.count + 2)
231234 }
235+
236+ function getTopApp(stage) {
237+ for (var i = 0; i < ApplicationManager.count; i++) {
238+ var app = ApplicationManager.get(i)
239+ if (app.stage === stage) {
240+ return app;
241+ }
242+ }
243+ return null;
244+ }
245+
246+ function setAppStage(appId, stage, save) {
247+ var app = ApplicationManager.findApplication(appId);
248+ if (app) {
249+ app.stage = stage;
250+ if (save) {
251+ WindowStateStorage.saveStage(appId, stage);
252+ }
253+ }
254+ }
255+
256+ function updateStageApps() {
257+ var app = priv.getTopApp(ApplicationInfoInterface.MainStage);
258+ priv.mainStageAppId = app ? app.appId : ""
259+ root.mainApp = app;
260+
261+ if (sideStage.shown) {
262+ app = priv.getTopApp(ApplicationInfoInterface.SideStage);
263+ priv.sideStageAppId = app ? app.appId : ""
264+ } else {
265+ priv.sideStageAppId = "";
266+ }
267+
268+ appId0 = ApplicationManager.count >= 1 ? ApplicationManager.get(0).appId : "";
269+ appId1 = ApplicationManager.count > 1 ? ApplicationManager.get(1).appId : "";
270+ }
271+
272+ readonly property bool sideStageEnabled: root.shellOrientation == Qt.LandscapeOrientation ||
273+ root.shellOrientation == Qt.InvertedLandscapeOrientation
274+ Component.onCompleted: updateStageApps();
232275 }
233276
234277 Connections {
@@ -256,7 +299,8 @@ AbstractStage {
256299 ApplicationManager.focusApplication("unity8-dash")
257300 }
258301 if (priv.sideStageAppId == appId) {
259− priv.sideStageAppId = "";
302+ var app = priv.getTopApp(ApplicationInfoInterface.SideStage);
303+ priv.sideStageAppId = app === null ? "" : app.appId;
260304 }
261305
262306 if (ApplicationManager.count == 0) {
@@ -267,6 +311,7 @@ AbstractStage {
267311 // lets make sure the spread doesn't mess up by the changing app list.
268312 spreadView.phase = 0;
269313 spreadView.contentX = -spreadView.shift;
314+
270315 ApplicationManager.focusApplication(ApplicationManager.get(0).appId);
271316 }
272317 }
@@ -281,8 +326,6 @@ AbstractStage {
281326 contentX: -shift
282327
283328 property int tileDistance: units.gu(20)
284− property int sideStageWidth: units.gu(40)
285− property bool sideStageVisible: priv.sideStageAppId
286329
287330 // This indicates when the spreadView is active. That means, all the animations
288331 // are activated and tiles need to line up for the spread.
@@ -322,6 +365,7 @@ AbstractStage {
322365 property int selectedIndex: -1
323366 property int draggedDelegateCount: 0
324367 property int closingIndex: -1
368+ property var selectedApplication: selectedIndex !== -1 ? ApplicationManager.get(selectedIndex) : null
325369
326370 // FIXME: Workaround Flickable's not keepping its contentX still when resized
327371 onContentXChanged: { forceItToRemainStillIfBeingResized(); }
@@ -343,15 +387,11 @@ AbstractStage {
343387 }
344388 }
345389
346− property bool sideStageDragging: sideStageDragHandle.dragging
347− property real sideStageDragProgress: sideStageDragHandle.progress
390+ property real sideStageDragProgress: sideStage.progress
391+ property bool sideStageVisible: priv.sideStageAppId
392+ property real sideStageWidth: units.gu(40)
348393
349− onSideStageDragProgressChanged: {
350− if (sideStageDragProgress == 1) {
351− ApplicationManager.focusApplication(priv.mainStageAppId);
352− priv.sideStageAppId = "";
353− }
354− }
394+ property bool surfaceDragging: triGestureArea.recognisedDrag
355395
356396 // In case the ApplicationManager already holds an app when starting up we're missing animations
357397 // Make sure we end up in the same state
@@ -402,7 +442,7 @@ AbstractStage {
402442 }
403443 ]
404444 state: {
405− if (priv.mainStageAppId && !priv.sideStageAppId) {
445+ if ((priv.mainStageAppId && !priv.sideStageAppId) || !priv.sideStageEnabled) {
406446 return "main";
407447 }
408448 if (!priv.mainStageAppId && priv.sideStageAppId) {
@@ -419,12 +459,26 @@ AbstractStage {
419459 // Flickabe.contentX wiggles during resizes. Don't react to it.
420460 return;
421461 }
422− if (spreadView.phase == 0 && spreadView.shiftedContentX > spreadView.width * spreadView.positionMarker2) {
423− spreadView.phase = 1;
424− } else if (spreadView.phase == 1 && spreadView.shiftedContentX > spreadView.width * spreadView.positionMarker4) {
425− spreadView.phase = 2;
426− } else if (spreadView.phase == 1 && spreadView.shiftedContentX < spreadView.width * spreadView.positionMarker2) {
427− spreadView.phase = 0;
462+
463+ switch (phase) {
464+ case 0:
465+ // the "spreadEnabled" part is because when code does "phase = 0; contentX = -shift" to
466+ // dismiss the spread because spreadEnabled went to false, for some reason, during tests,
467+ // Flickable might jump in and change contentX value back, causing the code below to do
468+ // "phase = 1" which will make the spread stay.
469+ // It sucks that we have no control whatsoever over whether or when Flickable animates its
470+ // contentX.
471+ if (root.spreadEnabled && shiftedContentX > width * positionMarker2) {
472+ phase = 1;
473+ }
474+ break;
475+ case 1:
476+ if (shiftedContentX < width * positionMarker2) {
477+ phase = 0;
478+ } else if (shiftedContentX >= width * positionMarker4 && !spreadDragArea.dragging) {
479+ phase = 2;
480+ }
481+ break;
428482 }
429483 }
430484
@@ -446,6 +500,7 @@ AbstractStage {
446500 }
447501
448502 function snapTo(index) {
503+ snapAnimation.stop();
449504 spreadView.selectedIndex = index;
450505 snapAnimation.targetContentX = -shift;
451506 snapAnimation.start();
@@ -455,55 +510,41 @@ AbstractStage {
455510 // We don't want to really reorder them in the model because that allows us to keep track
456511 // of the last focused order.
457512 function indexToZIndex(index) {
513+ // only shuffle when we've got a main and overlay
514+ if (state !== "mainAndOverlay") return index;
515+
458516 var app = ApplicationManager.get(index);
459517 if (!app) {
460518 return index;
461519 }
462520
463− var active = app.appId == priv.mainStageAppId || app.appId == priv.sideStageAppId;
464− if (active && app.stage == ApplicationInfoInterface.MainStage) {
465− // if this app is active, and its the MainStage, always put it to index 0
521+ // don't shuffle indexes greater than "actives or next"
522+ if (index > 2) return index;
523+
524+ if (app.appId === priv.mainStageAppId) {
525+ // Active main stage always at 0
466526 return 0;
467527 }
468− if (active && app.stage == ApplicationInfoInterface.SideStage) {
469− if (!priv.mainStageAppId) {
470− // Only have SS apps running. Put the active one at 0
471− return 0;
472− }
473528
474− // Precondition now: There's an active MS app and this is SS app:
475− if (spreadView.nextInStack >= 0 && ApplicationManager.get(spreadView.nextInStack).stage == ApplicationInfoInterface.MainStage) {
476− // If the next app coming from the right is a MS app, we need to elevate this SS ap above it.
477− // Put it to at least level 2, or higher if there's more apps coming in before this one.
478− return Math.max(index, 2);
479− } else {
480− // if this is no next app to come in from the right, place this one at index 1, just on top the active MS app.
529+ if (spreadView.nextInStack > 0) {
530+ var nextAppInStack = ApplicationManager.get(spreadView.nextInStack);
531+
532+ if (index === spreadView.nextInStack) {
533+ // this is the next app in stack.
534+
535+ if (app.stage === ApplicationInfoInterface.SideStage) {
536+ // if the next app in stack is a sidestage app, it must order on top of other side stage app
537+ return Math.min(2, ApplicationManager.count-1);
538+ }
481539 return 1;
482540 }
483− }
484− if (index <= 2 && app.stage == ApplicationInfoInterface.MainStage && priv.sideStageAppId) {
485− // Ok, this is an inactive MS app. If there's an active SS app around, we need to place this one
486− // in between the active MS app and the active SS app, so that it comes in from there when dragging from the right.
487− // If there's now active SS app, just leave it where it is.
488− return priv.indexOf(priv.sideStageAppId) < index ? index - 1 : index;
489− }
490− if (index == spreadView.nextInStack && app.stage == ApplicationInfoInterface.SideStage) {
491− // This is a SS app and the next one to come in from the right:
492− if (priv.sideStageAppId && priv.mainStageAppId) {
493− // If there's both, an active MS and an active SS app, put this one right on top of that
494− return 2;
541+ if (nextAppInStack.stage === ApplicationInfoInterface.SideStage) {
542+ // if the next app in stack is a sidestage app, it must order on top of other side stage app
543+ return 1;
495544 }
496− // Or if there's only one other active app, put it on top of that.
497− // The case that there isn't any other active app is already handled above.
498− return 1;
499− }
500− if (index == 2 && spreadView.nextInStack == 1 && priv.sideStageAppId) {
501− // If its index 2 but not the next one to come in, it means
502− // we've pulled another one down to index 2. Move this one up to 2 instead.
503− return 3;
545+ return Math.min(2, ApplicationManager.count-1);
504546 }
505− // don't touch all others... (mostly index > 3 + simple cases where the above doesn't shuffle much)
506− return index;
547+ return Math.min(index+1, ApplicationManager.count-1);
507548 }
508549
509550 SequentialAnimation {
@@ -521,8 +562,12 @@ AbstractStage {
521562 script: {
522563 if (spreadView.selectedIndex >= 0) {
523564 var newIndex = spreadView.selectedIndex;
565+ var application = ApplicationManager.get(newIndex);
566+ if (application.stage === ApplicationInfoInterface.SideStage) {
567+ sideStage.showNow();
568+ }
524569 spreadView.selectedIndex = -1;
525− ApplicationManager.focusApplication(ApplicationManager.get(newIndex).appId);
570+ ApplicationManager.focusApplication(application.appId);
526571 spreadView.phase = 0;
527572 spreadView.contentX = -spreadView.shift;
528573 }
@@ -545,87 +590,82 @@ AbstractStage {
545590 spreadView.snapTo(0);
546591 }
547592
548− Rectangle {
549− id: sideStageBackground
550− color: "black"
551− width: spreadView.sideStageWidth * (1 - sideStageDragHandle.progress)
552− height: priv.landscapeHeight
553− x: spreadView.width - width
554− z: spreadView.indexToZIndex(priv.indexOf(priv.sideStageAppId))
555− opacity: spreadView.phase == 0 ? 1 : 0
556− Behavior on opacity { UbuntuNumberAnimation {} }
593+ DropArea {
594+ objectName: "MainStageDropArea"
595+ anchors {
596+ left: parent.left
597+ top: parent.top
598+ bottom: parent.bottom
599+ }
600+ width: spreadView.width - sideStage.width
601+ enabled: priv.sideStageEnabled
602+
603+ onDropped: {
604+ priv.setAppStage(drag.source.appId, ApplicationInfoInterface.MainStage, true);
605+ ApplicationManager.focusApplication(drag.source.appId);
606+ }
607+ keys: "SideStage"
557608 }
558609
559− Item {
560− id: sideStageDragHandle
561− anchors.right: sideStageBackground.left
562− anchors.top: sideStageBackground.top
563− width: units.gu(2)
610+ SideStage {
611+ id: sideStage
612+ objectName: "sideStage"
564613 height: priv.landscapeHeight
565− z: sideStageBackground.z
566− opacity: spreadView.phase <= 0 && spreadView.sideStageVisible ? 1 : 0
567− property real progress: 0
568− property bool dragging: false
614+ x: spreadView.width - width
615+ z: {
616+ if (!priv.mainStageAppId) return 0;
569617
570− Behavior on opacity { UbuntuNumberAnimation {} }
618+ if (priv.sideStageAppId && spreadView.nextInStack > 0) {
619+ var nextAppInStack = ApplicationManager.get(spreadView.nextInStack);
571620
572− Connections {
573− target: spreadView
574− onSideStageVisibleChanged: {
575− if (spreadView.sideStageVisible) {
576− sideStageDragHandle.progress = 0;
621+ if (nextAppInStack.stage === ApplicationInfoInterface.MainStage) {
622+ // if the next app in stack is a main stage app, put the sidestage on top of it.
623+ return 2;
577624 }
625+ return 1;
578626 }
627+
628+ return 1;
579629 }
630+ visible: progress != 0
631+ enabled: priv.sideStageEnabled && sideStageDropArea.dropAllowed
632+ opacity: priv.sideStageEnabled && !spreadView.active ? 1 : 0
633+ Behavior on opacity { UbuntuNumberAnimation {} }
580634
581− Image {
582− anchors.centerIn: parent
583− width: sideStageDragHandleMouseArea.pressed ? parent.width * 2 : parent.width
584− height: parent.height
585− source: "graphics/sidestage_handle@20.png"
586− Behavior on width { UbuntuNumberAnimation {} }
635+ onShownChanged: {
636+ if (!shown && ApplicationManager.focusedApplicationId == priv.sideStageAppId) {
637+ ApplicationManager.requestFocusApplication(priv.mainStageAppId);
638+ }
639+ priv.updateStageApps();
640+ if (shown && priv.sideStageAppId) {
641+ ApplicationManager.requestFocusApplication(priv.sideStageAppId);
642+ }
587643 }
588644
589− MouseArea {
590− id: sideStageDragHandleMouseArea
645+ DropArea {
646+ id: sideStageDropArea
647+ objectName: "SideStageDropArea"
591648 anchors.fill: parent
592− enabled: spreadView.shiftedContentX == 0
593− property int startX
594− property var gesturePoints: new Array()
595− property real totalDiff
596−
597− onPressed: {
598− gesturePoints = [];
599− startX = mouseX;
600− totalDiff = 0.0;
601− sideStageDragHandle.progress = 0;
602− sideStageDragHandle.dragging = true;
603− }
604− onMouseXChanged: {
605− totalDiff += mouseX - startX;
606− if (priv.mainStageAppId) {
607− sideStageDragHandle.progress = Math.max(0, totalDiff / spreadView.sideStageWidth);
608− }
609− gesturePoints.push(mouseX);
649+
650+ property bool dropAllowed: true
651+
652+ onEntered: {
653+ dropAllowed = drag.keys != "Disabled";
610654 }
611− onReleased: {
612− if (priv.mainStageAppId) {
613− var oneWayFlick = priv.evaluateOneWayFlick(gesturePoints);
614− sideStageDragSnapAnimation.to = sideStageDragHandle.progress > 0.5 || oneWayFlick ? 1 : 0;
615− sideStageDragSnapAnimation.start();
616− } else {
617− sideStageDragHandle.dragging = false;
655+ onExited: {
656+ dropAllowed = true;
657+ }
658+ onDropped: {
659+ if (drop.keys == "MainStage") {
660+ priv.setAppStage(drop.source.appId, ApplicationInfoInterface.SideStage, true);
661+ ApplicationManager.requestFocusApplication(drop.source.appId);
618662 }
619663 }
620− }
621− UbuntuNumberAnimation {
622− id: sideStageDragSnapAnimation
623− target: sideStageDragHandle
624− property: "progress"
625−
626− onRunningChanged: {
627− if (!running) {
628− sideStageDragHandle.dragging = false;
664+ drag {
665+ onSourceChanged: {
666+ if (!sideStageDropArea.drag.source) {
667+ dropAllowed = true;
668+ }
629669 }
630670 }
631671 }
@@ -640,37 +680,53 @@ AbstractStage {
640680 id: spreadTile
641681 objectName: model.appId ? "tabletSpreadDelegate_" + model.appId
642682 : "tabletSpreadDelegate_null";
643− width: {
644− if (wantsMainStage) {
645− return spreadView.width;
646− } else {
647− return spreadView.sideStageWidth;
648− }
649− }
650− height: {
651− if (wantsMainStage) {
652− return spreadView.height;
653− } else {
654− return priv.landscapeHeight;
655− }
656− }
657− active: model.appId == priv.mainStageAppId || model.appId == priv.sideStageAppId
658− zIndex: spreadView.indexToZIndex(index)
683+ width: spreadView.width
684+ height: spreadView.height
685+ active: appId == priv.mainStageAppId || appId == priv.sideStageAppId
686+ zIndex: selected && stage == ApplicationInfoInterface.MainStage ? 0 : spreadView.indexToZIndex(index)
659687 selected: spreadView.selectedIndex == index
660688 otherSelected: spreadView.selectedIndex >= 0 && !selected
661− isInSideStage: priv.sideStageAppId == model.appId
689+ isInSideStage: priv.sideStageAppId === appId
662690 interactive: !spreadView.interactive && spreadView.phase === 0 && root.interactive
663691 swipeToCloseEnabled: spreadView.interactive && !snapAnimation.running
664692 maximizedAppTopMargin: root.maximizedAppTopMargin
665− dragOffset: !isDash && model.appId == priv.mainStageAppId && root.inverseProgress > 0 && spreadView.phase === 0 ? root.inverseProgress : 0
693+ dragOffset: !isDash && appId == priv.mainStageAppId && root.inverseProgress > 0 && spreadView.phase === 0 ? root.inverseProgress : 0
666694 application: ApplicationManager.get(index)
667695 closeable: !isDash
668696 highlightShown: root.altTabPressed && priv.highlightIndex == zIndex
669697
670698 readonly property bool wantsMainStage: model.stage == ApplicationInfoInterface.MainStage
671699
700+ readonly property string appId: model.appId
672701 readonly property bool isDash: model.appId == "unity8-dash"
673702
703+ stage: model.stage
704+ fullscreen: {
705+ if (mainApp && stage === ApplicationInfoInterface.SideStage) {
706+ return mainApp.fullscreen;
707+ }
708+ return application ? application.fullscreen : false;
709+ }
710+
711+ supportedOrientations: {
712+ if (application) {
713+ var orientations = application.supportedOrientations;
714+ if (stage == ApplicationInfoInterface.MainStage) {
715+ // When an app is in the mainstage, it always supports Landscape|InvertedLandscape
716+ // so that we can drag it from the main stage to the side stage
717+ orientations |= Qt.LandscapeOrientation | Qt.InvertedLandscapeOrientation;
718+ }
719+ return orientations;
720+ } else {
721+ // we just don't care
722+ return Qt.PortraitOrientation |
723+ Qt.LandscapeOrientation |
724+ Qt.InvertedPortraitOrientation |
725+ Qt.InvertedLandscapeOrientation;
726+ }
727+ }
728+
729+
674730 Binding {
675731 target: spreadTile.application
676732 property: "exemptFromLifecycle"
@@ -701,6 +757,28 @@ AbstractStage {
701757 enabled: spreadView.closingIndex >= 0
702758 UbuntuNumberAnimation {}
703759 }
760+ Connections {
761+ target: priv
762+ onSideStageEnabledChanged: refreshStage()
763+ }
764+
765+ Component.onCompleted: {
766+ refreshStage()
767+ stageChanged.connect(priv.updateStageApps);
768+ }
769+
770+ function refreshStage() {
771+ var stage = ApplicationInfoInterface.MainStage;
772+ if (priv.sideStageEnabled) {
773+ if (application && application.supportedOrientations & (Qt.PortraitOrientation|Qt.InvertedPortraitOrientation)) {
774+ stage = WindowStateStorage.getStage(appId);
775+ }
776+ }
777+
778+ if (model.stage !== stage) {
779+ priv.setAppStage(appId, stage, false);
780+ }
781+ }
704782
705783 // This is required because none of the bindings are triggered in some cases:
706784 // When an app is closed, it might happen that ApplicationManager.get(nextInStack)
@@ -725,7 +803,7 @@ AbstractStage {
725803
726804 // TODO: Hiding tile when progress is such that it will be off screen.
727805 property bool occluded: {
728− if (spreadView.active) return false;
806+ if (spreadView.active && !offScreen) return false;
729807 else if (spreadTile.active) return false;
730808 else if (xTranslateAnimating) return false;
731809 else if (z <= 1 && priv.focusedAppDelegateIsDislocated) return false;
@@ -749,17 +827,87 @@ AbstractStage {
749827 return progress;
750828 }
751829
752− shellOrientationAngle: wantsMainStage ? root.shellOrientationAngle : 0
753− shellOrientation: wantsMainStage ? root.shellOrientation : Qt.PortraitOrientation
754− orientations: Orientations {
755− primary: spreadTile.wantsMainStage ? root.orientations.primary : Qt.PortraitOrientation
756− native_: spreadTile.wantsMainStage ? root.orientations.native_ : Qt.PortraitOrientation
830+ shellOrientationAngle: root.shellOrientationAngle
831+ shellOrientation: root.shellOrientation
832+ orientations: root.orientations
833+
834+ states: [
835+ State {
836+ name: "MainStage"
837+ when: spreadTile.stage == ApplicationInfoInterface.MainStage
838+ },
839+ State {
840+ name: "SideStage"
841+ when: spreadTile.stage == ApplicationInfoInterface.SideStage
842+
843+ PropertyChanges {
844+ target: spreadTile
845+ width: spreadView.sideStageWidth
846+ height: priv.landscapeHeight
847+
848+ supportedOrientations: Qt.PortraitOrientation
849+ shellOrientationAngle: 0
850+ shellOrientation: Qt.PortraitOrientation
851+ orientations: sideStageOrientations
852+ }
853+ }
854+ ]
855+
856+ Orientations {
857+ id: sideStageOrientations
858+ primary: Qt.PortraitOrientation
859+ native_: Qt.PortraitOrientation
757860 portrait: root.orientations.portrait
758861 invertedPortrait: root.orientations.invertedPortrait
759862 landscape: root.orientations.landscape
760863 invertedLandscape: root.orientations.invertedLandscape
761864 }
762865
866+ transitions: [
867+ Transition {
868+ to: "SideStage"
869+ SequentialAnimation {
870+ PropertyAction {
871+ target: spreadTile
872+ properties: "width,height,supportedOrientations,shellOrientationAngle,shellOrientation,orientations"
873+ }
874+ ScriptAction {
875+ script: {
876+ // rotate immediately.
877+ spreadTile.matchShellOrientation();
878+ if (ApplicationManager.focusedApplicationId === spreadTile.appId &&
879+ priv.sideStageEnabled && !sideStage.shown) {
880+ // Sidestage was focused, so show the side stage.
881+ sideStage.show();
882+ // if we've switched to a main app which doesnt support portrait, hide the side stage.
883+ } else if (mainApp && (mainApp.supportedOrientations & (Qt.PortraitOrientation|Qt.InvertedPortraitOrientation)) == 0) {
884+ sideStage.hideNow();
885+ }
886+ }
887+ }
888+ }
889+ },
890+ Transition {
891+ from: "SideStage"
892+ SequentialAnimation {
893+ ScriptAction {
894+ script: {
895+ if (priv.sideStageAppId === spreadTile.appId &&
896+ mainApp && (mainApp.supportedOrientations & (Qt.PortraitOrientation|Qt.InvertedPortraitOrientation)) == 0) {
897+ // The mainstage app did not natively support portrait orientation, so focus the sidestage.
898+ ApplicationManager.requestFocusApplication(spreadTile.appId);
899+ }
900+ }
901+ }
902+ PropertyAction {
903+ target: spreadTile
904+ properties: "width,height,supportedOrientations,shellOrientationAngle,shellOrientation,orientations"
905+ }
906+ ScriptAction { script: { spreadTile.matchShellOrientation(); } }
907+ }
908+ }
909+ ]
910+
763911 onClicked: {
764912 if (spreadView.phase == 2) {
765913 spreadView.snapTo(index);
@@ -779,6 +927,16 @@ AbstractStage {
779927 ApplicationManager.stopApplication(ApplicationManager.get(index).appId);
780928 }
781929
930+ onFocusChanged: {
931+ if (focus && ApplicationManager.focusedApplicationId !== appId) {
932+ ApplicationManager.focusApplication(appId);
933+ }
934+
935+ if (focus && priv.sideStageEnabled && stage === ApplicationInfoInterface.SideStage) {
936+ sideStage.show();
937+ }
938+ }
939+
782940 Binding {
783941 target: root
784942 when: model.appId == priv.mainStageAppId
@@ -798,6 +956,80 @@ AbstractStage {
798956 period: (spreadView.positionMarker2 - spreadView.positionMarker1) / 3
799957 progress: spreadTile.progress - spreadView.positionMarker1
800958 }
959+
960+ StagedFullscreenPolicy {
961+ id: fullscreenPolicy
962+ application: spreadTile.application
963+ }
964+ Connections {
965+ target: root
966+ onStageAboutToBeUnloaded: fullscreenPolicy.active = false
967+ }
968+ }
969+ }
970+ }
971+ }
972+
973+ TabletSideStageTouchGesture {
974+ id: triGestureArea
975+ anchors.fill: parent
976+ enabled: priv.sideStageEnabled && !spreadView.active
977+ property var dragObject: null
978+ property string appId: ""
979+ dragComponent: dragComponent
980+ dragComponentProperties: { "appId": appId }
981+
982+ onPressed: {
983+ function matchDelegate(obj) { return String(obj.objectName).indexOf("tabletSpreadDelegate") >= 0; }
984+
985+ var delegateAtCenter = Functions.itemAt(spreadRow, x, y, matchDelegate);
986+ if (!delegateAtCenter) return;
987+
988+ appId = delegateAtCenter.appId;
989+ }
990+
991+ onClicked: {
992+ if (sideStage.shown) {
993+ sideStage.hide();
994+ } else {
995+ sideStage.show();
996+ }
997+ }
998+
999+ onDragStarted: {
1000+ // If we're dragging to the sidestage.
1001+ if (!sideStage.shown) {
1002+ sideStage.show();
1003+ }
1004+ }
1005+
1006+ Component {
1007+ id: dragComponent
1008+ SessionContainer {
1009+ property string appId: ""
1010+ property var application: ApplicationManager.findApplication(appId)
1011+
1012+ session: application ? application.session : null
1013+ interactive: false
1014+ resizeSurface: false
1015+ focus: false
1016+
1017+ width: units.gu(40)
1018+ height: units.gu(40)
1019+
1020+ Drag.hotSpot.x: width/2
1021+ Drag.hotSpot.y: height/2
1022+ // only accept opposite stage.
1023+ Drag.keys: {
1024+ if (!application) return "Disabled";
1025+
1026+ if (application.stage === ApplicationInfo.MainStage) {
1027+ if (application.supportedOrientations & (Qt.PortraitOrientation|Qt.InvertedPortraitOrientation)) {
1028+ return "MainStage";
1029+ }
1030+ return "Disabled";
1031+ }
1032+ return "SideStage";
8011033 }
8021034 }
8031035 }
@@ -812,7 +1044,8 @@ AbstractStage {
8121044 DirectionalDragArea {
8131045 id: spreadDragArea
8141046 objectName: "spreadDragArea"
815− anchors { top: parent.top; right: parent.right; bottom: parent.bottom }
1047+ x: parent.width - root.dragAreaWidth
1048+ anchors { top: parent.top; bottom: parent.bottom }
8161049 width: root.dragAreaWidth
8171050 direction: Direction.Leftwards
8181051 enabled: (spreadView.phase != 2 && root.spreadEnabled) || dragging
qml/Stages/TransformedTabletSpreadDelegate.qml+84−56
@@ -31,6 +31,7 @@ SpreadDelegate {
3131 // Set this to true when this tile a currently active on either the MS or the SS.
3232 property bool active: false
3333
34+ property int stage
3435 property int zIndex
3536 property real progress: 0
3637 property real animatedProgress: 0
@@ -49,10 +50,11 @@ SpreadDelegate {
4950
5051 property int dragOffset: 0
5152 readonly property alias xTranslateAnimating: xTranslateAnimation.running
53+ readonly property bool offScreen: priv.xTranslate >= 0
5254
5355 dropShadow: spreadView.active ||
5456 (active
55− && (model.stage == ApplicationInfoInterface.MainStage || !priv.shellIsLandscape)
57+ && (stage == ApplicationInfoInterface.MainStage || !priv.shellIsLandscape)
5658 && priv.xTranslate != 0)
5759
5860 onSelectedChanged: {
@@ -141,9 +143,9 @@ SpreadDelegate {
141143 enabled: !spreadView.active &&
142144 !snapAnimation.running &&
143145 model.appId !== "unity8-dash" &&
144− !spreadView.sideStageDragging &&
145146 spreadView.animateX &&
146− !spreadView.beingResized
147+ !spreadView.beingResized &&
148+ priv.state !== "sideStage"
147149 UbuntuNumberAnimation {
148150 id: xTranslateAnimation
149151 duration: UbuntuAnimation.FastDuration
@@ -153,21 +155,20 @@ SpreadDelegate {
153155 property real xTranslate: {
154156 var newTranslate = 0;
155157
156− if (otherSelected) {
157− return priv.selectedXTranslate;
158− }
159−
160− if (isSelected) {
161− if (model.stage == ApplicationInfoInterface.MainStage) {
158+ // selected app or opposite stage active app.
159+ if (isSelected || (otherSelected && root.active && spreadView.selectedApplication && spreadView.selectedApplication.stage !== stage)) {
160+ if (stage == ApplicationInfoInterface.MainStage) {
162161 return linearAnimation(selectedProgress, negativeProgress, selectedXTranslate, -spreadView.width, root.progress);
163162 } else {
164163 return linearAnimation(selectedProgress, negativeProgress, selectedXTranslate, -spreadView.sideStageWidth, root.progress);
165164 }
165+ } else if (otherSelected) {
166+ return selectedXTranslate;
166167 }
167168
168169 // The tile should move a bit to the left if a new one comes on top of it, but not for the Side Stage and not
169170 // when we're only dragging the side stage in on top of a main stage app
170− var shouldMoveAway = spreadView.nextInStack >= 0 && priv.movedActive && model.stage === ApplicationInfoInterface.MainStage &&
171+ var shouldMoveAway = spreadView.nextInStack >= 0 && priv.movedActive && stage === ApplicationInfoInterface.MainStage &&
171172 ApplicationManager.get(spreadView.nextInStack).stage === ApplicationInfoInterface.MainStage;
172173
173174 if (active) {
@@ -177,36 +178,45 @@ SpreadDelegate {
177178 newTranslate += linearAnimation(0, spreadView.positionMarker2, 0, -units.gu(4), root.animatedProgress);
178179 }
179180 newTranslate += root.dragOffset;
180− }
181− if (!spreadView.active && model.appId == "unity8-dash" && !root.active) {
181+ } else if (!spreadView.active && model.appId == "unity8-dash") {
182182 newTranslate -= root.width;
183183 }
184184
185− if (nextInStack && spreadView.phase == 0) {
186− if (model.stage == ApplicationInfoInterface.MainStage) {
187− if (spreadView.sideStageVisible && root.progress > 0) {
188− // Move it so it appears from behind the side stage immediately
189− newTranslate += -spreadView.sideStageWidth;
185+ if (!spreadView.active) {
186+ return newTranslate;
187+ }
188+
189+ var shadowOffset = units.gu(2);
190+
191+ if (spreadView.phase == 0) {
192+ if (nextInStack) {
193+ if (stage == ApplicationInfoInterface.MainStage) {
194+ if (spreadView.sideStageVisible && root.progress > 0) {
195+ // Move it so it appears from behind the side stage immediately
196+ newTranslate += -spreadView.sideStageWidth;
197+ }
190198 }
191− }
192199
193− if (model.stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
194− // This is when we only drag the side stage in, without rotation or snapping
195− newTranslate = linearAnimation(0, spreadView.positionMarker2, 0, -spreadView.sideStageWidth, root.progress);
196− } else {
197− newTranslate += linearAnimation(0, spreadView.positionMarker2, 0, -spreadView.sideStageWidth * spreadView.snapPosition, root.animatedProgress);
200+ if (stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
201+ // This is when we only drag the side stage in, without rotation or snapping
202+ newTranslate = linearAnimation(0, spreadView.positionMarker2, 0, -spreadView.sideStageWidth, root.animatedProgress);
203+ } else {
204+ newTranslate += linearAnimation(0, spreadView.positionMarker2, 0, -spreadView.sideStageWidth * spreadView.snapPosition, root.animatedProgress);
205+ }
206+ } else if (!root.active) {
207+ newTranslate += shadowOffset;
198208 }
199209 }
200210
201211 if (spreadView.phase == 1) {
202212 if (nextInStack) {
203− if (model.stage == ApplicationInfoInterface.MainStage) {
213+ if (stage == ApplicationInfoInterface.MainStage) {
204214 var startValue = -spreadView.sideStageWidth * spreadView.snapPosition + (spreadView.sideStageVisible ? -spreadView.sideStageWidth : 0);
205215 newTranslate += linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, startValue, priv.phase2StartTranslate, root.animatedProgress);
206216 } else {
207217 var endValue = -spreadView.width + spreadView.width * root.zIndex / 6;
208218 if (!spreadView.sideStageVisible) {
209− newTranslate += linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, -spreadView.sideStageWidth, priv.phase2StartTranslate, root.progress);
219+ newTranslate += linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, -spreadView.sideStageWidth, priv.phase2StartTranslate, root.animatedProgress);
210220 } else {
211221 newTranslate += linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, -spreadView.sideStageWidth * spreadView.snapPosition, priv.phase2StartTranslate, root.animatedProgress);
212222 }
@@ -215,16 +225,20 @@ SpreadDelegate {
215225 var startProgress = spreadView.positionMarker2 - (zIndex * spreadView.positionMarker2 / 2);
216226 var endProgress = spreadView.positionMarker4 - (zIndex * spreadView.tileDistance / spreadView.width);
217227 var startTranslate = -root.width + (shouldMoveAway ? -units.gu(4) : 0);
218− newTranslate = linearAnimation(startProgress, endProgress, startTranslate, priv.phase2StartTranslate, root.progress);
228+ newTranslate = linearAnimation(startProgress, endProgress, startTranslate, priv.phase2StartTranslate, root.animatedProgress);
219229 } else {
220230 var startProgress = spreadView.positionMarker2 - (zIndex * spreadView.positionMarker2 / 2);
221231 var endProgress = spreadView.positionMarker4 - (zIndex * spreadView.tileDistance / spreadView.width);
222− newTranslate = linearAnimation(startProgress, endProgress, 0, priv.phase2StartTranslate, root.progress);
232+ newTranslate = linearAnimation(startProgress, endProgress, shadowOffset * Math.max(0, zIndex-2), priv.phase2StartTranslate, root.animatedProgress);
223233 }
224234 }
225235
226236 if (spreadView.phase == 2) {
227− newTranslate = -easingCurve.value * (spreadView.width - root.zIndex * animatedEndDistance);
237+ if (easingCurve.value == 0 && !nextInStack) {
238+ newTranslate = shadowOffset;
239+ } else {
240+ newTranslate = -easingCurve.value * (spreadView.width - root.zIndex * animatedEndDistance);
241+ }
228242 }
229243
230244 return newTranslate;
@@ -235,33 +249,30 @@ SpreadDelegate {
235249 return 1;
236250 }
237251
238− if (otherSelected) {
239− return selectedScale;
240− }
241−
242− if (isSelected) {
252+ // selected app or opposite stage active app.
253+ if (isSelected || (otherSelected && root.active && spreadView.selectedApplication && spreadView.selectedApplication.stage !== stage)) {
243254 return linearAnimation(selectedProgress, negativeProgress, selectedScale, 1, root.progress);
255+ } else if (otherSelected) {
256+ return selectedScale;
244257 }
245258
246259 if (spreadView.phase == 0) {
247260 if (nextInStack) {
248− if (model.stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
261+ if (stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
249262 return 1;
250263 } else {
251264 var targetScale = root.dragStartScale - ((root.dragStartScale - 1) * spreadView.snapPosition);
252265 return linearAnimation(0, spreadView.positionMarker2, root.dragStartScale, targetScale, root.animatedProgress);
253266 }
254− } else if (active) {
255− return 1;
256267 } else {
257− return linearAnimation(0, spreadView.positionMarker2, root.startScale, root.endScale, root.progress);
268+ return 1;
258269 }
259270 }
260271
261272 if (spreadView.phase == 1) {
262273 if (nextInStack) {
263274 var startScale = 1;
264− if (model.stage !== ApplicationInfoInterface.SideStage || spreadView.sideStageVisible) {
275+ if (stage !== ApplicationInfoInterface.SideStage || spreadView.sideStageVisible) {
265276 startScale = root.dragStartScale - ((root.dragStartScale - 1) * spreadView.snapPosition);
266277 }
267278 return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, startScale, priv.phase2StartScale, root.animatedProgress);
@@ -283,21 +294,22 @@ SpreadDelegate {
283294 return 0;
284295 }
285296
286− if (otherSelected) {
287− return selectedAngle;
288− }
289− if (isSelected) {
297+ // selected app or opposite stage active app.
298+ if (isSelected || (otherSelected && root.active && spreadView.selectedApplication && spreadView.selectedApplication.stage !== stage)) {
290299 return linearAnimation(selectedProgress, negativeProgress, selectedAngle, 0, root.progress);
300+ } else if (otherSelected) {
301+ return selectedAngle;
291302 }
292303
293304 // The tile should rotate a bit when another one comes on top, but not when only dragging the side stage in
294− var shouldMoveAway = spreadView.nextInStack >= 0 && movedActive &&
305+ var shouldMoveAway = spreadView.nextInStack == -1 ||
306+ spreadView.nextInStack >= 0 && priv.movedActive &&
295307 (ApplicationManager.get(spreadView.nextInStack).stage === ApplicationInfoInterface.MainStage ||
296− model.stage == ApplicationInfoInterface.SideStage);
308+ stage == ApplicationInfoInterface.SideStage);
297309
298310 if (spreadView.phase == 0) {
299311 if (nextInStack) {
300− if (model.stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
312+ if (stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
301313 return 0;
302314 } else {
303315 return linearAnimation(0, spreadView.positionMarker2, root.startAngle, root.startAngle * (1-spreadView.snapPosition), root.animatedProgress);
@@ -309,7 +321,7 @@ SpreadDelegate {
309321 }
310322 if (spreadView.phase == 1) {
311323 if (nextInStack) {
312− if (model.stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
324+ if (stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
313325 return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, 0, priv.phase2StartAngle, root.animatedProgress);
314326 } else {
315327 return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, root.startAngle * (1-spreadView.snapPosition), priv.phase2StartAngle, root.animatedProgress);
@@ -328,31 +340,47 @@ SpreadDelegate {
328340 }
329341
330342 property real opacityTransform: {
331− if (otherSelected && spreadView.phase == 2) {
343+ // animate opacity for items not snapping into view.
344+ if (spreadView.phase !== 2) return 1;
345+ if (root.isSelected) return 1;
346+
347+ if (otherSelected) {
348+ if (root.active && spreadView.selectedApplication && spreadView.selectedApplication.stage !== stage) {
349+ return 1;
350+ }
332351 return linearAnimation(selectedProgress, negativeProgress, selectedOpacity, 0, root.progress);
333352 }
334−
335353 return 1;
336354 }
337355
338356 property real topMarginProgress: {
339− if (priv.isSelected) {
357+ // selected app or opposite stage active app.
358+ if (isSelected || (otherSelected && root.active && spreadView.selectedApplication && spreadView.selectedApplication.stage !== stage)) {
340359 return linearAnimation(selectedProgress, negativeProgress, selectedTopMarginProgress, 0, root.progress);
341360 }
342− switch (spreadView.phase) {
343− case 0:
361+
362+ if (spreadView.phase == 0) {
344363 return 0;
345− case 1:
346− return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4,
347− 0, priv.phase2StartTopMarginProgress, root.progress);
364+ } else if (spreadView.phase == 1) {
365+ if (nextInStack) {
366+ return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4,
367+ 0, 1, root.progress);
368+ }
369+ var startProgress = spreadView.positionMarker2 - (zIndex * spreadView.positionMarker2 / 2);
370+ var endProgress = spreadView.positionMarker4 - (zIndex * spreadView.tileDistance / spreadView.width);
371+ return linearAnimation(startProgress, endProgress, 0, 1, root.progress);
348372 }
349373 return 1;
350374 }
351375
352376 states: [
353377 State {
354− name: "sideStageDragging"; when: spreadView.sideStageDragging && root.isInSideStage
355− PropertyChanges { target: priv; xTranslate: -spreadView.sideStageWidth + spreadView.sideStageWidth * spreadView.sideStageDragProgress }
378+ name: "sideStage";
379+ when: root.isInSideStage && spreadView.shiftedContentX == 0 && spreadView.phase == 0
380+ PropertyChanges {
381+ target: priv;
382+ xTranslate: -spreadView.sideStageWidth + spreadView.sideStageWidth * (1-spreadView.sideStageDragProgress)
383+ }
356384 }
357385 ]
358386 }
qml/Stages/WindowResizeArea.qml+2−2
@@ -69,7 +69,7 @@ MouseArea {
6969 onHeightChanged: priv.updateNormalGeometry();
7070 }
7171
72− Component.onCompleted: {
72+ function loadWindowState() {
7373 var windowGeometry = windowStateStorage.getGeometry(root.windowId,
7474 Qt.rect(target.x, target.y, defaultWidth, defaultHeight));
7575
@@ -87,7 +87,7 @@ MouseArea {
8787 priv.updateNormalGeometry();
8888 }
8989
90− Component.onDestruction: {
90+ function saveWindowState() {
9191 windowStateStorage.saveState(root.windowId, target.state == "maximized" ? WindowStateStorage.WindowStateMaximized : WindowStateStorage.WindowStateNormal)
9292 windowStateStorage.saveGeometry(root.windowId, Qt.rect(priv.normalX, priv.normalY, priv.normalWidth, priv.normalHeight))
9393 }
qml/Stages/WindowedFullscreenPolicy.qmlnew file+41
@@ -0,0 +1,41 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQml 2.2
18+import Unity.Application 0.1
19+
20+// This component will change the state of the surface when the stage is loaded.
21+//
22+// On first surface load; if the surface is set to low chrome & fullscreen, the
23+// state of the window is returned to restored.
24+QtObject {
25+ property bool active: true
26+ property QtObject application: null
27+
28+ readonly property var lastSurface: application && application.session ?
29+ application.session.lastSurface : null
30+ property bool _firstTimeSurface: true
31+
32+ onLastSurfaceChanged: {
33+ if (!active || !lastSurface) return;
34+ if (!_firstTimeSurface) return;
35+ _firstTimeSurface = false;
36+
37+ if (lastSurface.state === Mir.FullscreenState && lastSurface.shellChrome === Mir.LowChrome) {
38+ lastSurface.state = Mir.RestoredState;
39+ }
40+ }
41+}
qml/Stages/graphics/sidestage_drag.svgnew file+207
@@ -0,0 +1,207 @@
1+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2+<!-- Created with Inkscape (http://www.inkscape.org/) -->
3+
4+<svg
5+ xmlns:dc="http://purl.org/dc/elements/1.1/"
6+ xmlns:cc="http://creativecommons.org/ns#"
7+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
8+ xmlns:svg="http://www.w3.org/2000/svg"
9+ xmlns="http://www.w3.org/2000/svg"
10+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
11+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
12+ width="540"
13+ height="324"
14+ id="svg4874"
15+ version="1.1"
16+ inkscape:version="0.91+devel r"
17+ viewBox="0 0 540 324"
18+ sodipodi:docname="gesture_3f_touch.svg">
19+ <defs
20+ id="defs4876" />
21+ <sodipodi:namedview
22+ id="base"
23+ pagecolor="#292929"
24+ bordercolor="#666666"
25+ borderopacity="1.0"
26+ inkscape:pageopacity="1"
27+ inkscape:pageshadow="2"
28+ inkscape:zoom="1.1785993"
29+ inkscape:cx="-19.084613"
30+ inkscape:cy="11.707485"
31+ inkscape:document-units="px"
32+ inkscape:current-layer="g4780"
33+ showgrid="false"
34+ showborder="true"
35+ fit-margin-top="0"
36+ fit-margin-left="0"
37+ fit-margin-right="0"
38+ fit-margin-bottom="0"
39+ inkscape:snap-bbox="true"
40+ inkscape:bbox-paths="true"
41+ inkscape:bbox-nodes="true"
42+ inkscape:snap-bbox-edge-midpoints="true"
43+ inkscape:snap-bbox-midpoints="true"
44+ inkscape:object-paths="true"
45+ inkscape:snap-intersection-paths="true"
46+ inkscape:object-nodes="true"
47+ inkscape:snap-smooth-nodes="true"
48+ inkscape:snap-midpoints="true"
49+ inkscape:snap-object-midpoints="true"
50+ inkscape:snap-center="true"
51+ showguides="false"
52+ inkscape:guide-bbox="true"
53+ inkscape:snap-global="true"
54+ inkscape:snap-others="false"
55+ inkscape:snap-page="true">
56+ <inkscape:grid
57+ type="xygrid"
58+ id="grid5451"
59+ empspacing="8"
60+ originx="-2.7715014e-06"
61+ originy="-2.5124622e-06" />
62+ <sodipodi:guide
63+ orientation="1,0"
64+ position="7.9999972,-8.0000025"
65+ id="guide4063"
66+ inkscape:locked="false" />
67+ <sodipodi:guide
68+ orientation="1,0"
69+ position="3.9999972,-8.0000025"
70+ id="guide4065"
71+ inkscape:locked="false" />
72+ <sodipodi:guide
73+ orientation="0,1"
74+ position="-8.0000027,87.999998"
75+ id="guide4067"
76+ inkscape:locked="false" />
77+ <sodipodi:guide
78+ orientation="0,1"
79+ position="-8.0000027,91.999998"
80+ id="guide4069"
81+ inkscape:locked="false" />
82+ <sodipodi:guide
83+ orientation="0,1"
84+ position="104,3.9999975"
85+ id="guide4071"
86+ inkscape:locked="false" />
87+ <sodipodi:guide
88+ orientation="0,1"
89+ position="-5.0000027,7.9999975"
90+ id="guide4073"
91+ inkscape:locked="false" />
92+ <sodipodi:guide
93+ orientation="1,0"
94+ position="91.999996,-8.0000025"
95+ id="guide4075"
96+ inkscape:locked="false" />
97+ <sodipodi:guide
98+ orientation="1,0"
99+ position="87.999997,-8.0000025"
100+ id="guide4077"
101+ inkscape:locked="false" />
102+ <sodipodi:guide
103+ orientation="0,1"
104+ position="-8.0000027,83.999998"
105+ id="guide4074"
106+ inkscape:locked="false" />
107+ <sodipodi:guide
108+ orientation="1,0"
109+ position="11.999997,-8.0000025"
110+ id="guide4076"
111+ inkscape:locked="false" />
112+ <sodipodi:guide
113+ orientation="0,1"
114+ position="-5.0000027,11.999997"
115+ id="guide4078"
116+ inkscape:locked="false" />
117+ <sodipodi:guide
118+ orientation="1,0"
119+ position="83.999997,-9.0000025"
120+ id="guide4080"
121+ inkscape:locked="false" />
122+ <sodipodi:guide
123+ position="47.999997,-8.0000025"
124+ orientation="1,0"
125+ id="guide4170"
126+ inkscape:locked="false" />
127+ <sodipodi:guide
128+ position="-8.0000027,47.999997"
129+ orientation="0,1"
130+ id="guide4172"
131+ inkscape:locked="false" />
132+ </sodipodi:namedview>
133+ <metadata
134+ id="metadata4879">
135+ <rdf:RDF>
136+ <cc:Work
137+ rdf:about="">
138+ <dc:format>image/svg+xml</dc:format>
139+ <dc:type
140+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
141+ <dc:title></dc:title>
142+ </cc:Work>
143+ </rdf:RDF>
144+ </metadata>
145+ <g
146+ inkscape:label="Layer 1"
147+ inkscape:groupmode="layer"
148+ id="layer1"
149+ transform="translate(67.857143,149.49498)">
150+ <g
151+ transform="matrix(0,-1,-1,0,373.50506,516.50504)"
152+ id="g4845"
153+ style="display:inline">
154+ <g
155+ inkscape:export-ydpi="90"
156+ inkscape:export-xdpi="90"
157+ inkscape:export-filename="next01.png"
158+ transform="matrix(-0.9996045,0,0,1,575.94296,-611.00001)"
159+ id="g4778"
160+ inkscape:label="Layer 1">
161+ <g
162+ transform="matrix(-1,0,0,1,575.99999,611)"
163+ id="g4780"
164+ style="display:inline">
165+ <path
166+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:53.6204567px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:end;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:end;display:inline;fill:none;fill-opacity:1;stroke:#f7f7f7;stroke-width:4.00079107;stroke-miterlimit:4;stroke-dasharray:none"
167+ d="m 526.90578,343.72628 c 14.3803,3.85163 8.37809,26.10701 -5.96811,22.26448 l -10e-6,10e-6 -50.91437,-13.63709 67.92084,18.21108 c 15.93089,4.26693 9.28148,28.92202 -6.61162,24.66516 l -10e-6,1e-5 -67.93497,-18.15834 58.38757,15.63874 c 15.85637,4.24702 9.23761,28.78845 -6.58115,24.5515 v -2e-5 l 1e-5,-2e-5 -71.03537,-19.00646 -33.09607,-8.85524 -0.18781,-0.0503 v 0 l -6.91408,-1.85189 18.07353,23.47068 c 8.45364,10.48525 11.9521,17.66446 -3.54852,27.89697 l -43.91145,-37.31132 c -28.38683,-22.97811 -35.17793,-41.86133 -27.95127,-70.42985 10.25002,-40.5206 40.5632,-49.89857 76.24862,-40.34047 4.5224,1.21129 36.24839,9.63672 40.29164,10.71968 14.0412,3.76084 12.54552,13.74563 6.76936,26.96549 z"
168+ id="path4845-6"
169+ inkscape:connector-curvature="0"
170+ sodipodi:nodetypes="ccccccccccccccccccccssscc" />
171+ <path
172+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.25;fill:#f7f7f7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.51758671;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
173+ d="m 571.27587,383.27233 a 35.064884,35.05101 0 0 1 -16.02978,29.39649 35.064884,35.05101 0 0 1 -34.38665,28.3418 35.064884,35.05101 0 0 1 -34.85362,-31.56055 l 29.19904,7.8125 c 15.81876,4.23695 22.4371,-20.30571 6.58073,-24.55274 l -28.91769,-7.74609 a 35.064884,35.05101 0 0 1 0.01,-0.0137 l 38.45466,10.2793 c 15.8931,4.25686 22.54288,-20.39714 6.61199,-24.66407 l -48.41758,-12.98242 a 35.064884,35.05101 0 0 1 0,-0.006 l 31.41086,8.41407 c 14.3462,3.84253 20.34945,-18.414 5.96915,-22.26563 l -31.70199,-8.49023 a 35.064884,35.05101 0 0 1 29.21468,-15.70704 35.064884,35.05101 0 0 1 35.06465,35.05274 35.064884,35.05101 0 0 1 -0.0879,2.45312 35.064884,35.05101 0 0 1 11.8797,26.23828 z"
174+ id="ellipse4239-1"
175+ inkscape:connector-curvature="0" />
176+ <g
177+ style="display:inline"
178+ id="g5379"
179+ transform="matrix(-0.99999722,0,0,-0.99999722,645.25394,919.46958)">
180+ <path
181+ style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:#f7f7f7;stroke-width:4.00079107;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
182+ id="path4116"
183+ sodipodi:type="arc"
184+ sodipodi:cx="808.72699"
185+ sodipodi:cy="244.79436"
186+ sodipodi:rx="250.36082"
187+ sodipodi:ry="263.63275"
188+ sodipodi:start="3.8048178"
189+ sodipodi:end="5.6286868"
190+ sodipodi:open="true"
191+ d="M 611.43997,82.485822 A 250.36082,263.63275 0 0 1 809.81939,-18.835885 250.36082,263.63275 0 0 1 1007.3516,84.304899"
192+ inkscape:transform-center-x="-17.044876"
193+ inkscape:transform-center-y="-0.0015034578"
194+ transform="matrix(0,1,1,0,0,0)" />
195+ <path
196+ inkscape:transform-center-y="4.9738279"
197+ inkscape:transform-center-x="2.0443183"
198+ inkscape:connector-curvature="0"
199+ id="path4198"
200+ d="m 73.559638,1014.0226 14.78221,-18.91287 c 0,0 6.80763,12.50667 9.95238,23.00047 -10.9469,-0.5114 -24.73459,-4.0876 -24.73459,-4.0876 z"
201+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#f7f7f7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.00059342;marker:none;enable-background:accumulate" />
202+ </g>
203+ </g>
204+ </g>
205+ </g>
206+ </g>
207+</svg>
qml/Stages/graphics/sidestage_open.svgnew file+181
@@ -0,0 +1,181 @@
1+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2+<!-- Created with Inkscape (http://www.inkscape.org/) -->
3+
4+<svg
5+ xmlns:dc="http://purl.org/dc/elements/1.1/"
6+ xmlns:cc="http://creativecommons.org/ns#"
7+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
8+ xmlns:svg="http://www.w3.org/2000/svg"
9+ xmlns="http://www.w3.org/2000/svg"
10+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
11+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
12+ width="162"
13+ height="230"
14+ id="svg4874"
15+ version="1.1"
16+ inkscape:version="0.91+devel r"
17+ viewBox="0 0 162 230"
18+ sodipodi:docname="gesture_3f_tap.svg"
19+ style="shape-rendering:crispEdges">
20+ <defs
21+ id="defs4876" />
22+ <sodipodi:namedview
23+ id="base"
24+ pagecolor="#292929"
25+ bordercolor="#666666"
26+ borderopacity="1.0"
27+ inkscape:pageopacity="1"
28+ inkscape:pageshadow="2"
29+ inkscape:zoom="1.8415614"
30+ inkscape:cx="162.13161"
31+ inkscape:cy="116.01485"
32+ inkscape:document-units="px"
33+ inkscape:current-layer="g4780"
34+ showgrid="false"
35+ showborder="true"
36+ fit-margin-top="0"
37+ fit-margin-left="0"
38+ fit-margin-right="0"
39+ fit-margin-bottom="0"
40+ inkscape:snap-bbox="true"
41+ inkscape:bbox-paths="true"
42+ inkscape:bbox-nodes="true"
43+ inkscape:snap-bbox-edge-midpoints="true"
44+ inkscape:snap-bbox-midpoints="true"
45+ inkscape:object-paths="true"
46+ inkscape:snap-intersection-paths="true"
47+ inkscape:object-nodes="true"
48+ inkscape:snap-smooth-nodes="true"
49+ inkscape:snap-midpoints="true"
50+ inkscape:snap-object-midpoints="true"
51+ inkscape:snap-center="true"
52+ showguides="false"
53+ inkscape:guide-bbox="true"
54+ inkscape:snap-global="false"
55+ inkscape:snap-others="false"
56+ inkscape:snap-page="true">
57+ <inkscape:grid
58+ type="xygrid"
59+ id="grid5451"
60+ empspacing="8"
61+ originx="-2.7715014e-06"
62+ originy="-2.5124622e-06" />
63+ <sodipodi:guide
64+ orientation="1,0"
65+ position="7.9999972,-8.0000026"
66+ id="guide4063"
67+ inkscape:locked="false" />
68+ <sodipodi:guide
69+ orientation="1,0"
70+ position="3.9999972,-8.0000026"
71+ id="guide4065"
72+ inkscape:locked="false" />
73+ <sodipodi:guide
74+ orientation="0,1"
75+ position="-8.0000027,87.999998"
76+ id="guide4067"
77+ inkscape:locked="false" />
78+ <sodipodi:guide
79+ orientation="0,1"
80+ position="-8.0000027,91.999998"
81+ id="guide4069"
82+ inkscape:locked="false" />
83+ <sodipodi:guide
84+ orientation="0,1"
85+ position="104,3.9999975"
86+ id="guide4071"
87+ inkscape:locked="false" />
88+ <sodipodi:guide
89+ orientation="0,1"
90+ position="-5.0000027,7.9999975"
91+ id="guide4073"
92+ inkscape:locked="false" />
93+ <sodipodi:guide
94+ orientation="1,0"
95+ position="91.999997,-8.0000026"
96+ id="guide4075"
97+ inkscape:locked="false" />
98+ <sodipodi:guide
99+ orientation="1,0"
100+ position="87.999997,-8.0000026"
101+ id="guide4077"
102+ inkscape:locked="false" />
103+ <sodipodi:guide
104+ orientation="0,1"
105+ position="-8.0000027,83.999998"
106+ id="guide4074"
107+ inkscape:locked="false" />
108+ <sodipodi:guide
109+ orientation="1,0"
110+ position="11.999997,-8.0000026"
111+ id="guide4076"
112+ inkscape:locked="false" />
113+ <sodipodi:guide
114+ orientation="0,1"
115+ position="-5.0000027,11.999997"
116+ id="guide4078"
117+ inkscape:locked="false" />
118+ <sodipodi:guide
119+ orientation="1,0"
120+ position="83.999997,-9.0000026"
121+ id="guide4080"
122+ inkscape:locked="false" />
123+ <sodipodi:guide
124+ position="47.999997,-8.0000026"
125+ orientation="1,0"
126+ id="guide4170"
127+ inkscape:locked="false" />
128+ <sodipodi:guide
129+ position="-8.0000027,47.999997"
130+ orientation="0,1"
131+ id="guide4172"
132+ inkscape:locked="false" />
133+ </sodipodi:namedview>
134+ <metadata
135+ id="metadata4879">
136+ <rdf:RDF>
137+ <cc:Work
138+ rdf:about="">
139+ <dc:format>image/svg+xml</dc:format>
140+ <dc:type
141+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
142+ <dc:title></dc:title>
143+ </cc:Work>
144+ </rdf:RDF>
145+ </metadata>
146+ <g
147+ inkscape:label="Layer 1"
148+ inkscape:groupmode="layer"
149+ id="layer1"
150+ transform="translate(67.857143,55.494966)">
151+ <g
152+ transform="matrix(0,-1,-1,0,373.50506,516.50504)"
153+ id="g4845"
154+ style="display:inline">
155+ <g
156+ inkscape:export-ydpi="90"
157+ inkscape:export-xdpi="90"
158+ inkscape:export-filename="next01.png"
159+ transform="matrix(-0.9996045,0,0,1,575.94296,-611.00001)"
160+ id="g4778"
161+ inkscape:label="Layer 1">
162+ <g
163+ transform="matrix(-1,0,0,1,575.99999,611)"
164+ id="g4780"
165+ style="display:inline">
166+ <path
167+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:53.6204567px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:end;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:end;display:inline;fill:none;fill-opacity:1;stroke:#f7f7f7;stroke-width:4.00079107;stroke-miterlimit:4;stroke-dasharray:none"
168+ d="m 526.90578,343.72628 c 14.3803,3.85163 8.37809,26.10701 -5.96811,22.26448 l -10e-6,10e-6 -50.91437,-13.63709 67.92084,18.21108 c 15.93089,4.26693 9.28148,28.92202 -6.61162,24.66516 l -10e-6,1e-5 -67.93497,-18.15834 58.38757,15.63874 c 15.85637,4.24702 9.23761,28.78845 -6.58115,24.5515 v -2e-5 l 1e-5,-2e-5 -71.03537,-19.00646 -33.09607,-8.85524 -0.18781,-0.0503 v 0 l -6.91408,-1.85189 18.07353,23.47068 c 8.45364,10.48525 11.9521,17.66446 -3.54852,27.89697 l -43.91145,-37.31132 c -28.38683,-22.97811 -35.17793,-41.86133 -27.95127,-70.42985 10.25002,-40.5206 40.5632,-49.89857 76.24862,-40.34047 4.5224,1.21129 36.24839,9.63672 40.29164,10.71968 14.0412,3.76084 12.54552,13.74563 6.76936,26.96549 z"
169+ id="path4845-6"
170+ inkscape:connector-curvature="0"
171+ sodipodi:nodetypes="ccccccccccccccccccccssscc" />
172+ <path
173+ style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.5;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#f7f7f7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.99999976;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
174+ d="m 571.45563,383.2743 c -0.0175,12.04505 -6.22338,23.19765 -16.35804,29.64062 -3.3052,16.24493 -17.57029,28.04101 -34.23815,28.04493 -18.13447,4.3e-4 -33.08059,-13.83538 -34.83995,-31.50586 l 4.16766,1.11523 c 2.22233,14.95164 15.08799,26.39099 30.67229,26.39063 15.06147,-0.004 27.92054,-10.79228 30.53552,-25.61915 l 0.15631,-0.89648 0.78156,-0.46875 c 9.37156,-5.64255 15.10396,-15.76543 15.12121,-26.70117 v -0.004 c -0.0108,-9.30873 -4.17362,-18.12357 -11.35605,-24.04883 l -0.8265,-0.68359 0.10942,-1.06641 c 0.10013,-0.96319 0.1562,-1.9301 0.16608,-2.89843 -0.004,-17.20453 -13.91519,-31.10752 -31.12755,-31.10743 -10.37812,-5e-5 -19.55027,5.05891 -25.20333,12.84375 l -4.0856,-1.09375 c 6.29481,-9.48451 17.06827,-15.75006 29.28893,-15.75 19.37764,-1e-4 35.12924,15.74527 35.12913,35.11524 v 0.01 0.01 c -0.008,0.77822 -0.0716,1.55426 -0.13091,2.33008 7.57765,6.66645 12.02745,16.21382 12.03797,26.33984 v 0.002 z m -74.3634,2.80273 c -0.004,0.005 -0.008,0.0106 -0.0117,0.0156 l -4.15595,-1.11328 c 0.003,-0.004 0.006,-0.009 0.01,-0.0137 z m -3.51507,-27.4082 -4.14421,-1.10938 c -1.8e-4,-0.002 1.7e-4,-0.004 0,-0.006 l 4.14226,1.10937 c 2.7e-4,0.002 0.002,0.004 0.002,0.006 z"
175+ id="ellipse4488"
176+ inkscape:connector-curvature="0" />
177+ </g>
178+ </g>
179+ </g>
180+ </g>
181+</svg>
qml/Tutorial/Arrow.qmldeleted−56
@@ -1,56 +0,0 @@
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.4
18−import Ubuntu.Components 1.3
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/InactivityTimer.qmlnew file+62
@@ -0,0 +1,62 @@
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.4
18+import Ubuntu.Components 1.3
19+
20+Item {
21+ readonly property alias running: internalTimer.running
22+ property alias interval: internalTimer.interval
23+ property var page
24+ property int lastInputTimestamp
25+
26+ function start() {
27+ internalTimer.start();
28+ }
29+
30+ ////
31+
32+ onLastInputTimestampChanged: {
33+ if (internalTimer.running) {
34+ internalTimer.restart();
35+ }
36+ }
37+
38+ Connections {
39+ target: page
40+ onIsReadyChanged: {
41+ if (page.isReady && internalTimer.running) {
42+ internalTimer.restart();
43+ }
44+ }
45+ }
46+
47+ Timer {
48+ id: internalTimer
49+
50+ interval: 3000
51+
52+ onTriggered: {
53+ if (page.isReady) {
54+ if (!page.shown) {
55+ page.show();
56+ }
57+ } else if (!page.skipped) {
58+ restart();
59+ }
60+ }
61+ }
62+}
qml/Tutorial/Slider.qmldeleted−123
@@ -1,123 +0,0 @@
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.4
18−import Ubuntu.Components 1.3
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/Tutorial.qml+39−14
@@ -16,21 +16,29 @@
1616
1717import QtQuick 2.4
1818import Ubuntu.Components 1.3
19+import AccountsService 0.1
20+
21+/**
22+ * This object is always present, so it should be lean and mean. It will
23+ * use a Loader to create the heavier tutorial pages if needed.
24+ */
1925
2026Item {
2127 id: root
2228
2329 property alias active: loader.active
24− property bool paused
25− property real edgeSize
2630
2731 property Item launcher
2832 property Item panel
33+ property Item stage
34+ property string usageScenario
35+ property bool paused
36+ property bool keyboardVisible
37+ property int lastInputTimestamp
2938
3039 readonly property bool launcherEnabled: loader.item ? loader.item.launcherEnabled : true
3140 readonly property bool spreadEnabled: loader.item ? loader.item.spreadEnabled : true
3241 readonly property bool panelEnabled: loader.item ? loader.item.panelEnabled : true
33− readonly property bool panelContentEnabled: loader.item ? loader.item.panelContentEnabled : true
3442 readonly property bool running: loader.item ? loader.item.running : false
3543
3644 function finish() {
@@ -39,40 +47,57 @@ Item {
3947 }
4048 }
4149
42− signal finished()
43−
4450 Loader {
4551 id: loader
4652 anchors.fill: parent
4753 source: "TutorialContent.qml"
54+ active: AccountsService.demoEdges
4855
4956 Binding {
5057 target: loader.item
51− property: "paused"
52− value: root.paused
58+ property: "launcher"
59+ value: root.launcher
5360 }
5461
5562 Binding {
5663 target: loader.item
57− property: "edgeSize"
58− value: root.edgeSize
64+ property: "panel"
65+ value: root.panel
5966 }
6067
6168 Binding {
6269 target: loader.item
63− property: "launcher"
64− value: root.launcher
70+ property: "stage"
71+ value: root.stage
6572 }
6673
6774 Binding {
6875 target: loader.item
69− property: "panel"
70− value: root.panel
76+ property: "usageScenario"
77+ value: root.usageScenario
78+ }
79+
80+ Binding {
81+ target: loader.item
82+ property: "paused"
83+ value: root.paused
84+ }
85+
86+ Binding {
87+ target: loader.item
88+ property: "keyboardVisible"
89+ value: root.keyboardVisible
90+ }
91+
92+ Binding {
93+ target: loader.item
94+ property: "lastInputTimestamp"
95+ value: root.lastInputTimestamp
7196 }
7297
7398 Connections {
7499 target: loader.item
75− onFinished: root.finished()
100+ onFinished: AccountsService.demoEdges = false
76101 }
77102 }
78103}
qml/Tutorial/TutorialBottom.qml+86−66
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2014 Canonical, Ltd.
2+ * Copyright (C) 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
@@ -17,88 +17,108 @@
1717import QtQuick 2.4
1818import Ubuntu.Components 1.3
1919import Ubuntu.Gestures 0.1
20−import "../Components"
21−import "." as LocalComponents
20+import Unity.Application 0.1
2221
2322TutorialPage {
2423 id: root
2524
26− property alias edgeSize: dragArea.height
25+ property string usageScenario
26+ property var stage
27+ property var application: null
2728
28− title: i18n.tr("Open special menus")
29− text: i18n.tr("Swipe up from the bottom edge.")
30− fullTextWidth: true
29+ // This page is a bit fragile. It relies on knowing how the app beneath
30+ // the shell will react to a drag. What we do is put a monitor-only DDA
31+ // at the bottom of the page (so that we know when the drag is finished)
32+ // and pass the events on through to the app. Thus, it sees the drag and
33+ // brings its bottom edge up.
34+ //
35+ // Unfortunately, each app is on its own when implementing the bottom edge
36+ // drag. Most share copied-and-pasted code right now, but they will
37+ // eventually consolidate on a version of DirectionalDragArea that will
38+ // land in the SDK (making our guessing job easier). Though, also in the
39+ // future, this whole bottom tutorial component will also land in the SDK,
40+ // rendering our version here obsolete.
41+ //
42+ // Anyway, for the moment, we base our guesses on the copied-and-pasted
43+ // code used in several of the core apps and only bring this component
44+ // up if we are in those core apps.
3145
32− SequentialAnimation {
33− id: teaseAnimation
34− paused: running && root.paused
35− running: !dragArea.useTouchY && slider.dragOffset === 0
36− loops: Animation.Infinite
46+ readonly property real mainStageWidth: stage.width - sideStageWidth
47+ readonly property real sideStageWidth: root.usageScenario === "tablet" && stage.sideStageVisible ?
48+ stage.sideStageWidth : 0
49+ readonly property bool isMainStageApp: usageScenario !== "tablet" ||
50+ application.stage === ApplicationInfoInterface.MainStage
51+ readonly property real dragAreaHeight: units.gu(3) // based on PageWithBottomEdge.qml
52+ readonly property real targetDistance: height * 0.2 + dragAreaHeight // based on PageWithBottomEdge.qml
3753
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− }
54+ opacityOverride: dragArea.dragging ? 1 - (-dragArea.distance / targetDistance) : 1
55+
56+ mouseArea {
57+ anchors.bottomMargin: root.dragAreaHeight
58+ }
59+
60+ background {
61+ sourceSize.height: 1916
62+ sourceSize.width: 1080
63+ source: Qt.resolvedUrl("graphics/background2.png")
64+ rotation: 180
5065 }
5166
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
67+ arrow {
68+ anchors.bottom: root.bottom
69+ anchors.bottomMargin: units.gu(3)
70+ anchors.horizontalCenter: label.horizontalCenter
71+ anchors.horizontalCenterOffset: -(label.width - label.contentWidth) / 2
72+ rotation: 90
73+ }
6474
65− property real teaseOffset
66− property real dragOffset: dragArea.useTouchY ? -dragArea.touchY : 0
75+ label {
76+ text: !application ? "" :
77+ application.appId === "address-book-app" ?
78+ i18n.tr("Swipe up to add a contact") :
79+ application.appId === "com.ubuntu.calculator_calculator" ?
80+ i18n.tr("Swipe up for favorite calculations") :
81+ application.appId === "dialer-app" ?
82+ i18n.tr("Swipe up for recent calls") :
83+ application.appId === "messaging-app" ?
84+ i18n.tr("Swipe up to create a message") :
85+ i18n.tr("Swipe up to manage the app") // shouldn't be used
86+ anchors.bottom: arrow.top
87+ anchors.bottomMargin: units.gu(3)
88+ anchors.left: root.left
89+ anchors.leftMargin: (label.width - label.contentWidth) / 2 + sideMargin +
90+ (isMainStageApp ? 0 : mainStageWidth)
91+ width: (isMainStageApp ? mainStageWidth : sideStageWidth) - sideMargin * 2
6792
68− Behavior on dragOffset {
69− id: offsetAnimation
70− UbuntuNumberAnimation {}
71− }
72− }
73− ]
93+ readonly property real sideMargin: units.gu(4)
7494 }
7595
96+ // Watches drag events but does not intercept them, so that the app beneath
97+ // will still drag the bottom edge up.
7698 DirectionalDragArea {
7799 id: dragArea
100+ monitorOnly: true
78101 direction: Direction.Upwards
79− anchors {
80− bottom: parent.bottom
81− left: parent.left
82− right: parent.right
83− }
84−
85− property bool useTouchY
102+ anchors.left: parent.left
103+ anchors.right: parent.right
104+ anchors.bottom: parent.bottom
105+ height: root.dragAreaHeight
86106
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− }
107+ // Apps currently don't use DDA. DDA will stop a gesture if
108+ // horizontal motion is detected. But our apps won't. So turn off
109+ // that gesture cleverness on our part, it will only get us out of sync.
110+ immediateRecognition: true
111+ }
95112
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− }
113+ MouseArea {
114+ // A second mouse area because in tablet mode, we only want to let the
115+ // user drag up on one of the stages, not both. So we want to cover
116+ // the second bottom edge with an event eater.
117+ enabled: root.usageScenario === "tablet"
118+ height: root.dragAreaHeight
119+ width: isMainStageApp ? sideStageWidth : mainStageWidth
120+ anchors.bottom: parent.bottom
121+ anchors.left: isMainStageApp ? undefined : parent.left
122+ anchors.right: isMainStageApp ? parent.right : undefined
103123 }
104124}
qml/Tutorial/TutorialBottomFinish.qmldeleted−41
@@ -1,41 +0,0 @@
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.4
18−import Ubuntu.Components 1.3
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.qml+227−55
@@ -16,103 +16,275 @@
1616
1717import QtQuick 2.4
1818import Ubuntu.Components 1.3
19+import AccountsService 0.1
20+import Unity.Application 0.1
1921
2022Item {
2123 id: root
2224
2325 property Item launcher
2426 property Item panel
27+ property Item stage
28+ property string usageScenario
29+ property bool paused
30+ property bool keyboardVisible
31+ property var lastInputTimestamp
2532
26− readonly property bool launcherEnabled: !running ||
27− (!paused && tutorialLeft.shown)
28− readonly property bool spreadEnabled: !running
29− readonly property bool panelEnabled: !running
30− readonly property bool panelContentEnabled: !running
31− readonly property alias running: d.running
32−
33− property bool paused: false
34− property real edgeSize
33+ readonly property bool launcherEnabled: !running
34+ || tutorialLeftLoader.shown
35+ || tutorialLeftLongLoader.shown
36+ readonly property bool spreadEnabled: !running || tutorialRightLoader.shown
37+ readonly property bool panelEnabled: !running || tutorialTopLoader.shown
38+ readonly property bool running: tutorialLeftLoader.shown
39+ || tutorialTopLoader.shown
40+ || tutorialRightLoader.shown
41+ || tutorialBottomLoader.shown
3542
3643 signal finished()
3744
3845 function finish() {
39− d.stop();
4046 finished();
4147 }
4248
4349 ////
4450
45− Component.onCompleted: {
46− d.start();
47− }
48−
4951 QtObject {
5052 id: d
5153
52− property bool running
54+ // We allow "" because it is briefly empty on startup, and we don't
55+ // want to improperly skip any mobile tutorials.
56+ property bool mobileScenario: root.usageScenario === "" ||
57+ root.usageScenario === "phone" ||
58+ root.usageScenario === "tablet"
5359
54− function stop() {
55− running = false;
56− }
60+ property var focusedApp: ApplicationManager.focusedApplicationId
61+ ? ApplicationManager.findApplication(ApplicationManager.focusedApplicationId)
62+ : null
5763
58− function start() {
59− running = true;
60− tutorialLeft.show();
64+ function haveShown(tutorialId) {
65+ return AccountsService.demoEdgesCompleted.indexOf(tutorialId) != -1;
6166 }
67+
68+ property bool endPointsFinished: tutorialRightLoader.skipped &&
69+ tutorialBottomLoader.skipped
70+ onEndPointsFinishedChanged: if (endPointsFinished) root.finish()
6271 }
6372
64− TutorialLeft {
65− id: tutorialLeft
66− objectName: "tutorialLeft"
73+ Loader {
74+ id: tutorialLeftLoader
75+ objectName: "tutorialLeftLoader"
6776 anchors.fill: parent
68− launcher: root.launcher
69− paused: !shown || root.paused
7077
71− onFinished: tutorialLeftFinish.show()
78+ readonly property bool skipped: !d.mobileScenario || d.haveShown("left")
79+ readonly property bool shown: item && item.shown
80+ active: !skipped || (item && item.visible)
81+ onSkippedChanged: if (skipped && shown) item.hide()
82+
83+ sourceComponent: TutorialLeft {
84+ id: tutorialLeft
85+ objectName: "tutorialLeft"
86+ anchors.fill: parent
87+ launcher: root.launcher
88+ hides: [launcher, panel.indicators]
89+ paused: root.paused
90+
91+ isReady: !tutorialLeftLoader.skipped && !paused && !keyboardVisible &&
92+ !tutorialBottomLoader.shown && !tutorialBottomLoader.mightShow
93+
94+ // Use an idle timer here, because when constructed, all our isReady variables will be false.
95+ // Qml needs a moment to copy their values from Tutorial.qml. So we idle it and recheck.
96+ Timer {
97+ id: tutorialLeftTimer
98+ interval: 0
99+ onTriggered: if (tutorialLeft.isReady && !tutorialLeft.shown) tutorialLeft.show()
100+ }
101+
102+ onIsReadyChanged: if (isReady && !shown) tutorialLeftTimer.start()
103+ onFinished: AccountsService.markDemoEdgeCompleted("left")
104+ }
72105 }
73106
74− TutorialLeftFinish {
75− id: tutorialLeftFinish
76− objectName: "tutorialLeftFinish"
107+ Loader {
108+ id: tutorialLeftLongLoader
109+ objectName: "tutorialLeftLongLoader"
77110 anchors.fill: parent
78− textXOffset: root.launcher.panelWidth
79− paused: !shown || root.paused
80− text: i18n.tr("Tap here to continue.")
81111
82− onFinished: {
83− root.launcher.hide();
84− tutorialRight.show();
112+ readonly property bool skipped: !d.mobileScenario || d.haveShown("left-long")
113+ readonly property bool shown: item && item.shown
114+ active: !skipped || (item && item.visible)
115+ onSkippedChanged: if (skipped && shown) item.hide()
116+
117+ sourceComponent: TutorialLeftLong {
118+ id: tutorialLeftLong
119+ objectName: "tutorialLeftLong"
120+ anchors.fill: parent
121+ launcher: root.launcher
122+ hides: [launcher, panel.indicators]
123+ paused: root.paused
124+
125+ skipped: tutorialLeftLongLoader.skipped
126+ isReady: tutorialLeftLoader.skipped && !skipped && !paused && !keyboardVisible &&
127+ !tutorialBottomLoader.shown && !tutorialBottomLoader.mightShow
128+
129+ Timer {
130+ id: tutorialLeftLongTimer
131+ objectName: "tutorialLeftLongTimer"
132+ interval: 5000
133+ onTriggered: {
134+ if (parent.isReady) {
135+ if (!parent.shown) {
136+ parent.show();
137+ }
138+ } else if (!parent.skipped) {
139+ restart();
140+ }
141+ }
142+ }
143+
144+ onIsReadyChanged: if (isReady && !shown) tutorialLeftLongTimer.start()
145+ onFinished: AccountsService.markDemoEdgeCompleted("left-long")
85146 }
86147 }
87148
88− TutorialRight {
89− id: tutorialRight
90− objectName: "tutorialRight"
149+ Loader {
150+ id: tutorialTopLoader
151+ objectName: "tutorialTopLoader"
91152 anchors.fill: parent
92− edgeSize: root.edgeSize
93− panel: root.panel
94− paused: !shown || root.paused
95153
96− onFinished: tutorialBottom.show()
154+ readonly property bool skipped: !d.mobileScenario || d.haveShown("top")
155+ readonly property bool shown: item && item.shown
156+ active: !skipped || (item && item.visible)
157+ onSkippedChanged: if (skipped && shown) item.hide()
158+
159+ sourceComponent: TutorialTop {
160+ id: tutorialTop
161+ objectName: "tutorialTop"
162+ anchors.fill: parent
163+ panel: root.panel
164+ hides: [launcher, panel.indicators]
165+ paused: root.paused
166+
167+ skipped: tutorialTopLoader.skipped
168+ isReady: tutorialLeftLongLoader.skipped && !skipped && !paused && !keyboardVisible &&
169+ !tutorialBottomLoader.shown && !tutorialBottomLoader.mightShow
170+
171+ // We fire 30s after left edge tutorial, with at least 3s of inactivity
172+
173+ InactivityTimer {
174+ id: tutorialTopInactivityTimer
175+ lastInputTimestamp: root.lastInputTimestamp
176+ page: parent
177+ }
178+
179+ Timer {
180+ id: tutorialTopTimer
181+ objectName: "tutorialTopTimer"
182+ interval: 27000
183+ onTriggered: tutorialTopInactivityTimer.start()
184+ }
185+
186+ onIsReadyChanged: if (isReady && !shown) tutorialTopTimer.start()
187+ onFinished: AccountsService.markDemoEdgeCompleted("top")
188+ }
97189 }
98190
99− TutorialBottom {
100− id: tutorialBottom
101− objectName: "tutorialBottom"
191+ Loader {
192+ id: tutorialRightLoader
193+ objectName: "tutorialRightLoader"
102194 anchors.fill: parent
103− edgeSize: root.edgeSize
104− paused: !shown || root.paused
105195
106− onFinished: tutorialBottomFinish.show()
196+ readonly property bool skipped: d.haveShown("right")
197+ readonly property bool shown: item && item.shown
198+ active: !skipped || (item && item.visible)
199+ onSkippedChanged: if (skipped && shown) item.hide()
200+
201+ sourceComponent: TutorialRight {
202+ id: tutorialRight
203+ objectName: "tutorialRight"
204+ anchors.fill: parent
205+ stage: root.stage
206+ usageScenario: root.usageScenario
207+ hides: [launcher, panel.indicators]
208+ paused: root.paused
209+
210+ skipped: tutorialRightLoader.skipped
211+ isReady: tutorialTopLoader.skipped && !skipped && !paused && !keyboardVisible &&
212+ !tutorialBottomLoader.shown && !tutorialBottomLoader.mightShow &&
213+ ApplicationManager.count >= 3
214+
215+ InactivityTimer {
216+ id: tutorialRightInactivityTimer
217+ objectName: "tutorialRightInactivityTimer"
218+ lastInputTimestamp: root.lastInputTimestamp
219+ page: parent
220+ }
221+
222+ Connections {
223+ target: d
224+ onFocusedAppChanged: {
225+ if (tutorialRight.isReady && !tutorialRight.shown && d.focusedApp
226+ && d.focusedApp.state === ApplicationInfoInterface.Starting) {
227+ tutorialRight.show();
228+ }
229+ }
230+ }
231+
232+ onIsReadyChanged: if (isReady && !shown) tutorialRightInactivityTimer.start()
233+ onFinished: AccountsService.markDemoEdgeCompleted("right")
234+ }
107235 }
108236
109− TutorialBottomFinish {
110− id: tutorialBottomFinish
111− objectName: "tutorialBottomFinish"
237+ Loader {
238+ id: tutorialBottomLoader
239+ objectName: "tutorialBottomLoader"
112240 anchors.fill: parent
113− backgroundFadesOut: true
114− paused: !shown || root.paused
115241
116− onFinished: root.finish()
242+ // See TutorialBottom.qml for an explanation of why we only support
243+ // certain apps.
244+ readonly property var supportedApps: ["address-book-app",
245+ "com.ubuntu.calculator_calculator",
246+ "dialer-app",
247+ "messaging-app"]
248+ readonly property bool skipped: {
249+ if (!d.mobileScenario) {
250+ return true;
251+ }
252+ for (var i = 0; i < supportedApps.length; i++) {
253+ if (!d.haveShown("bottom-" + supportedApps[i])) {
254+ return false;
255+ }
256+ }
257+ return true;
258+ }
259+ readonly property bool shown: item && item.shown
260+ readonly property bool haveShownFocusedApp: d.focusedApp &&
261+ d.haveShown("bottom-" + d.focusedApp.appId)
262+ readonly property bool mightShow: !skipped && d.focusedApp &&
263+ supportedApps.indexOf(d.focusedApp.appId) !== -1 &&
264+ !haveShownFocusedApp
265+ active: !skipped || (item && item.visible)
266+ onHaveShownFocusedAppChanged: if (haveShownFocusedApp && shown) hide()
267+ onSkippedChanged: if (skipped && shown) item.hide()
268+
269+ sourceComponent: TutorialBottom {
270+ id: tutorialBottom
271+ objectName: "tutorialBottom"
272+ anchors.fill: parent
273+ hides: [launcher, panel.indicators]
274+ paused: root.paused
275+ usageScenario: root.usageScenario
276+ stage: root.stage
277+ application: d.focusedApp
278+
279+ skipped: tutorialBottomLoader.skipped
280+ isReady: !tutorialBottomLoader.skipped && !paused && !keyboardVisible &&
281+ !tutorialLeftLoader.shown && !tutorialLeftLongLoader.shown &&
282+ !tutorialTopLoader.shown && !tutorialRightLoader.shown &&
283+ tutorialBottomLoader.mightShow &&
284+ d.focusedApp.state === ApplicationInfoInterface.Running
285+
286+ onIsReadyChanged: if (isReady && !shown) show()
287+ onFinished: AccountsService.markDemoEdgeCompleted("bottom-" + d.focusedApp.appId)
288+ }
117289 }
118290}
qml/Tutorial/TutorialLeft.qml+21−68
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2014,2015 Canonical, Ltd.
2+ * Copyright (C) 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
@@ -23,79 +23,32 @@ TutorialPage {
2323
2424 property var launcher
2525
26− title: i18n.tr("Open the launcher")
27− text: i18n.tr("Short swipe from the left edge.")
26+ opacityOverride: 1 - launcher.visibleWidth / launcher.panelWidth
2827
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− objectName: "teaseAnimation"
50− paused: running && root.paused
51− running: !slider.active && root.launcher.visibleWidth === 0 && root.shown
52− loops: Animation.Infinite
53− property real bounce: 0
54− readonly property real maxBounce: units.gu(2)
55−
56− UbuntuNumberAnimation {
57− target: teaseAnimation
58− property: "bounce"
59− to: teaseAnimation.maxBounce
60− duration: UbuntuAnimation.SleepyDuration
61− }
62− UbuntuNumberAnimation {
63− target: teaseAnimation
64− property: "bounce"
65− to: 0
66− duration: UbuntuAnimation.SleepyDuration
67− }
28+ mouseArea {
29+ anchors.leftMargin: launcher.dragAreaWidth
6830 }
6931
70− Binding {
71− target: root.launcher
72− when: root.shown
73− property: "x"
74− value: Math.min(root.launcher.panelWidth - root.launcher.visibleWidth, teaseAnimation.bounce)
32+ background {
33+ sourceSize.height: 1916
34+ sourceSize.width: 1080
35+ source: Qt.resolvedUrl("graphics/background1.png")
36+ mirror: true
7537 }
7638
77− Timer {
78− id: finishTimer
79− interval: 1
80− onTriggered: {
81− root.hide();
82− root.launcher.x = 0; // make sure to reset launcher before we go
83− }
39+ arrow {
40+ anchors.left: root.left
41+ anchors.leftMargin: units.gu(2)
42+ anchors.verticalCenter: root.verticalCenter
43+ rotation: 180
8444 }
8545
86− foreground {
87− children: [
88− LocalComponents.Slider {
89− id: slider
90− anchors {
91− left: parent.left
92− top: parent.top
93− topMargin: root.textBottom + units.gu(3)
94− }
95− offset: root.launcher.x + root.launcher.visibleWidth + root.launcher.progress
96− active: root.launcher.dragging
97− shortSwipe: true
98− }
99− ]
46+ label {
47+ text: i18n.tr("Swipe from the left edge to open the launcher")
48+ anchors.left: arrow.right
49+ anchors.leftMargin: units.gu(3)
50+ anchors.right: root.right
51+ anchors.rightMargin: units.gu(4)
52+ anchors.verticalCenter: arrow.verticalCenter
10053 }
10154}
qml/Tutorial/TutorialLeftLong.qmlnew file+54
@@ -0,0 +1,54 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import Ubuntu.Components 1.3
19+import "." as LocalComponents
20+
21+TutorialPage {
22+ id: root
23+
24+ property var launcher
25+
26+ opacityOverride: 1 - launcher.dragDistance / launcher.minimizeDistance
27+
28+ mouseArea {
29+ anchors.leftMargin: launcher.dragAreaWidth
30+ }
31+
32+ background {
33+ sourceSize.height: 1916
34+ sourceSize.width: 1080
35+ source: Qt.resolvedUrl("graphics/background1.png")
36+ mirror: true
37+ }
38+
39+ arrow {
40+ anchors.left: root.left
41+ anchors.leftMargin: units.gu(2)
42+ anchors.verticalCenter: root.verticalCenter
43+ rotation: 180
44+ }
45+
46+ label {
47+ text: i18n.tr("Long swipe from the left edge to open the Today scope")
48+ anchors.left: arrow.right
49+ anchors.leftMargin: units.gu(3)
50+ anchors.right: root.right
51+ anchors.rightMargin: units.gu(4)
52+ anchors.verticalCenter: arrow.verticalCenter
53+ }
54+}
qml/Tutorial/TutorialPage.qml+62−186
@@ -21,225 +21,101 @@ import "../Components"
2121Showable {
2222 id: root
2323
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)
24+ property alias arrow: arrow
25+ property alias label: label
26+ property alias background: background
4927 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
28+ property real opacityOverride: 1
29+ property bool paused
30+ property bool skipped
31+ property bool isReady
5732
5833 signal finished()
5934
60− function showError() {
61− errorTimer.start();
62− }
63−
6435 ////
6536
37+ QtObject {
38+ id: d
39+ property bool showOnUnpause
40+ }
41+
6642 visible: false
6743 shown: false
6844
69− property real _foregroundHideOpacity
45+ opacity: Math.max(Math.min(_showOpacity, opacityOverride), 0)
46+ onOpacityOverrideChanged: {
47+ if (opacityOverride <= 0) {
48+ d.showOnUnpause = false;
49+ hide();
50+ }
51+ }
52+ property real _showOpacity: 0
53+
54+ onPausedChanged: {
55+ if (paused && shown) {
56+ d.showOnUnpause = true;
57+ hide();
58+ } else if (!paused && d.showOnUnpause) {
59+ if (isReady) {
60+ show();
61+ } else if (hideAnimation.running) {
62+ hideAnimation.stop();
63+ }
64+ d.showOnUnpause = false;
65+ }
66+ }
7067
7168 showAnimation: StandardAnimation {
72− property: root.backgroundFadesIn ? "opacity" : "_foregroundHideOpacity"
69+ property: "_showOpacity"
7370 from: 0
7471 to: 1
75− duration: root.backgroundFadesIn ? UbuntuAnimation.SleepyDuration : UbuntuAnimation.BriskDuration
72+ duration: UbuntuAnimation.SleepyDuration
7673 onStarted: root.visible = true
7774 }
7875
7976 hideAnimation: StandardAnimation {
80− property: root.backgroundFadesOut ? "opacity" : "_foregroundHideOpacity"
77+ property: "_showOpacity"
8178 to: 0
8279 duration: UbuntuAnimation.BriskDuration
8380 onStopped: {
8481 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;
82+ if (!d.showOnUnpause) {
83+ root.finished();
10284 }
10385 }
10486 }
10587
106− Timer {
107− id: errorTimer
108− interval: 3500
109− }
110−
11188 MouseArea { // eat any errant presses
11289 id: mouseArea
11390 anchors.fill: parent
11491 }
11592
116− Rectangle {
117− anchors.fill: parent
118− color: "black"
119− opacity: 0.82
93+ Image {
94+ id: background
95+ // Use x/y/height/width instead of anchors so that we don't adjust
96+ // the image if the OSK appears.
97+ x: 0
98+ y: 0
99+ height: root.height
100+ width: root.width
101+ fillMode: Image.PreserveAspectCrop
120102 }
121103
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 }
104+ Image {
105+ id: arrow
106+ width: units.gu(1.5)
107+ source: Qt.resolvedUrl("graphics/arrow.svg")
108+ fillMode: Image.PreserveAspectFit
109+ mipmap: true
213110 }
214111
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− }
112+ Label {
113+ id: label
114+ objectName: "tutorialLabel"
115+ fontSize: "large"
116+ font.weight: Font.Light
117+ color: "#333333"
118+ wrapMode: Text.Wrap
119+ lineHeight: 1.2
244120 }
245121}
qml/Tutorial/TutorialRight.qml+30−192
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2014 Canonical, Ltd.
2+ * Copyright (C) 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
@@ -16,208 +16,46 @@
1616
1717import QtQuick 2.4
1818import Ubuntu.Components 1.3
19−import Ubuntu.Gestures 0.1
20−import Unity.Application 0.1
21−import "../Components"
22−import "../Stages"
23−import "." as LocalComponents
2419
2520TutorialPage {
2621 id: root
2722
28− property var panel
29− property alias edgeSize: stage.dragAreaWidth
23+ property var stage
24+ property string usageScenario
3025
31− title: i18n.tr("To view open apps")
32− text: i18n.tr("Long swipe from the right edge.")
26+ // When on phone or tablet, fade out as the drag progresses
27+ opacityOverride: usageScenario === "desktop" ? 1 : 1 - stage.dragProgress * 2
3328
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: -units.gu(2)
46− duration: UbuntuAnimation.SleepyDuration
47− }
48− UbuntuNumberAnimation {
49− target: stage
50− property: "x"
51− to: 0
52− duration: UbuntuAnimation.SleepyDuration
53− }
29+ // Else on desktop, fade out when the spread is shown
30+ Connections {
31+ target: usageScenario === "desktop" ? stage : null
32+ ignoreUnknownSignals: true
33+ onSpreadShownChanged: if (stage.spreadShown && root.shown) root.hide()
5434 }
5535
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: -x
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− ]
36+ mouseArea {
37+ anchors.rightMargin: stage.dragAreaWidth
14838 }
14939
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− }
40+ background {
41+ sourceSize.height: 1916
42+ sourceSize.width: 1080
43+ source: Qt.resolvedUrl("graphics/background1.png")
44+ }
19245
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− }
46+ arrow {
47+ anchors.right: root.right
48+ anchors.rightMargin: units.gu(2)
49+ anchors.verticalCenter: root.verticalCenter
50+ }
20751
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− }
52+ label {
53+ text: root.usageScenario === "desktop" ?
54+ i18n.tr("Hover your mouse on the right edge to view your open apps") :
55+ i18n.tr("Short or long swipe from the right edge to view your open apps")
56+ anchors.right: arrow.left
57+ anchors.rightMargin: units.gu(2) - (label.width - label.contentWidth)
58+ anchors.verticalCenter: arrow.verticalCenter
59+ width: Math.min(units.gu(40), arrow.x - units.gu(4))
22260 }
22361}
qml/Tutorial/TutorialTop.qmlnew file+61
@@ -0,0 +1,61 @@
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.4
18+import Ubuntu.Components 1.3
19+import "." as LocalComponents
20+
21+TutorialPage {
22+ id: root
23+
24+ property var panel
25+
26+ opacityOverride: 1 - panel.indicators.unitProgress
27+
28+ QtObject {
29+ id: d
30+ readonly property bool landscape: root.width > units.gu(50)
31+ readonly property real columnWidth: landscape ? panel.indicators.width : root.width
32+ }
33+
34+ mouseArea {
35+ anchors.topMargin: panel.indicators.minimizedPanelHeight
36+ }
37+
38+ background {
39+ sourceSize.height: 1916
40+ sourceSize.width: 1080
41+ source: Qt.resolvedUrl("graphics/background2.png")
42+ }
43+
44+ arrow {
45+ anchors.top: root.top
46+ anchors.topMargin: units.gu(4)
47+ anchors.horizontalCenter: root.right
48+ anchors.horizontalCenterOffset: - d.columnWidth / 2
49+ rotation: -90
50+ }
51+
52+ label {
53+ text: d.landscape ? i18n.tr("Swipe from the top right edge to open the notification bar")
54+ : i18n.tr("Swipe from the top edge to open the notification bar")
55+ anchors.top: arrow.bottom
56+ anchors.topMargin: units.gu(3)
57+ anchors.horizontalCenter: arrow.horizontalCenter
58+ anchors.horizontalCenterOffset: (label.width - label.contentWidth) / 2
59+ width: d.columnWidth - units.gu(8)
60+ }
61+}
qml/Tutorial/graphics/arrow.svgnew file+19
@@ -0,0 +1,19 @@
1+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2+<svg width="35px" height="106px" viewBox="0 0 35 106" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
3+ <!-- Generator: Sketch 3.3.3 (12072) - http://www.bohemiancoding.com/sketch -->
4+ <title>left swipe arrow</title>
5+ <desc>Created with Sketch.</desc>
6+ <defs></defs>
7+ <g id="Final-design" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
8+ <g id="1.Left-swipe" sketch:type="MSArtboardGroup" transform="translate(-54.000000, -906.000000)">
9+ <g id="left-swipe-arrow" sketch:type="MSLayerGroup" transform="translate(15.000000, 903.000000)">
10+ <g id="layer1" transform="translate(0.857146, 0.494959)" sketch:type="MSShapeGroup">
11+ <g id="g4216" transform="translate(0.952384, 0.831650)">
12+ <path d="M38.0127338,5.94292186 C40.8696258,9.70328846 43.6961579,13.5492607 46.4521869,17.5776877 C50.0756129,22.8738077 53.4767719,28.2917937 56.6611719,33.8315937 C60.6031619,40.9214757 64.3797399,47.5658927 67.7217189,54.6714377 C64.3797399,61.7769828 60.6031619,68.4213998 56.6611719,75.5112818 C53.4767719,81.0510808 50.0756129,86.4690678 46.4521869,91.7651878 C43.6961579,95.7936138 40.8696258,99.6395938 38.0127338,103.399951 L38.0127338,107.669481 C42.0030779,102.508161 45.9748129,97.3405748 49.5537499,92.0679218 C54.5609019,84.6937758 59.0395839,77.5443828 62.9990629,70.6226098 C66.5019159,64.4983168 69.0866949,59.3452977 71.2158589,54.6733907 L69.4678129,54.6713907 L71.2158589,54.6693907 C69.0866949,49.9975777 66.5019159,44.8445587 62.9990629,38.7202657 C59.0395839,31.7984927 54.5609019,24.6490997 49.5537499,17.2749527 C45.9748129,12.0022997 42.0030779,6.83471186 38.0127338,1.67339065 L38.0127338,5.94292186 Z" id="left-swipe-arrow" fill="#6C0D52" transform="translate(54.614296, 54.671436) scale(-1, 1) translate(-54.614296, -54.671436) "></path>
13+ <rect id="rect4782-01" transform="translate(54.190469, 54.673389) rotate(90.000000) translate(-54.190469, -54.673389) " x="0.190470662" y="0.67339065" width="107.999997" height="107.999997"></rect>
14+ </g>
15+ </g>
16+ </g>
17+ </g>
18+ </g>
19+</svg>
qml/Tutorial/graphics/background1.pngnew file

Binary file not shown.

qml/Tutorial/graphics/background2.pngnew file

Binary file not shown.

qml/Tutorial/graphics/camera.pngdeleted

Binary file not shown.

qml/Tutorial/graphics/chevron.pngdeleted

Binary file not shown.

qml/Tutorial/graphics/dialer.pngdeleted

Binary file not shown.

qml/Tutorial/graphics/facebook.pngdeleted

Binary file not shown.

qml/Tutorial/graphics/gallery.pngdeleted

Binary file not shown.

qml/Tutorial/graphics/tick.pngdeleted

Binary file not shown.

qml/Wizard/CheckableSetting.qml+11−3
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2014,2015 Canonical, Ltd.
2+ * Copyright (C) 2014-2016 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
@@ -31,9 +31,11 @@ ListItem.Empty {
3131 signal linkActivated(string link)
3232
3333 implicitHeight: Math.max(label.height, checkBox.height)
34+ highlightWhenPressed: false
3435
35− Item {
36+ FocusScope {
3637 anchors.fill: parent
38+ focus: true
3739
3840 CheckBox {
3941 id: checkBox
@@ -77,8 +79,14 @@ ListItem.Empty {
7779 rightMargin: listItem.rightMargin
7880 }
7981 wrapMode: Text.Wrap
80− linkColor: theme.palette.normal.foregroundText
82+ linkColor: UbuntuColors.orange
83+ color: textColor
84+ font.weight: Font.Light
85+ fontSize: "small"
86+ lineHeight: 1.2
8187 onLinkActivated: listItem.linkActivated(link)
8288 }
89+
90+ Keys.onSpacePressed: checkBox.trigger()
8391 }
8492}
qml/Wizard/Page.qml+261−44
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013,2015 Canonical, Ltd.
2+ * Copyright (C) 2013-2016 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
@@ -16,14 +16,33 @@
1616
1717import QtQuick 2.4
1818import Ubuntu.Components 1.3
19+import Wizard 0.1
1920
2021Item {
21− readonly property real buttonMargin: units.gu(2)
22+ readonly property real maximumContentWidth: units.gu(50)
23+ readonly property bool wideMode: width > units.gu(90) && width > height
24+ readonly property bool contentAnimationRunning: contentAnimation.running
25+
26+ readonly property real buttonMargin: units.gu(3)
2227 readonly property real buttonWidth: (width - buttonMargin * 2) / 2 -
2328 buttonMargin / 2
24− readonly property real topMargin: units.gu(8)
25− readonly property real leftMargin: units.gu(2)
26− readonly property real rightMargin: units.gu(2)
29+ readonly property real buttonBarHeight: units.gu(5)
30+ readonly property real titleRectHeight: customTitle ? units.gu(10) : units.gu(16)
31+
32+ readonly property real topMargin: units.gu(11)
33+ readonly property real bottomMargin: units.gu(3)
34+ readonly property real leftMargin: Math.max(units.gu(3), (width - maximumContentWidth) / 3)
35+ readonly property real rightMargin: leftMargin
36+ readonly property real customMargin: wideMode ? units.gu(8) : units.gu(4) // margin for the content
37+ readonly property real staticMargin: units.gu(3)
38+
39+ // colors
40+ readonly property color backgroundColor: "#fdfdfd"
41+ readonly property color dividerColor: UbuntuColors.silk
42+ readonly property color textColor: UbuntuColors.slate
43+ readonly property color errorColor: theme.palette.normal.negative
44+ readonly property color okColor: theme.palette.normal.positive
45+ readonly property color whiteColor: "white" // workaround for a UITK bug
2746
2847 // If you want to skip a page, mark skipValid false while you figure out
2948 // whether to skip, then set it to true once you've determined the value
@@ -32,9 +51,13 @@ Item {
3251 property bool skip: false
3352
3453 property bool hasBackButton: true
54+ property string backButtonText: i18n.ctr("Button: Go back one page in the Wizard", "Back")
3555 property bool customBack: false
56+ property bool customTitle: false
3657 property alias forwardButtonSourceComponent: forwardButton.sourceComponent
3758 property alias content: contentHolder
59+ property bool lastPage: false
60+ property bool buttonBarVisible: true
3861
3962 property string title: ""
4063
@@ -43,66 +66,260 @@ Item {
4366 visible: false
4467 anchors.fill: parent
4568
46− // We want larger than even fontSize: "x-large", so we use a Text instead
47− // of a Label.
48− Text {
49− id: titleLabel
69+ Timer {
70+ id: indicatorTimer
71+ interval: 1000
72+ triggeredOnStart: true
73+ repeat: true
74+ running: System.wizardEnabled
75+ onTriggered: {
76+ indicatorTime.text = Qt.formatTime(new Date(), "h:mm")
77+ }
78+ }
79+
80+ // page header
81+ Image {
82+ id: titleRect
83+ visible: !lastPage
5084 anchors {
5185 top: parent.top
5286 left: parent.left
5387 right: parent.right
54− topMargin: topMargin
55− leftMargin: leftMargin
56− rightMargin: rightMargin
5788 }
58− wrapMode: Text.Wrap
59− text: title
60− color: theme.palette.normal.baseText
61− font.pixelSize: units.gu(4)
89+ source: customTitle ? "" : (wideMode ? "Pages/data/Desktop_header_bkg.png" : "Pages/data/Phone_header_bkg.png")
90+ height: titleRectHeight
91+ clip: true
92+
93+ // page title
94+ Label {
95+ id: titleLabel
96+ property real animatedTopMargin: 0
97+ anchors {
98+ left: parent.left
99+ right: parent.right
100+ bottom: parent.bottom
101+ bottomMargin: bottomMargin
102+ leftMargin: staticMargin
103+ rightMargin: rightMargin
104+ topMargin: titleLabel.animatedTopMargin
105+ }
106+ text: title
107+ color: customTitle ? textColor : backgroundColor
108+ fontSize: customTitle ? "large" : "x-large"
109+ font.weight: Font.Light
110+ wrapMode: Text.WordWrap
111+ maximumLineCount: 2
112+ elide: Text.ElideRight
113+ }
114+
115+ // indicators
116+ Row {
117+ id: indicatorRow
118+ visible: !customTitle
119+ anchors {
120+ top: parent.top
121+ right: parent.right
122+ topMargin: units.gu(.5)
123+ rightMargin: units.gu(.5)
124+ }
125+ height: units.gu(2)
126+ spacing: units.gu(1)
127+
128+ Icon {
129+ id: indicatorSim
130+ anchors.verticalCenter: parent.verticalCenter
131+ name: "no-simcard"
132+ height: parent.height
133+ width: height
134+ visible: !(root.simManager0.present || root.simManager1.present) && root.modemManager.modems.length > 0
135+ color: "white"
136+ }
137+
138+ Icon {
139+ id: indicatorNet
140+ anchors.verticalCenter: parent.verticalCenter
141+ name: Status.networkIcon
142+ height: parent.height
143+ width: height
144+ color: "white"
145+ }
146+
147+ Icon {
148+ id: indicatorBattery
149+ anchors.verticalCenter: parent.verticalCenter
150+ name: Status.batteryIcon
151+ height: parent.height
152+ width: height * 1.4 // the battery icon is not rectangular :/
153+ color: "white"
154+ }
155+
156+ Label {
157+ id: indicatorTime
158+ anchors.verticalCenter: parent.verticalCenter
159+ color: whiteColor
160+ fontSize: "small"
161+ }
162+ }
62163 }
63164
165+ // content
64166 Item {
65167 id: contentHolder
168+ property real animatedMargin: 0
169+ property real animatedTopMargin: 0
66170 anchors {
67− top: titleLabel.bottom
171+ top: titleRect.bottom
68172 left: parent.left
69173 right: parent.right
70− bottom: backButton.top
71− topMargin: units.gu(4)
72− leftMargin: leftMargin
73− rightMargin: rightMargin
74− bottomMargin: buttonMargin
174+ bottom: buttonBarVisible ? buttonRect.top : parent.bottom
175+ leftMargin: content.animatedMargin
176+ rightMargin: -content.animatedMargin
177+ topMargin: content.animatedTopMargin
75178 }
179+ visible: opacity > 0
76180 }
77181
78− StackButton {
79− id: backButton
80− objectName: "backButton"
81− width: buttonWidth
182+ // button bar
183+ Rectangle {
184+ id: buttonRect
185+ visible: buttonBarVisible
82186 anchors {
83− left: parent.left
84187 bottom: parent.bottom
85− leftMargin: buttonMargin
86− bottomMargin: buttonMargin
188+ bottomMargin: Qt.inputMethod.visible ? Qt.inputMethod.keyboardRectangle.height : 0
189+ left: parent.left
190+ right: parent.right
191+ }
192+ height: buttonBarHeight
193+ color: "#f5f5f5"
194+ z: content.z + 1
195+
196+ StackButton {
197+ id: backButton
198+ objectName: "backButton"
199+ width: buttonWidth
200+ anchors {
201+ left: parent.left
202+ bottom: parent.bottom
203+ leftMargin: staticMargin
204+ verticalCenter: parent.verticalCenter
205+ }
206+ text: backButtonText
207+ visible: pageStack.depth > 1 && hasBackButton
208+ backArrow: true
209+
210+ onClicked: customBack ? backClicked() : pageStack.prev()
211+ }
212+
213+ Loader {
214+ id: forwardButton
215+ objectName: "forwardButton"
216+ width: buttonWidth
217+ anchors {
218+ right: parent.right
219+ bottom: parent.bottom
220+ rightMargin: staticMargin
221+ verticalCenter: parent.verticalCenter
222+ }
87223 }
88− z: 1
89− text: i18n.ctr("Button: Go back one page in the Wizard", "Back")
90− visible: pageStack.depth > 1 && hasBackButton
91− backArrow: true
224+ }
92225
93− onClicked: customBack ? backClicked() : pageStack.prev()
226+ // transitions
227+ function aboutToShow(duration, direction) {
228+ startContentAnimation(duration, direction)
229+ startControlsAnimation(duration)
94230 }
95231
96− Loader {
97− id: forwardButton
98− objectName: "forwardButton"
99− width: buttonWidth
100− anchors {
101− right: parent.right
102− bottom: parent.bottom
103− rightMargin: buttonMargin
104− bottomMargin: buttonMargin
232+ function aboutToShowSecondary(duration) {
233+ secondaryAnimation.restart()
234+ startControlsAnimation(duration)
235+ }
236+
237+ function startContentAnimation(duration, direction) {
238+ contentAnimation.animationDurationBase = duration
239+ contentAnimation.direction = direction
240+ contentAnimation.restart()
241+ }
242+
243+ function startControlsAnimation(showDuration) {
244+ actionsShowAnimation.showDuration = showDuration
245+ actionsShowAnimation.restart()
246+ }
247+
248+ SequentialAnimation { // animation for the button bar
249+ id: actionsShowAnimation
250+ property int showDuration: 0
251+ PropertyAction {
252+ target: buttonRect
253+ property: 'opacity'
254+ value: 0
255+ }
256+ PauseAnimation { duration: Math.max(0, actionsShowAnimation.showDuration - UbuntuAnimation.SnapDuration) }
257+ NumberAnimation {
258+ target: buttonRect
259+ property: 'opacity'
260+ to: 1
261+ duration: UbuntuAnimation.SnapDuration
262+ }
263+ }
264+
265+ SequentialAnimation { // animations for the content
266+ id: contentAnimation
267+ objectName: "contentAnimation"
268+ property int animationDurationBase: UbuntuAnimation.BriskDuration
269+ readonly property int additionalDuration: 200
270+ property int direction: Qt.LeftToRight
271+ ScriptAction { // direction of the effect
272+ script: {
273+ if (contentAnimation.direction === Qt.LeftToRight) {
274+ content.animatedMargin = -content.width;
275+ } else {
276+ content.animatedMargin = content.width;
277+ }
278+ }
279+ }
280+ ParallelAnimation {
281+ NumberAnimation { // the slide-in animation
282+ targets: content
283+ property: 'animatedMargin'
284+ to: 0
285+ duration: contentAnimation.animationDurationBase + contentAnimation.additionalDuration
286+ easing.type: Easing.OutCubic
287+ }
288+ NumberAnimation { // opacity animation
289+ targets: [titleLabel,content]
290+ property: 'opacity'
291+ from: 0
292+ to: 1
293+ duration: contentAnimation.animationDurationBase
294+ }
295+ }
296+ }
297+
298+ ParallelAnimation { // animation for the secondary pages
299+ id: secondaryAnimation
300+ objectName: "secondaryAnimation"
301+ NumberAnimation { // the slide-up animation
302+ target: titleLabel
303+ property: 'animatedTopMargin'
304+ from: content.height
305+ to: customMargin
306+ duration: UbuntuAnimation.BriskDuration
307+ easing: UbuntuAnimation.StandardEasing
308+ }
309+ NumberAnimation {
310+ target: content
311+ property: 'animatedTopMargin'
312+ from: content.height
313+ to: 0
314+ duration: UbuntuAnimation.BriskDuration
315+ easing: UbuntuAnimation.StandardEasing
316+ }
317+ NumberAnimation { // opacity animation
318+ target: content
319+ property: 'opacity'
320+ from: 0
321+ to: 1
322+ duration: UbuntuAnimation.BriskDuration
105323 }
106− z: 1
107324 }
108325}
qml/Wizard/Pages.qml+75−31
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013,2014,2015 Canonical, Ltd.
2+ * Copyright (C) 2013-2016 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
@@ -15,40 +15,77 @@
1515 */
1616
1717import QtQuick 2.4
18+import MeeGo.QOfono 0.2
1819import Ubuntu.Components 1.3
1920import Ubuntu.SystemSettings.SecurityPrivacy 1.0
21+import Ubuntu.SystemSettings.Diagnostics 1.0
2022import Wizard 0.1
2123import "../Components"
2224
23−Item {
25+StyledItem {
2426 id: root
2527 objectName: "wizardPages"
2628 focus: true
2729
28− // The background wallpaper to use
29− property string background
30−
3130 signal quit()
3231
3332 // These should be set by a security page and we apply the settings when
3433 // the user exits the wizard.
35− property int passwordMethod: UbuntuSecurityPrivacyPanel.Passcode
34+ property int passwordMethod: UbuntuSecurityPrivacyPanel.Passphrase
3635 property string password: ""
3736
37+ property bool seenSIMPage: false // we want to see the SIM page at most once
38+
39+ property alias modemManager: modemManager
40+ property alias simManager0: simManager0
41+ property alias simManager1: simManager1
42+
43+ theme: ThemeSettings {
44+ name: "Ubuntu.Components.Themes.Ambiance"
45+ }
46+
3847 UbuntuSecurityPrivacyPanel {
3948 id: securityPrivacy
4049 objectName: "securityPrivacy"
4150 }
4251
52+ UbuntuDiagnostics {
53+ id: diagnostics
54+ objectName: "diagnostics"
55+ }
56+
57+ OfonoManager { // need it here for the language and country detection
58+ id: modemManager
59+ readonly property bool gotSimCard: available && ((simManager0.ready && simManager0.present) || (simManager1.ready && simManager1.present))
60+ property bool ready: false
61+ onModemsChanged: {
62+ ready = true;
63+ }
64+ }
65+
66+ // Ideally we would query the system more cleverly than hardcoding two
67+ // sims. But we don't yet have a more clever way. :(
68+ OfonoSimManager {
69+ id: simManager0
70+ modemPath: modemManager.modems.length >= 1 ? modemManager.modems[0] : ""
71+ }
72+
73+ OfonoSimManager {
74+ id: simManager1
75+ modemPath: modemManager.modems.length >= 2 ? modemManager.modems[1] : ""
76+ }
77+
4378 function quitWizard() {
4479 pageStack.currentPage.enabled = false;
4580
46− var errorMsg = securityPrivacy.setSecurity("", password, passwordMethod)
47− if (errorMsg !== "") {
48− // Ignore (but log) any errors, since we're past where the user set
49− // the method. Worst case, we just leave the user with a swipe
50− // security method and they fix it in the system settings.
51− console.log("Error setting security method:", errorMsg)
81+ if (password != "") {
82+ var errorMsg = securityPrivacy.setSecurity("", password, passwordMethod)
83+ if (errorMsg !== "") {
84+ // Ignore (but log) any errors, since we're past where the user set
85+ // the method. Worst case, we just leave the user with a swipe
86+ // security method and they fix it in the system settings.
87+ console.log("Error setting security method:", errorMsg)
88+ }
5289 }
5390
5491 quit();
@@ -58,19 +95,10 @@ Item {
5895 anchors.fill: parent
5996 }
6097
61− Image {
62− id: image
63− // Use x/y/height/width instead of anchors so that we don't adjust
64− // the image when the OSK appears.
65− x: 0
66− y: 0
67− height: root.height
68− width: root.width
69− sourceSize.height: height
70− sourceSize.width: width
71− source: root.background
72− fillMode: Image.PreserveAspectCrop
73− visible: status === Image.Ready
98+ Rectangle {
99+ id: background
100+ anchors.fill: root
101+ color: "#fdfdfd"
74102 }
75103
76104 PageList {
@@ -86,18 +114,26 @@ Item {
86114 // If we've opened any extra (non-main) pages, pop them before
87115 // continuing so back button returns to the previous main page.
88116 while (pageList.index < pageStack.depth - 1)
89− pop()
90− load(pageList.next())
117+ pop();
118+ load(pageList.next());
91119 }
92120
93121 function prev() {
94− if (pageList.index >= pageStack.depth - 1)
95− pageList.prev() // update pageList.index, but not for extra pages
122+ var isPrimaryPage = currentPage && !currentPage.customTitle;
123+ if (pageList.index >= pageStack.depth - 1) {
124+ pageList.prev(); // update pageList.index, but not for extra pages
125+ }
96126 pop()
97127 if (!currentPage || currentPage.opacity === 0) { // undo skipped pages
98− prev()
128+ prev();
99129 } else {
100− currentPage.enabled = true
130+ currentPage.enabled = true;
131+ }
132+
133+ if (isPrimaryPage) {
134+ currentPage.aboutToShow(UbuntuAnimation.BriskDuration, Qt.LeftToRight);
135+ } else {
136+ currentPage.aboutToShowSecondary(UbuntuAnimation.BriskDuration);
101137 }
102138 }
103139
@@ -115,6 +151,13 @@ Item {
115151 // Check for immediate skip or not. We may have to wait for
116152 // skipValid to be assigned (see Connections object below)
117153 checkSkip()
154+
155+ var isPrimaryPage = !currentPage.customTitle;
156+ if (isPrimaryPage) {
157+ currentPage.aboutToShow(UbuntuAnimation.BriskDuration, Qt.RightToLeft);
158+ } else {
159+ currentPage.aboutToShowSecondary(UbuntuAnimation.BriskDuration);
160+ }
118161 }
119162
120163 function checkSkip() {
@@ -136,6 +179,7 @@ Item {
136179 objectName: "timeout"
137180 interval: 2000 // wizard pages shouldn't take long
138181 onTriggered: {
182+ console.warn("Wizard page " + pageStack.currentPage.objectName + " skipped due to taking too long!!!");
139183 pageStack.currentPage.skip = true;
140184 pageStack.currentPage.skipValid = true;
141185 }
qml/Wizard/Pages/10-welcome.qml+128−48
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013 Canonical, Ltd.
2+ * Copyright (C) 2013-2016 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
@@ -24,60 +24,128 @@ import ".." as LocalComponents
2424LocalComponents.Page {
2525 objectName: "languagePage"
2626
27− title: i18n.tr("Hi!")
27+ title: i18n.tr("Language")
2828 forwardButtonSourceComponent: forwardButton
2929
3030 UbuntuLanguagePlugin {
3131 id: plugin
3232 }
3333
34− Column {
35− id: column
36− anchors.fill: content
37− spacing: units.gu(1)
38−
39− Label {
40− id: label1
41− anchors.left: parent.left
42− anchors.right: parent.right
43− wrapMode: Text.Wrap
44− text: i18n.tr("Welcome to your Ubuntu device.")
34+ OnScreenKeyboardPlugin {
35+ id: oskPlugin
36+ }
37+
38+ function init()
39+ {
40+ var detectedLang = "";
41+ // try to detect the language+country from the SIM card
42+ if (root.simManager0.present && root.simManager0.preferredLanguages.length > 0) {
43+ detectedLang = root.simManager0.preferredLanguages[0] + "_" + LocalePlugin.mccToCountryCode(root.simManager0.mobileCountryCode);
44+ } else if (root.simManager1.present && root.simManager1.preferredLanguages.length > 0) {
45+ detectedLang = root.simManager1.preferredLanguages[0] + "_" + LocalePlugin.mccToCountryCode(root.simManager1.mobileCountryCode);
46+ } else if (plugin.currentLanguage != -1) {
47+ detectedLang = plugin.languageCodes[plugin.currentLanguage].split(".")[0]; // remove the encoding part, after dot (en_US.utf8 -> en_US)
48+ } else {
49+ detectedLang = "en_US"; // fallback to default lang
4550 }
4651
47− Label {
48− id: label2
49− anchors.left: parent.left
50− anchors.right: parent.right
51− wrapMode: Text.Wrap
52− text: i18n.tr("Let’s get started.")
52+ // preselect the detected language
53+ for (var i = 0; i < plugin.languageCodes.length; i++) {
54+ var code = plugin.languageCodes[i].split(".")[0]; // remove the encoding part, after dot (en_US.utf8 -> en_US)
55+ if (detectedLang === code) {
56+ languagesListView.currentIndex = i;
57+ languagesListView.positionViewAtIndex(i, ListView.Center);
58+ i18n.language = plugin.languageCodes[i];
59+ break;
60+ }
5361 }
62+ }
5463
55− Item { // spacer
56− height: units.gu(2)
57− width: units.gu(1) // needed else it will be ignored
64+ // splash screen (this has to be on the first page)
65+ Image {
66+ id: splashImage
67+ anchors.top: parent.top
68+ anchors.left: parent.left
69+ anchors.right: parent.right
70+ height: parent.height
71+ source: wideMode ? "data/Desktop_splash_screen_bkg.png" : "data/Phone_splash_screen_bkg.png"
72+ fillMode: Image.PreserveAspectCrop
73+ z: 2
74+ visible: opacity > 0
75+ Component.onCompleted: splashAnimation.start()
76+ }
77+
78+ SequentialAnimation {
79+ id: splashAnimation
80+ PauseAnimation { duration: UbuntuAnimation.BriskDuration }
81+ SmoothedAnimation {
82+ target: splashImage
83+ property: "height"
84+ to: units.gu(16)
85+ duration: UbuntuAnimation.BriskDuration
86+ }
87+ NumberAnimation {
88+ target: splashImage
89+ property: 'opacity'
90+ from: 1
91+ to: 0
5892 }
93+ onStopped: init();
94+ }
95+
96+ ListView {
97+ id: languagesListView
98+ clip: true
99+ snapMode: ListView.SnapToItem
100+
101+ anchors {
102+ fill: content
103+ leftMargin: wideMode ? parent.leftMargin : 0
104+ rightMargin: wideMode ? parent.rightMargin : 0
105+ topMargin: wideMode ? parent.customMargin : 0
106+ }
107+
108+ model: plugin.languageNames
109+
110+ delegate: ListItem {
111+ id: itemDelegate
112+ objectName: "languageDelegate" + index
113+ highlightColor: backgroundColor
114+ divider.colorFrom: dividerColor
115+ divider.colorTo: backgroundColor
116+ readonly property bool isCurrent: index === ListView.view.currentIndex
59117
60− ComboButton {
61− id: combo
62− objectName: "languageCombo"
63− anchors.left: parent.left
64− anchors.right: parent.right
65− text: listview.currentItem.text
66− onClicked: expanded = !expanded
67− expandedHeight: column.height - combo.y
68− UbuntuListView {
69− id: listview
70− model: plugin.languageNames
71− currentIndex: plugin.currentLanguage
72− delegate: Standard {
73− objectName: "languageDelegate" + index
74− text: modelData
75− onClicked: {
76− listview.currentIndex = index
77− combo.expanded = false
78− i18n.language = plugin.languageCodes[index]
79− }
118+ Label {
119+ id: langLabel
120+ text: modelData
121+
122+ anchors {
123+ left: parent.left
124+ verticalCenter: parent.verticalCenter
125+ leftMargin: languagesListView.anchors.leftMargin == 0 ? staticMargin : 0
126+ }
127+
128+ fontSize: "medium"
129+ font.weight: itemDelegate.isCurrent ? Font.Normal : Font.Light
130+ color: textColor
131+ }
132+
133+ Image {
134+ anchors {
135+ right: parent.right;
136+ verticalCenter: parent.verticalCenter;
137+ rightMargin: languagesListView.anchors.rightMargin == 0 ? staticMargin : 0
80138 }
139+ fillMode: Image.PreserveAspectFit
140+ height: units.gu(1.5)
141+
142+ source: "data/Tick@30.png"
143+ visible: itemDelegate.isCurrent
144+ }
145+
146+ onClicked: {
147+ languagesListView.currentIndex = index;
148+ i18n.language = plugin.languageCodes[index];
81149 }
82150 }
83151 }
@@ -85,14 +153,26 @@ LocalComponents.Page {
85153 Component {
86154 id: forwardButton
87155 LocalComponents.StackButton {
88− text: i18n.tr("Continue")
156+ text: i18n.tr("Next")
157+ enabled: languagesListView.currentIndex != -1
89158 onClicked: {
90− if (plugin.currentLanguage !== listview.currentIndex) {
91− plugin.currentLanguage = listview.currentIndex;
92− System.updateSessionLanguage(plugin.languageCodes[listview.currentIndex]);
93− i18n.language = i18n.language; // re-notify of change after above call (for qlocale change)
159+ if (plugin.currentLanguage !== languagesListView.currentIndex) {
160+ var locale = plugin.languageCodes[languagesListView.currentIndex];
161+ var language = locale.split("_")[0].split(".")[0];
162+ plugin.currentLanguage = languagesListView.currentIndex;
163+ oskPlugin.setCurrentLayout(language);
164+ System.updateSessionLocale(locale);
165+ }
166+ i18n.language = plugin.languageCodes[plugin.currentLanguage]; // re-notify of change after above call (for qlocale change)
167+
168+ if (!root.modemManager.available || !root.modemManager.ready || root.modemManager.modems.length === 0 ||
169+ (root.simManager0.present && root.simManager0.ready) || (root.simManager1.present && root.simManager1.ready) ||
170+ root.seenSIMPage) { // go to next page
171+ pageStack.next();
172+ }
173+ else {
174+ pageStack.load(Qt.resolvedUrl("sim.qml")); // show the SIM page
94175 }
95− pageStack.next()
96176 }
97177 }
98178 }
qml/Wizard/Pages/20-sim.qmldeleted−77
@@ -1,77 +0,0 @@
1−/*
2− * Copyright (C) 2013 Canonical, Ltd.
3− *
4− * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU General Public License as published by
6− * the Free Software Foundation; version 3.
7− *
8− * This program is distributed in the hope that it will be useful,
9− * but WITHOUT ANY WARRANTY; without even the implied warranty of
10− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU General Public License for more details.
12− *
13− * You should have received a copy of the GNU General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− */
16−
17−import QtQuick 2.4
18−import MeeGo.QOfono 0.2
19−import Ubuntu.Components 1.3
20−import ".." as LocalComponents
21−
22−LocalComponents.Page {
23− objectName: "simPage"
24−
25− title: i18n.tr("Add a SIM card and restart your device")
26− forwardButtonSourceComponent: forwardButton
27−
28− skipValid: !manager.available ||
29− (manager.ready && (manager.modems.length < 1 || simManager0.ready)
30− && (manager.modems.length < 2 || simManager1.ready))
31− skip: !manager.available || manager.modems.length === 0 || simManager0.present || simManager1.present
32−
33− OfonoManager {
34− id: manager
35− property bool ready: false
36− onModemsChanged: ready = true
37− }
38−
39− // Ideally we would query the system more cleverly than hardcoding two
40− // modems. But we don't yet have a more clever way. :(
41− OfonoSimManager {
42− id: simManager0
43− modemPath: manager.modems.length >= 1 ? manager.modems[0] : ""
44− }
45−
46− OfonoSimManager {
47− id: simManager1
48− modemPath: manager.modems.length >= 2 ? manager.modems[1] : ""
49− }
50−
51− Column {
52− anchors.fill: content
53− spacing: units.gu(4)
54−
55− Label {
56− anchors.left: parent.left
57− anchors.right: parent.right
58− wrapMode: Text.Wrap
59− text: i18n.tr("Without it, you won’t be able to make calls or use text messaging.")
60− }
61−
62− Image {
63− id: image
64− source: "data/meet_ubuntu_simcard@30.png"
65− height: units.gu(6.5)
66− width: units.gu(9)
67− }
68− }
69−
70− Component {
71− id: forwardButton
72− LocalComponents.StackButton {
73− text: i18n.tr("Skip")
74− onClicked: pageStack.next()
75− }
76− }
77−}
qml/Wizard/Pages/30-passwd-type.qmldeleted−140
@@ -1,140 +0,0 @@
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−import QtQuick 2.4
18−import Ubuntu.Components 1.3
19−import Ubuntu.Components.ListItems 1.3
20−import Ubuntu.SystemSettings.SecurityPrivacy 1.0
21−import ".." as LocalComponents
22−
23−/**
24− * One quirk with this page: we don't actually set the password. We avoid
25− * doing it here because the user can come back to this page and change their
26− * answer. We don't run as root, so if we did set the password immediately,
27− * we'd need to prompt for their previous password when they came back and
28− * changed their answer. Which is silly UX. So instead, we just keep track
29− * of their choice and set the password at the end (see main.qml).
30− * Setting the password shouldn't fail, since Ubuntu Touch has loose password
31− * requirements, but we'll check what we can here. Ideally we'd be able to ask
32− * the system if a password is legal without actually setting that password.
33− */
34−
35−LocalComponents.Page {
36− id: passwdPage
37− objectName: "passwdPage"
38−
39− title: i18n.tr("Lock security")
40− forwardButtonSourceComponent: forwardButton
41−
42− // If the user has set a password some other way (via ubuntu-device-flash
43− // or this isn't the first time the wizard has been run, etc). We can't
44− // properly set the password again, so let's not pretend we can.
45− skip: securityPrivacy.securityType !== UbuntuSecurityPrivacyPanel.Swipe
46−
47− function indexToMethod(index) {
48− if (index === 0)
49− return UbuntuSecurityPrivacyPanel.Swipe
50− else if (index === 1)
51− return UbuntuSecurityPrivacyPanel.Passcode
52− else
53− return UbuntuSecurityPrivacyPanel.Passphrase
54− }
55−
56− function methodToIndex(method) {
57− if (method === UbuntuSecurityPrivacyPanel.Swipe)
58− return 0
59− else if (method === UbuntuSecurityPrivacyPanel.Passcode)
60− return 1
61− else
62− return 2
63− }
64−
65− Connections {
66− target: root
67− onPasswordMethodChanged: selector.selectedIndex = methodToIndex(root.passwordMethod)
68− }
69−
70− Column {
71− id: column
72− anchors.fill: content
73− spacing: units.gu(4)
74−
75− Label {
76− anchors.left: parent.left
77− anchors.right: parent.right
78− wrapMode: Text.Wrap
79− text: i18n.tr("Please select how you’d like to unlock your device.")
80− }
81−
82− ItemSelector {
83− id: selector
84− expanded: true
85− anchors.left: parent.left
86− anchors.right: parent.right
87− property color originalBackground
88−
89− model: ["", "", ""] // otherwise the delegate will show the text itself and we only want subText
90−
91− selectedIndex: methodToIndex(root.passwordMethod)
92−
93− delegate: OptionSelectorDelegate {
94− objectName: "passwdDelegate" + index
95−
96− // use subText because we want the text to be small, and we have no other way to control it
97− subText: {
98− var method = indexToMethod(index)
99− var name = ""
100− var desc = ""
101− if (method === UbuntuSecurityPrivacyPanel.Swipe) {
102− name = i18n.ctr("Label: Type of security method", "Swipe")
103− desc = i18n.ctr("Label: Description of security method", "No security")
104− } else if (method === UbuntuSecurityPrivacyPanel.Passcode) {
105− name = i18n.ctr("Label: Type of security method", "Passcode")
106− desc = i18n.ctr("Label: Description of security method", "4 digits only")
107− } else {
108− name = i18n.ctr("Label: Type of security method", "Passphrase")
109− desc = i18n.ctr("Label: Description of security method", "Numbers and letters")
110− }
111− return "<b>%1</b> (%2)".arg(name).arg(desc)
112− }
113− }
114−
115− style: Item {}
116−
117− Component.onCompleted: {
118− // A visible selected background looks bad in ListItem widgets with our theme
119− originalBackground = theme.palette.selected.background
120− theme.palette.selected.background = "transparent"
121− }
122−
123− Component.onDestruction: {
124− // Restore original theme background
125− theme.palette.selected.background = originalBackground
126− }
127− }
128− }
129−
130− Component {
131− id: forwardButton
132− LocalComponents.StackButton {
133− text: i18n.tr("Continue")
134− onClicked: {
135− root.passwordMethod = indexToMethod(selector.selectedIndex)
136− pageStack.load(Qt.resolvedUrl("passwd-set.qml"))
137− }
138− }
139− }
140−}
qml/Wizard/Pages/30-wifi.qmlnew file+210
@@ -0,0 +1,210 @@
1+/*
2+ * Copyright (C) 2013-2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import QtQuick.Layouts 1.1
19+import QMenuModel 0.1 as QMenuModel
20+import Ubuntu.Components 1.3
21+import Wizard 0.1
22+import Ubuntu.Connectivity 1.0
23+import ".." as LocalComponents
24+
25+LocalComponents.Page {
26+ id: wifiPage
27+ objectName: "wifiPage"
28+
29+ title: i18n.tr("Connect to Wi‑Fi")
30+ forwardButtonSourceComponent: forwardButton
31+
32+ readonly property bool connected: Connectivity.online
33+
34+ function getExtendedProperty(object, propertyName, defaultValue) {
35+ if (object && object.hasOwnProperty(propertyName)) {
36+ return object[propertyName];
37+ }
38+ return defaultValue;
39+ }
40+
41+ function getAPIcon(adHoc, signalStrength, secure) {
42+ var imageName = "nm-no-connection";
43+
44+ if (adHoc) {
45+ imageName = "nm-adhoc";
46+ } else if (signalStrength == 0) {
47+ imageName = "nm-signal-00";
48+ } else if (signalStrength <= 25) {
49+ imageName = "nm-signal-25";
50+ } else if (signalStrength <= 50) {
51+ imageName = "nm-signal-50";
52+ } else if (signalStrength <= 75) {
53+ imageName = "nm-signal-75";
54+ } else if (signalStrength <= 100) {
55+ imageName = "nm-signal-100";
56+ }
57+
58+ if (secure) {
59+ imageName += "-secure";
60+ }
61+ return imageName;
62+ }
63+
64+ QMenuModel.UnityMenuModel {
65+ id: menuModel
66+ busName: "com.canonical.indicator.network"
67+ actions: { "indicator": "/com/canonical/indicator/network" }
68+ menuObjectPath: "/com/canonical/indicator/network/phone_wifi_settings"
69+ }
70+
71+ Component {
72+ id: accessPointComponent
73+ ListItem {
74+ id: accessPoint
75+ objectName: "accessPoint"
76+ highlightColor: backgroundColor
77+ enabled: menuData && menuData.sensitive || false
78+ divider.colorFrom: dividerColor
79+ divider.colorTo: backgroundColor
80+
81+ property QtObject menuData: null
82+ property var unityMenuModel: menuModel
83+ property var extendedData: menuData && menuData.ext || undefined
84+ property var strengthAction: QMenuModel.UnityMenuAction {
85+ model: unityMenuModel
86+ index: menuIndex
87+ name: getExtendedProperty(extendedData, "xCanonicalWifiApStrengthAction", "")
88+ }
89+ readonly property bool secure: getExtendedProperty(extendedData, "xCanonicalWifiApIsSecure", false)
90+ readonly property bool adHoc: getExtendedProperty(extendedData, "xCanonicalWifiApIsAdhoc", false)
91+ readonly property bool isConnected: menuData && menuData.actionState
92+ readonly property bool isEnterprise: getExtendedProperty(extendedData, "xCanonicalWifiApIsEnterprise", false)
93+ readonly property int signalStrength: strengthAction.valid ? strengthAction.state : 0
94+ property int menuIndex: -1
95+
96+ function loadAttributes() {
97+ if (!unityMenuModel || menuIndex == -1) return;
98+ unityMenuModel.loadExtendedAttributes(menuIndex, {'x-canonical-wifi-ap-is-adhoc': 'bool',
99+ 'x-canonical-wifi-ap-is-secure': 'bool',
100+ 'x-canonical-wifi-ap-is-enterprise': 'bool',
101+ 'x-canonical-wifi-ap-strength-action': 'string'});
102+ }
103+
104+ Icon {
105+ id: apIcon
106+ anchors {
107+ left: parent.left
108+ verticalCenter: parent.verticalCenter
109+ leftMargin: column.anchors.leftMargin == 0 ? staticMargin : 0
110+ }
111+ height: units.gu(2.5)
112+ width: height
113+ name: getAPIcon(accessPoint.adHoc, accessPoint.signalStrength, accessPoint.secure)
114+ color: textColor
115+ }
116+
117+ Column {
118+ anchors.verticalCenter: parent.verticalCenter
119+ anchors.left: apIcon.right
120+ anchors.leftMargin: units.gu(2)
121+ Label {
122+ id: apName
123+ text: menuData && menuData.label || ""
124+ font.weight: accessPoint.isConnected ? Font.Normal : Font.Light
125+ fontSize: "medium"
126+ color: textColor
127+ }
128+ Label {
129+ id: connectedLabel
130+ text: i18n.tr("Connected")
131+ font.weight: Font.Light
132+ fontSize: "small"
133+ color: okColor
134+ visible: accessPoint.isConnected
135+ }
136+ }
137+
138+ onClicked: {
139+ unityMenuModel.activate(menuIndex);
140+ listview.positionViewAtBeginning();
141+ }
142+ }
143+ }
144+
145+ ColumnLayout {
146+ id: column
147+ spacing: units.gu(2)
148+ anchors {
149+ fill: content
150+ topMargin: customMargin
151+ leftMargin: wideMode ? parent.leftMargin : 0
152+ rightMargin: wideMode ? parent.rightMargin : 0
153+ }
154+
155+ Label {
156+ id: label
157+ anchors.left: parent.left
158+ anchors.right: parent.right
159+ anchors.leftMargin: column.anchors.leftMargin == 0 ? staticMargin : 0
160+ font.weight: Font.Light
161+ color: "#68064d"
162+ wrapMode: Text.Wrap
163+ text: listview.count > 0 ? i18n.tr("Available Wi-Fi networks")
164+ : i18n.tr("No available Wi-Fi networks")
165+ }
166+
167+ ListView {
168+ id: listview
169+ anchors.left: parent.left
170+ anchors.right: parent.right
171+ clip: true
172+ model: menuModel
173+ Layout.fillHeight: true
174+
175+ delegate: Loader {
176+ id: loader
177+
178+ readonly property bool isAccessPoint: model.type === "unity.widgets.systemsettings.tablet.accesspoint"
179+ readonly property bool isConnected: item && item.menuData && item.menuData.actionState
180+ readonly property bool isEnterprise: item && item.isEnterprise
181+
182+ height: !!sourceComponent ? (isConnected ? units.gu(9) : units.gu(7)) : 0
183+ anchors.left: parent.left
184+ anchors.right: parent.right
185+
186+ asynchronous: true
187+ sourceComponent: {
188+ if (isAccessPoint && !isEnterprise) {
189+ return accessPointComponent;
190+ }
191+ return null;
192+ }
193+
194+ onLoaded: {
195+ item.menuData = Qt.binding(function() { return model; });
196+ item.menuIndex = Qt.binding(function() { return index; });
197+ item.loadAttributes();
198+ }
199+ }
200+ }
201+ }
202+
203+ Component {
204+ id: forwardButton
205+ LocalComponents.StackButton {
206+ text: (connected || listview.count === 0) ? i18n.tr("Next") : i18n.tr("Skip")
207+ onClicked: pageStack.next()
208+ }
209+ }
210+}
qml/Wizard/Pages/40-location.qmlnew file+290
@@ -0,0 +1,290 @@
1+/*
2+ * Copyright (C) 2013-2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import AccountsService 0.1
19+import QMenuModel 0.1 as QMenuModel
20+import Qt.labs.folderlistmodel 2.1
21+import Ubuntu.Components 1.3
22+import ".." as LocalComponents
23+
24+LocalComponents.Page {
25+ objectName: "locationPage"
26+
27+ title: i18n.tr("Location Services")
28+ forwardButtonSourceComponent: forwardButton
29+
30+ readonly property bool pathSet: AccountsService.hereLicensePathValid
31+ skipValid: pathSet && (AccountsService.hereLicensePath === "" || termsModel.count > 0)
32+ skip: skipValid && (AccountsService.hereLicensePath === "" || termsModel.count === 2) // no files but . and ..
33+
34+ FolderListModel {
35+ id: termsModel
36+ folder: AccountsService.hereLicensePath
37+ nameFilters: ["*.html"]
38+ showOnlyReadable: true
39+ showDotAndDotDot: true // so that count == 0 means we're not done scanning yet
40+ }
41+
42+ QMenuModel.QDBusActionGroup {
43+ id: locationActionGroup
44+ objectName: "locationActionGroup"
45+ busType: QMenuModel.DBus.SessionBus
46+ busName: "com.canonical.indicator.location"
47+ objectPath: "/com/canonical/indicator/location"
48+ property variant location: action("location-detection-enabled")
49+ property variant gps: action("gps-detection-enabled")
50+ Component.onCompleted: start()
51+ }
52+
53+ Item {
54+ id: column
55+ anchors.fill: content
56+ anchors.topMargin: customMargin
57+ anchors.leftMargin: wideMode ? parent.leftMargin : 0
58+ anchors.rightMargin: wideMode ? parent.rightMargin : 0
59+
60+ Item {
61+ id: hereCheckGroup
62+ anchors {
63+ left: parent.left
64+ right: parent.right
65+ top: parent.top
66+ }
67+ height: hereCheckLabel.height + hereTermsLabel.height
68+
69+ Label {
70+ id: hereCheckLabel
71+ objectName: "hereCheckLabel"
72+ anchors {
73+ left: parent.left
74+ right: hereTermsCheck.left
75+ top: parent.top
76+ leftMargin: column.anchors.leftMargin == 0 ? staticMargin : 0
77+ rightMargin: units.gu(2)
78+ }
79+ lineHeight: 1.2
80+ wrapMode: Text.WordWrap
81+ color: textColor
82+ font.weight: checked ? Font.Normal : Font.Light
83+ text: i18n.tr("Use GPS, Wi-Fi hotspots and mobile network anonymously to detect location (recommended)")
84+ property bool checked: true
85+ }
86+
87+ Label {
88+ id: hereTermsLabel
89+ objectName: "hereTermsLabel"
90+ anchors {
91+ left: parent.left
92+ right: hereTermsCheck.left
93+ top: hereCheckLabel.bottom
94+ topMargin: units.gu(1)
95+ leftMargin: column.anchors.leftMargin == 0 ? staticMargin : 0
96+ rightMargin: units.gu(2)
97+ }
98+
99+ wrapMode: Text.WordWrap
100+ color: textColor
101+ font.weight: Font.Light
102+ fontSize: "small"
103+ lineHeight: 1.2
104+ linkColor: UbuntuColors.orange
105+ text: i18n.tr("By selecting this option you agree to the Nokia HERE %1.")
106+ .arg("<a href='#'>" + i18n.ctr("part of: Nokia HERE terms and conditions", "terms and conditions") + "</a>")
107+ Mouse.forwardTo: hereCheckArea // clicking on the link also selects this group
108+ Mouse.priority: Mouse.AfterItem
109+ onLinkActivated: {
110+ pageStack.load(Qt.resolvedUrl("here-terms.qml"));
111+ }
112+ }
113+
114+ Image {
115+ id: hereTermsCheck
116+ fillMode: Image.PreserveAspectFit
117+ height: units.gu(1.5)
118+ source: "data/Tick@30.png"
119+ opacity: hereCheckLabel.checked ? 1 : 0
120+ anchors.right: parent.right
121+ anchors.verticalCenter: hereCheckLabel.verticalCenter
122+ anchors.rightMargin: column.anchors.rightMargin == 0 ? staticMargin : 0
123+ }
124+
125+ MouseArea {
126+ id: hereCheckArea
127+ anchors.fill: parent
128+ z: hereCheckGroup.z - 1 // so that the link can be activated
129+ onClicked: {
130+ if (!hereCheckLabel.checked) {
131+ hereCheckLabel.checked = true;
132+ gpsCheckLabel.checked = false;
133+ nopeCheckLabel.checked = false;
134+ }
135+ }
136+ }
137+ }
138+
139+ Rectangle {
140+ id: divider
141+ anchors.left: parent.left
142+ anchors.right: parent.right
143+ anchors.top: hereCheckGroup.bottom
144+ anchors.topMargin: units.gu(3)
145+ height: units.dp(1)
146+ color: dividerColor
147+ }
148+
149+ Label {
150+ id: gpsCheckLabel
151+ objectName: "gpsCheckLabel"
152+ anchors {
153+ left: parent.left
154+ right: parent.right
155+ top: divider.bottom
156+ topMargin: units.gu(3)
157+ leftMargin: column.anchors.leftMargin == 0 ? staticMargin : 0
158+ rightMargin: units.gu(2)
159+ }
160+ text: i18n.tr("GPS only")
161+ wrapMode: Text.WordWrap
162+ color: textColor
163+ font.weight: checked ? Font.Normal : Font.Light
164+ width: content.width
165+ property bool checked: false
166+
167+ MouseArea {
168+ anchors.fill: parent
169+ onClicked: {
170+ if (!gpsCheckLabel.checked) {
171+ hereCheckLabel.checked = false;
172+ gpsCheckLabel.checked = true;
173+ nopeCheckLabel.checked = false;
174+ }
175+ }
176+ }
177+ }
178+
179+ Image {
180+ fillMode: Image.PreserveAspectFit
181+ height: units.gu(1.5)
182+ source: "data/Tick@30.png"
183+ opacity: gpsCheckLabel.checked ? 1 : 0
184+ anchors.right: parent.right
185+ anchors.verticalCenter: gpsCheckLabel.verticalCenter
186+ anchors.rightMargin: column.anchors.rightMargin == 0 ? staticMargin : 0
187+ }
188+
189+ Rectangle {
190+ id: divider2
191+ anchors.left: parent.left
192+ anchors.right: parent.right
193+ anchors.top: gpsCheckLabel.bottom
194+ anchors.topMargin: units.gu(3)
195+ height: units.dp(1)
196+ color: dividerColor
197+ }
198+
199+ Label {
200+ id: nopeCheckLabel
201+ objectName: "nopeCheckLabel"
202+ anchors {
203+ left: parent.left
204+ right: parent.right
205+ top: divider2.bottom
206+ topMargin: units.gu(3)
207+ leftMargin: column.anchors.leftMargin == 0 ? staticMargin : 0
208+ rightMargin: units.gu(2)
209+ }
210+ wrapMode: Text.WordWrap
211+ color: textColor
212+ font.weight: checked ? Font.Normal : Font.Light
213+ width: content.width
214+ text: i18n.tr("Don't use my location")
215+ property bool checked: false
216+
217+ MouseArea {
218+ anchors.fill: parent
219+ onClicked: {
220+ if (!nopeCheckLabel.checked) {
221+ hereCheckLabel.checked = false;
222+ gpsCheckLabel.checked = false;
223+ nopeCheckLabel.checked = true;
224+ }
225+ }
226+ }
227+ }
228+
229+ Image {
230+ fillMode: Image.PreserveAspectFit
231+ height: units.gu(1.5)
232+ source: "data/Tick@30.png"
233+ opacity: nopeCheckLabel.checked ? 1 : 0
234+ anchors.right: parent.right
235+ anchors.verticalCenter: nopeCheckLabel.verticalCenter
236+ anchors.rightMargin: column.anchors.rightMargin == 0 ? staticMargin : 0
237+ }
238+
239+ Rectangle {
240+ id: divider3
241+ anchors.left: parent.left
242+ anchors.right: parent.right
243+ anchors.top: nopeCheckLabel.bottom
244+ anchors.topMargin: units.gu(3)
245+ height: units.dp(1)
246+ color: dividerColor
247+ }
248+
249+ Label {
250+ id: infoLabel
251+ anchors {
252+ left: parent.left
253+ right: parent.right
254+ top: divider3.bottom
255+ topMargin: units.gu(4)
256+ leftMargin: column.anchors.leftMargin == 0 ? staticMargin : 0
257+ rightMargin: column.anchors.rightMargin == 0 ? staticMargin : 0
258+ }
259+ wrapMode: Text.Wrap
260+ text: i18n.tr("You can change it later in System Settings.")
261+ color: textColor
262+ fontSize: "small"
263+ font.weight: Font.Light
264+ width: content.width
265+ }
266+ }
267+
268+ Component {
269+ id: forwardButton
270+ LocalComponents.StackButton {
271+ text: i18n.tr("Next")
272+ onClicked: {
273+ var locationOn = gpsCheckLabel.checked || hereCheckLabel.checked;
274+ var gpsOn = gpsCheckLabel.checked || hereCheckLabel.checked;
275+ var hereOn = hereCheckLabel.checked;
276+
277+ // location service doesn't currently listen to updateState
278+ // requests, so we activate the actions if needed.
279+ if (locationActionGroup.location.state != locationOn) {
280+ locationActionGroup.location.activate();
281+ }
282+ if (locationActionGroup.gps.state != gpsOn) {
283+ locationActionGroup.gps.activate();
284+ }
285+ AccountsService.hereEnabled = hereOn;
286+ pageStack.next();
287+ }
288+ }
289+ }
290+}
qml/Wizard/Pages/40-wifi.qmldeleted−219
@@ -1,219 +0,0 @@
1−/*
2− * Copyright (C) 2013 Canonical, Ltd.
3− *
4− * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU General Public License as published by
6− * the Free Software Foundation; version 3.
7− *
8− * This program is distributed in the hope that it will be useful,
9− * but WITHOUT ANY WARRANTY; without even the implied warranty of
10− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU General Public License for more details.
12− *
13− * You should have received a copy of the GNU General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− */
16−
17−import QtQuick 2.4
18−import QMenuModel 0.1 as QMenuModel
19−import QtSystemInfo 5.0
20−import Ubuntu.Components 1.3
21−import Ubuntu.Components.ListItems 1.3 as ListItem
22−import Ubuntu.Settings.Menus 0.1 as Menus
23−import ".." as LocalComponents
24−
25−LocalComponents.Page {
26− id: wifiPage
27− objectName: "wifiPage"
28−
29− title: i18n.tr("Connect to Wi‑Fi")
30− forwardButtonSourceComponent: forwardButton
31−
32− readonly property bool connected: networkInfo.accessPointName
33−
34− function getExtendedProperty(object, propertyName, defaultValue) {
35− if (object && object.hasOwnProperty(propertyName)) {
36− return object[propertyName];
37− }
38− return defaultValue;
39− }
40−
41− QMenuModel.UnityMenuModel {
42− id: menuModel
43− busName: "com.canonical.indicator.network"
44− actions: { "indicator": "/com/canonical/indicator/network" }
45− menuObjectPath: "/com/canonical/indicator/network/phone_wifi_settings"
46− }
47−
48− NetworkInfo {
49− id: networkInfo
50−
51− property string accessPointName
52−
53− monitorCurrentNetworkMode: true
54− monitorNetworkName: true
55− monitorNetworkStatus: true
56−
57− onCurrentNetworkModeChanged: getAccessPointName()
58− onNetworkNameChanged: getAccessPointName()
59− onNetworkStatusChanged: if (status !== NetworkInfo.HomeNetwork) accessPointName = ""
60−
61− Component.onCompleted: getAccessPointName()
62−
63− function getAccessPointName() {
64− // 0 is the interface
65− if (currentNetworkMode === NetworkInfo.WlanMode &&
66− networkStatus(NetworkInfo.WlanMode, 0) === NetworkInfo.HomeNetwork)
67− accessPointName = networkName(NetworkInfo.WlanMode, 0);
68− else
69− accessPointName = "";
70− }
71− }
72−
73− Component {
74− id: accessPointComponent
75− ListItem.Standard {
76− id: accessPoint
77− objectName: "accessPoint"
78−
79− property QtObject menuData: null
80− property var unityMenuModel: menuModel
81− property var extendedData: menuData && menuData.ext || undefined
82− property var strengthAction: QMenuModel.UnityMenuAction {
83− model: unityMenuModel
84− index: menuIndex
85− name: getExtendedProperty(extendedData, "xCanonicalWifiApStrengthAction", "")
86− }
87− property bool checked: menuData && menuData.isToggled || false
88− property bool secure: getExtendedProperty(extendedData, "xCanonicalWifiApIsSecure", false)
89− property bool adHoc: getExtendedProperty(extendedData, "xCanonicalWifiApIsAdhoc", false)
90− property int signalStrength: strengthAction.valid ? strengthAction.state : 0
91− property int menuIndex: -1
92−
93− function loadAttributes() {
94− if (!unityMenuModel || menuIndex == -1) return;
95− unityMenuModel.loadExtendedAttributes(menuIndex, {'x-canonical-wifi-ap-is-adhoc': 'bool',
96− 'x-canonical-wifi-ap-is-secure': 'bool',
97− 'x-canonical-wifi-ap-strength-action': 'string'});
98− }
99−
100− signal activate()
101−
102− text: menuData && menuData.label || ""
103− enabled: menuData && menuData.sensitive || false
104− iconName: {
105− var imageName = "nm-signal-100";
106−
107− if (adHoc) {
108− imageName = "nm-adhoc";
109− } else if (signalStrength == 0) {
110− imageName = "nm-signal-00";
111− } else if (signalStrength <= 25) {
112− imageName = "nm-signal-25";
113− } else if (signalStrength <= 50) {
114− imageName = "nm-signal-50";
115− } else if (signalStrength <= 75) {
116− imageName = "nm-signal-75";
117− }
118−
119− if (secure) {
120− imageName += "-secure";
121− }
122− return imageName;
123− }
124− iconFrame: false
125− control: CheckBox {
126− id: checkBoxActive
127−
128− onClicked: {
129− accessPoint.activate();
130− }
131− }
132− style: Rectangle {
133− color: "#4c000000"
134− }
135−
136− Component.onCompleted: {
137− loadAttributes();
138− }
139− onUnityMenuModelChanged: {
140− loadAttributes();
141− }
142− onMenuIndexChanged: {
143− loadAttributes();
144− }
145− onCheckedChanged: {
146− // Can't rely on binding. Checked is assigned on click.
147− checkBoxActive.checked = checked;
148− }
149− onActivate: unityMenuModel.activate(menuIndex);
150− }
151− }
152−
153− Column {
154− id: column
155− spacing: units.gu(2)
156− anchors.top: content.top
157− anchors.bottom: content.bottom
158− anchors.left: wifiPage.left
159− anchors.right: wifiPage.right
160−
161− Label {
162− id: label
163− anchors.left: parent.left
164− anchors.leftMargin: leftMargin
165− anchors.right: parent.right
166− anchors.rightMargin: rightMargin
167− fontSize: "small"
168− text: mainMenu.count > 0 ? i18n.tr("Available networks…")
169− : i18n.tr("No available networks.")
170− }
171−
172− Flickable {
173− anchors.left: parent.left
174− anchors.right: parent.right
175− height: column.height - label.height - column.spacing
176− contentHeight: contentItem.childrenRect.height
177− clip: true
178− flickDeceleration: 1500 * units.gridUnit / 8
179− maximumFlickVelocity: 2500 * units.gridUnit / 8
180− boundsBehavior: (contentHeight > height) ? Flickable.DragAndOvershootBounds : Flickable.StopAtBounds
181−
182− Column {
183− anchors.left: parent.left
184− anchors.right: parent.right
185−
186− Repeater {
187− id: mainMenu
188−
189− model: menuModel
190−
191− delegate: Loader {
192− id: loader
193−
194− readonly property bool isAccessPoint: model.type === "unity.widgets.systemsettings.tablet.accesspoint"
195−
196− anchors.left: parent.left
197− anchors.right: parent.right
198− height: isAccessPoint ? units.gu(6) : 0
199− asynchronous: true
200− sourceComponent: isAccessPoint ? accessPointComponent : null
201−
202− onLoaded: {
203− item.menuData = Qt.binding(function() { return model; });
204− item.menuIndex = Qt.binding(function() { return index; });
205− }
206− }
207− }
208− }
209− }
210− }
211−
212− Component {
213− id: forwardButton
214− LocalComponents.StackButton {
215− text: (connected || mainMenu.count === 0) ? i18n.tr("Continue") : i18n.tr("Skip")
216− onClicked: pageStack.next()
217− }
218− }
219−}
qml/Wizard/Pages/50-location.qmldeleted−150
@@ -1,150 +0,0 @@
1−/*
2− * Copyright (C) 2013-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.4
18−import AccountsService 0.1
19−import QMenuModel 0.1 as QMenuModel
20−import Qt.labs.folderlistmodel 2.1
21−import Ubuntu.Components 1.3
22−import ".." as LocalComponents
23−
24−LocalComponents.Page {
25− objectName: "locationPage"
26−
27− title: i18n.tr("Location")
28− forwardButtonSourceComponent: forwardButton
29−
30− property bool pathSet: AccountsService.hereLicensePathValid
31− skipValid: pathSet && (AccountsService.hereLicensePath === "" || termsModel.count > 0)
32− skip: skipValid && (AccountsService.hereLicensePath === "" || termsModel.count === 2) // no files but . and ..
33−
34− FolderListModel {
35− id: termsModel
36− folder: AccountsService.hereLicensePath
37− nameFilters: ["*.html"]
38− showOnlyReadable: true
39− showDotAndDotDot: true // so that count == 0 means we're not done scanning yet
40− }
41−
42− QMenuModel.QDBusActionGroup {
43− id: locationActionGroup
44− objectName: "locationActionGroup"
45− busType: QMenuModel.DBus.SessionBus
46− busName: "com.canonical.indicator.location"
47− objectPath: "/com/canonical/indicator/location"
48− property variant location: action("location-detection-enabled")
49− property variant gps: action("gps-detection-enabled")
50− Component.onCompleted: start()
51− }
52−
53− Column {
54− id: column
55− anchors.fill: content
56− spacing: units.gu(3)
57−
58− Label {
59− anchors.left: parent.left
60− anchors.right: parent.right
61− wrapMode: Text.Wrap
62− text: i18n.tr("Let the phone detect your location:")
63− }
64−
65− LocalComponents.CheckableSetting {
66− id: gpsCheck
67− objectName: "gpsCheck"
68− showDivider: false
69− text: i18n.tr("Using GPS only (less accurate)")
70− onTriggered: {
71− gpsCheck.checked = true;
72− hereCheck.checked = false;
73− nopeCheck.checked = false;
74− }
75− }
76−
77− Column {
78− anchors.left: parent.left
79− anchors.right: parent.right
80− height: childrenRect.height
81−
82− LocalComponents.CheckableSetting {
83− id: hereCheck
84− objectName: "hereCheck"
85− showDivider: false
86− text: i18n.tr("Using GPS, anonymized Wi-Fi and cellular network info (recommended)")
87− checked: true
88− onTriggered: {
89− gpsCheck.checked = false;
90− hereCheck.checked = true;
91− nopeCheck.checked = false;
92− }
93− }
94−
95− Label {
96− objectName: "hereTermsLink"
97− anchors.left: parent.left
98− anchors.leftMargin: hereCheck.labelOffset
99− anchors.right: parent.right
100− wrapMode: Text.Wrap
101− linkColor: theme.palette.normal.foregroundText
102− // Translators: HERE is a trademark for Nokia's location service, you probably shouldn't translate it
103− text: i18n.tr("By selecting this option you agree to the Nokia HERE <a href='#'>terms and conditions</a>.")
104− onLinkActivated: pageStack.load(Qt.resolvedUrl("here-terms.qml"))
105− }
106− }
107−
108− LocalComponents.CheckableSetting {
109− id: nopeCheck
110− objectName: "nopeCheck"
111− showDivider: false
112− text: i18n.tr("Not at all")
113− onTriggered: {
114− gpsCheck.checked = false;
115− hereCheck.checked = false;
116− nopeCheck.checked = true;
117− }
118− }
119−
120− Label {
121− anchors.left: parent.left
122− anchors.right: parent.right
123− wrapMode: Text.Wrap
124− text: i18n.tr("You can change your mind later in <b>System Settings</b>.")
125− }
126− }
127−
128− Component {
129− id: forwardButton
130− LocalComponents.StackButton {
131− text: i18n.tr("Continue")
132− onClicked: {
133− var locationOn = gpsCheck.checked || hereCheck.checked;
134− var gpsOn = gpsCheck.checked || hereCheck.checked;
135− var hereOn = hereCheck.checked;
136−
137− // location service doesn't currently listen to updateState
138− // requests, so we activate the actions if needed.
139− if (locationActionGroup.location.state != locationOn) {
140− locationActionGroup.location.activate();
141− }
142− if (locationActionGroup.gps.state != gpsOn) {
143− locationActionGroup.gps.activate();
144− }
145− AccountsService.hereEnabled = hereOn;
146− pageStack.next()
147− }
148− }
149− }
150−}
qml/Wizard/Pages/50-timezone.qmlnew file+275
@@ -0,0 +1,275 @@
1+/*
2+ * Copyright (C) 2015-2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import QtQuick.Layouts 1.1
19+import Ubuntu.Components 1.3
20+import Wizard 0.1
21+import Ubuntu.SystemSettings.TimeDate 1.0
22+import Utils 0.1 as Utils
23+import ".." as LocalComponents
24+
25+LocalComponents.Page {
26+ id: tzPage
27+ objectName: "tzPage"
28+
29+ title: i18n.tr("Time Zone")
30+ forwardButtonSourceComponent: forwardButton
31+
32+ property string selectedTimeZone: ""
33+ readonly property bool showingMap: wideMode && width >= units.gu(110)
34+
35+ // for testing
36+ readonly property alias tdModule: timeDatePanel
37+
38+ function highlightTimezone(offset) {
39+ highlightImage.source = "data/timezonemap/timezone_" + offset + ".png";
40+ }
41+
42+ // geo coords conversion functions (adapted from libtimezonemap)
43+ function radians(degrees) {
44+ return degrees * Math.PI / 180;
45+ }
46+
47+ function longitudeToX(longitude, map_width) {
48+ const xdeg_offset = -6;
49+ const x = (map_width * (180.0 + longitude) / 360.0) + (map_width * xdeg_offset / 180.0);
50+ return x;
51+ }
52+
53+ function latitudeToY(latitude, map_height) {
54+ const bottom_lat = -59;
55+ const top_lat = 81;
56+ const top_per = top_lat / 180.0;
57+
58+ var y = 1.25 * Math.log(Math.tan(0.25*Math.PI + 0.4 * radians(latitude)));
59+ const full_range = 4.6068250867599998;
60+ const top_offset = full_range * top_per;
61+ const map_range = Math.abs(1.25 * Math.log(Math.tan(0.25*Math.PI + 0.4 * radians(bottom_lat))) - top_offset);
62+ y = Math.abs(y - top_offset);
63+ y = y / map_range;
64+ y = y * map_height;
65+ return y;
66+ }
67+
68+ function resetViews() {
69+ selectedTimeZone = ""
70+ tzList.currentIndex = -1
71+ highlightImage.source = ""
72+ pinImage.x = 0;
73+ pinImage.y = 0;
74+ }
75+
76+ UbuntuTimeDatePanel {
77+ id: timeDatePanel
78+ }
79+
80+ onContentAnimationRunningChanged: {
81+ if (!contentAnimationRunning) {
82+ if (tzList.count == 1) { // preselect the first (and only) TZ
83+ var tz = tzList.itemAt(0,0);
84+ if (!!tz) {
85+ tz.clicked();
86+ }
87+ }
88+
89+ resetViews();
90+ searchField.forceActiveFocus();
91+ }
92+ }
93+
94+ Component {
95+ id: tzComponent
96+ ListItem {
97+ id: tz
98+ objectName: "tz" + index
99+ highlightColor: backgroundColor
100+ divider.colorFrom: dividerColor
101+ divider.colorTo: backgroundColor
102+ readonly property bool currentTz: ListView.view.currentIndex === index
103+
104+ Column {
105+ anchors.verticalCenter: parent.verticalCenter
106+ anchors.left: parent.left
107+ anchors.leftMargin: !wideMode ? staticMargin : 0
108+ anchors.right: image.left
109+ anchors.rightMargin: units.gu(2)
110+
111+ Label {
112+ id: cityLabel
113+ text: displayName
114+ font.weight: tz.currentTz ? Font.Normal : Font.Light
115+ fontSize: "medium"
116+ color: textColor
117+ elide: Text.ElideMiddle
118+ maximumLineCount: 1
119+ width: parent.width
120+ }
121+ Label {
122+ id: timeLabel
123+ text: Utils.TimezoneFormatter.currentTimeInTimezoneWithAbbrev(timeZone)
124+ font.weight: tz.currentTz ? Font.Normal : Font.Light
125+ fontSize: "small"
126+ color: textColor
127+ }
128+ }
129+ Image {
130+ id: image
131+ anchors {
132+ right: parent.right
133+ verticalCenter: parent.verticalCenter
134+ rightMargin: !wideMode ? staticMargin : 0
135+ }
136+ fillMode: Image.PreserveAspectFit
137+ height: units.gu(1.5)
138+
139+ source: "data/Tick@30.png"
140+ visible: tz.currentTz
141+ }
142+
143+ onClicked: {
144+ highlightTimezone(offset);
145+ ListView.view.currentIndex = index;
146+ selectedTimeZone = timeZone;
147+ //print("Clicked at city with coords:", longitude, latitude);
148+ //print("Clicked on TZ:", timeZone);
149+ //print("Highlight at (x,y):", longitudeToX(longitude, map.width), latitudeToY(latitude, map.height));
150+ pinImage.x = Qt.binding(function() { return longitudeToX(longitude, map.width) - pinImage.width; });
151+ pinImage.y = Qt.binding(function() { return latitudeToY(latitude, map.height) - pinImage.height; });
152+ }
153+ }
154+ }
155+
156+
157+ ColumnLayout {
158+ id: leftColumn
159+ anchors {
160+ left: content.left
161+ top: content.top
162+ bottom: content.bottom
163+ right: !showingMap ? content.right : undefined
164+ leftMargin: showingMap ? staticMargin : (wideMode ? tzPage.leftMargin : 0)
165+ rightMargin: showingMap ? staticMargin : (wideMode ? tzPage.rightMargin : 0)
166+ topMargin: customMargin
167+ }
168+
169+ width: Math.min(parent.width, units.gu(34))
170+
171+ LocalComponents.WizardTextField {
172+ id: searchField
173+ objectName: "tzFilter"
174+ anchors.left: parent.left
175+ anchors.right: parent.right
176+ anchors.leftMargin: !showingMap && !wideMode ? staticMargin : undefined
177+ anchors.rightMargin: !showingMap && !wideMode ? staticMargin : undefined
178+ placeholderText: i18n.tr("Enter your city")
179+ inputMethodHints: Qt.ImhNoPredictiveText
180+ onTextChanged: resetViews();
181+ }
182+
183+ ListView {
184+ Layout.fillHeight: true
185+ id: tzList
186+ objectName: "tzList"
187+ clip: true
188+ anchors.left: parent.left
189+ anchors.right: parent.right
190+ currentIndex: -1
191+ model: TimeZoneModel {
192+ id: timeZoneModel
193+ filter: searchField.text
194+ country: i18n.language.split('_')[1].split('.')[0]
195+ }
196+ delegate: tzComponent
197+ }
198+
199+ ActivityIndicator {
200+ anchors.centerIn: tzList
201+ running: tzList.count == 0 &&
202+ searchField.length > 0 &&
203+ timeZoneModel.listUpdating
204+ visible: running
205+ }
206+ }
207+
208+ Item {
209+ id: mapContainer
210+ visible: showingMap && !contentAnimationRunning
211+ enabled: visible
212+
213+ anchors {
214+ left: leftColumn.right
215+ leftMargin: units.gu(4)
216+ right: content.right
217+ rightMargin: staticMargin
218+ top: content.top
219+ topMargin: customMargin
220+ bottom: parent.bottom
221+ bottomMargin: buttonBarHeight
222+ }
223+
224+ Item {
225+ id: map
226+ width: Math.min(parent.width, height * 1.95) // keep our aspect ratio
227+ height: parent.height
228+ anchors {
229+ centerIn: parent
230+ }
231+
232+ Image {
233+ id: backgroundImage
234+ source: "data/timezonemap/map.png"
235+ sourceSize: Qt.size(map.width, map.height)
236+ fillMode: Image.PreserveAspectFit
237+ smooth: false
238+ visible: mapContainer.visible
239+ asynchronous: true
240+ anchors.fill: parent
241+ }
242+
243+ Image {
244+ id: highlightImage
245+ sourceSize: Qt.size(map.width, map.height)
246+ fillMode: Image.PreserveAspectFit
247+ smooth: false
248+ visible: selectedTimeZone != ""
249+ asynchronous: true
250+ anchors.fill: backgroundImage
251+ }
252+
253+ Image {
254+ id: pinImage
255+ source: "data/timezonemap/pin.png"
256+ visible: x != 0 && y != 0
257+ width: units.dp(12)
258+ height: units.dp(20)
259+ z: map.z + 1
260+ }
261+ }
262+ }
263+
264+ Component {
265+ id: forwardButton
266+ LocalComponents.StackButton {
267+ text: i18n.tr("Next")
268+ enabled: selectedTimeZone != ""
269+ onClicked: {
270+ timeDatePanel.timeZone = selectedTimeZone;
271+ pageStack.next();
272+ }
273+ }
274+ }
275+}
qml/Wizard/Pages/60-reporting.qml → qml/Wizard/Pages/60-account.qmlrenamed+30−20
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013,2014 Canonical, Ltd.
2+ * Copyright (C) 2015-2016 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
@@ -16,49 +16,59 @@
1616
1717import QtQuick 2.4
1818import Ubuntu.Components 1.3
19−import Ubuntu.SystemSettings.Diagnostics 1.0
19+import AccountsService 0.1
2020import ".." as LocalComponents
2121
2222LocalComponents.Page {
23− id: reportingPage
24− objectName: "reportingPage"
23+ objectName: "accountPage"
24+ title: i18n.tr("Personalize Your Device")
2525
26− title: i18n.tr("Improving your experience")
2726 forwardButtonSourceComponent: forwardButton
2827
29− skipValid: false
30− skip: !diagnostics.reportCrashes // skip the page when the system is configured not to report crashes
31−
32− UbuntuDiagnostics {
33− id: diagnostics
34− Component.onCompleted: reportingPage.skipValid = true;
28+ QtObject {
29+ id: d
30+ readonly property string validName: nameInput.text.trim()
3531 }
3632
3733 Column {
3834 id: column
39− anchors.fill: content
40− spacing: units.gu(2)
35+ spacing: units.gu(1)
36+ anchors {
37+ fill: content
38+ leftMargin: parent.leftMargin
39+ rightMargin: parent.rightMargin
40+ topMargin: customMargin
41+ }
4142
43+ // name
4244 Label {
45+ id: nameLabel
4346 anchors.left: parent.left
4447 anchors.right: parent.right
45− wrapMode: Text.Wrap
46− text: i18n.tr("Your phone is set up to automatically report errors to Canonical and its partners, the makers of the operating system.")
48+ text: i18n.tr("Preferred Name")
49+ color: textColor
50+ font.weight: Font.Light
4751 }
4852
49− Label {
53+ LocalComponents.WizardTextField {
54+ id: nameInput
55+ objectName: "nameInput"
5056 anchors.left: parent.left
5157 anchors.right: parent.right
52− wrapMode: Text.Wrap
53− text: i18n.tr("This can be disabled in <b>System Settings</b> under <b>Security &amp; Privacy</b>")
58+ inputMethodHints: Qt.ImhNoPredictiveText
5459 }
5560 }
5661
5762 Component {
5863 id: forwardButton
5964 LocalComponents.StackButton {
60− text: i18n.tr("Continue")
61− onClicked: pageStack.next()
65+ text: d.validName ? i18n.tr("Next") : i18n.tr("Skip")
66+ onClicked: {
67+ if (d.validName) {
68+ AccountsService.realName = d.validName;
69+ }
70+ pageStack.next();
71+ }
6272 }
6373 }
6474}
qml/Wizard/Pages/70-passwd-type.qmlnew file+166
@@ -0,0 +1,166 @@
1+/*
2+ * Copyright (C) 2014-2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import Ubuntu.Components 1.3
19+import Ubuntu.Components.ListItems 1.3
20+import Ubuntu.SystemSettings.SecurityPrivacy 1.0
21+import ".." as LocalComponents
22+
23+/**
24+ * One quirk with this page: we don't actually set the password. We avoid
25+ * doing it here because the user can come back to this page and change their
26+ * answer. We don't run as root, so if we did set the password immediately,
27+ * we'd need to prompt for their previous password when they came back and
28+ * changed their answer. Which is silly UX. So instead, we just keep track
29+ * of their choice and set the password at the end (see Pages.qml).
30+ * Setting the password shouldn't fail, since Ubuntu Touch has loose password
31+ * requirements, but we'll check what we can here. Ideally we'd be able to ask
32+ * the system if a password is legal without actually setting that password.
33+ */
34+
35+LocalComponents.Page {
36+ id: passwdPage
37+ objectName: "passwdPage"
38+
39+ title: i18n.tr("Lock Screen")
40+ forwardButtonSourceComponent: forwardButton
41+
42+ // If the user has set a password some other way (via ubuntu-device-flash
43+ // or this isn't the first time the wizard has been run, etc). We can't
44+ // properly set the password again, so let's not pretend we can.
45+ skip: securityPrivacy.securityType !== UbuntuSecurityPrivacyPanel.Swipe
46+
47+ function indexToMethod(index) {
48+ if (index === 0/* || index === 1*/)
49+ return UbuntuSecurityPrivacyPanel.Passphrase;
50+ else if (index === 1/*2*/)
51+ return UbuntuSecurityPrivacyPanel.Passcode;
52+ else
53+ return UbuntuSecurityPrivacyPanel.Swipe;
54+ }
55+
56+// Component.onCompleted: {
57+// if (root.password !== "") // the user has set a password as part of the previous page
58+// selector.currentIndex = 0;
59+// else
60+// selector.currentIndex = 1;
61+// }
62+
63+ Item {
64+ id: column
65+ anchors.fill: content
66+ anchors.topMargin: customMargin
67+ anchors.leftMargin: wideMode ? parent.leftMargin : 0
68+ anchors.rightMargin: wideMode ? parent.rightMargin : 0
69+
70+ ListView {
71+ id: selector
72+ anchors.left: parent.left
73+ anchors.right: parent.right
74+ boundsBehavior: Flickable.StopAtBounds
75+ clip: true
76+ height: childrenRect.height
77+
78+ // this is the order we want to display it; cf indexToMethod()
79+ model: [/*UbuntuSecurityPrivacyPanel.Passphrase, */UbuntuSecurityPrivacyPanel.Passphrase,
80+ UbuntuSecurityPrivacyPanel.Passcode, UbuntuSecurityPrivacyPanel.Swipe]
81+
82+ delegate: ListItem {
83+ id: itemDelegate
84+ objectName: "passwdDelegate" + index
85+ readonly property bool isCurrent: index === ListView.view.currentIndex
86+ highlightColor: backgroundColor
87+ divider.colorFrom: dividerColor
88+ divider.colorTo: backgroundColor
89+ Label {
90+ anchors.verticalCenter: parent.verticalCenter;
91+ anchors.left: parent.left
92+ anchors.leftMargin: column.anchors.leftMargin == 0 ? staticMargin : 0
93+ fontSize: "medium"
94+ color: textColor
95+ font.weight: itemDelegate.isCurrent ? Font.Normal : Font.Light
96+ text: {
97+ switch (index) {
98+// case 0:
99+// return i18n.ctr("Label: Type of security method", "Ubuntu administrator password");
100+ case 0:
101+ return i18n.ctr("Label: Type of security method", "Create new password");
102+ case 1:
103+ return i18n.ctr("Label: Type of security method", "Create passcode (numbers only)");
104+ case 2:
105+ return i18n.ctr("Label: Type of security method", "No lock code");
106+ }
107+ }
108+ width: parent.width
109+ wrapMode: Text.WordWrap
110+ }
111+
112+ Image {
113+ anchors {
114+ right: parent.right
115+ verticalCenter: parent.verticalCenter
116+ rightMargin: column.anchors.rightMargin == 0 ? staticMargin : 0
117+ }
118+ fillMode: Image.PreserveAspectFit
119+ height: units.gu(1.5)
120+
121+ source: "data/Tick@30.png"
122+ visible: itemDelegate.isCurrent
123+ }
124+
125+ onClicked: {
126+ selector.currentIndex = index;
127+ }
128+ }
129+ }
130+
131+ Rectangle {
132+ id: divider2
133+ anchors.left: parent.left
134+ anchors.right: parent.right
135+ anchors.top: selector.bottom
136+ height: units.dp(1)
137+ color: dividerColor
138+ }
139+ }
140+
141+ Component {
142+ id: forwardButton
143+ LocalComponents.StackButton {
144+ text: i18n.tr("Next")
145+ onClicked: {
146+ var method = indexToMethod(selector.currentIndex);
147+ root.passwordMethod = method;
148+
149+ if (method === UbuntuSecurityPrivacyPanel.Passphrase) { // any password
150+ if (selector.currentIndex == 0/*1*/)
151+ pageStack.load(Qt.resolvedUrl("password-set.qml")); // let the user choose a new password
152+ else
153+ pageStack.next(); // got the password already, go next page
154+ } else if (method === UbuntuSecurityPrivacyPanel.Passcode) { // passcode
155+ if (wideMode) {
156+ pageStack.load(Qt.resolvedUrl("passcode-desktop.qml"));
157+ } else {
158+ pageStack.load(Qt.resolvedUrl("passcode-set.qml"));
159+ }
160+ } else { //swipe
161+ pageStack.next();
162+ }
163+ }
164+ }
165+ }
166+}
qml/Wizard/Pages/75-report-check.qmlnew file+91
@@ -0,0 +1,91 @@
1+/*
2+ * Copyright (C) 2015-2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import Ubuntu.Components 1.3
19+import Ubuntu.Web 0.2
20+import ".." as LocalComponents
21+
22+LocalComponents.Page {
23+ id: reportingPage
24+ objectName: "reportingPage"
25+
26+ title: webview.visible ? i18n.tr("Privacy Policy") : i18n.tr("Help Us Improve")
27+ forwardButtonSourceComponent: !webview.visible ? forwardButton : null
28+ customBack: true
29+ customTitle: webview.visible
30+
31+ skip: !diagnostics.reportCrashes // skip the page when the system is configured not to report crashes
32+
33+ onBackClicked: {
34+ if (webview.visible) {
35+ webview.visible = false;
36+ } else {
37+ pageStack.prev();
38+ }
39+ }
40+
41+ Column {
42+ id: column
43+ anchors {
44+ fill: content
45+ leftMargin: leftMargin
46+ rightMargin: rightMargin
47+ topMargin: customMargin
48+ }
49+
50+ LocalComponents.CheckableSetting {
51+ anchors {
52+ left: parent.left
53+ right: parent.right
54+ }
55+
56+ id: reportCheck
57+ objectName: "reportCheck"
58+ showDivider: false
59+ text: i18n.tr("Improve system performance by sending us crashes and error reports.") + "<br><br><a href='#'>" +
60+ i18n.tr("Privacy policy") + "</a>"
61+ checked: diagnostics.reportCrashes
62+ visible: !webview.visible
63+ onLinkActivated: {
64+ webview.url = "http://www.ubuntu.com/legal/terms-and-policies/privacy-policy";
65+ webview.visible = true;
66+ }
67+ }
68+
69+ WebView {
70+ id: webview
71+ objectName: "webview"
72+ anchors.left: parent.left
73+ anchors.right: parent.right
74+ anchors.leftMargin: -leftMargin
75+ anchors.rightMargin: -rightMargin
76+ height: parent.height
77+ visible: false
78+ }
79+ }
80+
81+ Component {
82+ id: forwardButton
83+ LocalComponents.StackButton {
84+ text: i18n.tr("Next")
85+ onClicked: {
86+ diagnostics.setReportCrashes(reportCheck.checked);
87+ pageStack.next();
88+ }
89+ }
90+ }
91+}
qml/Wizard/Pages/80-finished.qml+94−16
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013,2014 Canonical, Ltd.
2+ * Copyright (C) 2013-2016 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
@@ -21,37 +21,115 @@ import ".." as LocalComponents
2121LocalComponents.Page {
2222 objectName: "finishedPage"
2323
24− title: i18n.tr("All done")
25− forwardButtonSourceComponent: forwardButton
2624 hasBackButton: false
25+ customTitle: true
26+ lastPage: true
27+ buttonBarVisible: false
2728
28− Column {
29+ Component.onCompleted: {
30+ state = "reanchored";
31+ }
32+
33+ states: State {
34+ name: "reanchored"
35+ AnchorChanges { target: bgImage; anchors.top: parent.top; anchors.bottom: parent.bottom }
36+ AnchorChanges { target: column;
37+ anchors.verticalCenter: parent.verticalCenter;
38+ anchors.top: undefined
39+ }
40+ }
41+
42+ transitions: Transition {
43+ ParallelAnimation {
44+ AnchorAnimation {
45+ targets: [bgImage, column]
46+ duration: UbuntuAnimation.SlowDuration
47+ easing.type: Easing.OutCirc
48+ }
49+ NumberAnimation {
50+ targets: [bgImage,column]
51+ property: "opacity"
52+ from: 0
53+ to: 1
54+ duration: UbuntuAnimation.SlowDuration
55+ easing.type: Easing.OutCirc
56+ }
57+ }
58+ }
59+
60+ Image {
61+ id: bgImage
62+ source: wideMode ? "data/Desktop_splash_screen_bkg.png" : "data/Phone_splash_screen_bkg.png"
63+ scale: Image.PreserveAspectFit
64+ anchors.left: parent.left
65+ anchors.right: parent.right
66+ anchors.bottom: parent.top // outside to let it slide down
67+ visible: opacity > 0
68+ }
69+
70+ Item {
2971 id: column
30− anchors.fill: content
31− spacing: units.gu(1)
72+ anchors.leftMargin: leftMargin
73+ anchors.rightMargin: rightMargin
74+ anchors.left: parent.left
75+ anchors.right: parent.right
76+ anchors.top: parent.bottom // outside to let it slide in
77+ height: childrenRect.height
78+ visible: opacity > 0
3279
3380 Label {
81+ id: welcomeLabel
3482 anchors.left: parent.left
3583 anchors.right: parent.right
84+ horizontalAlignment: Text.AlignHCenter
3685 wrapMode: Text.Wrap
37− fontSize: "large"
38− font.bold: true
39− text: i18n.tr("Nice work!")
86+ fontSize: "x-large"
87+ font.weight: Font.Light
88+ lineHeight: 1.2
89+ text: i18n.tr("Welcome to Ubuntu")
90+ color: whiteColor
4091 }
4192
4293 Label {
94+ id: welcomeText
4395 anchors.left: parent.left
4496 anchors.right: parent.right
97+ anchors.top: welcomeLabel.bottom
98+ anchors.topMargin: units.gu(2)
99+ horizontalAlignment: Text.AlignHCenter
45100 wrapMode: Text.Wrap
46− text: i18n.tr("Your device is now ready to use.")
101+ fontSize: "large"
102+ font.weight: Font.Light
103+ lineHeight: 1.2
104+ text: i18n.tr("You are ready to use your device now")
105+ color: whiteColor
47106 }
48− }
49107
50− Component {
51− id: forwardButton
52− LocalComponents.StackButton {
53− text: i18n.tr("Finish")
54− onClicked: root.quitWizard()
108+ Rectangle {
109+ anchors {
110+ top: welcomeText.bottom
111+ horizontalCenter: parent.horizontalCenter
112+ topMargin: units.gu(4)
113+ }
114+ color: "transparent"
115+ border.width: units.dp(1)
116+ border.color: whiteColor
117+ radius: units.dp(4)
118+ width: buttonLabel.paintedWidth + units.gu(6)
119+ height: buttonLabel.paintedHeight + units.gu(1.8)
120+
121+ Label {
122+ id: buttonLabel
123+ color: whiteColor
124+ text: i18n.tr("Get Started")
125+ fontSize: "medium"
126+ anchors.centerIn: parent
127+ }
128+ AbstractButton {
129+ objectName: "finishButton"
130+ anchors.fill: parent
131+ onClicked: root.quitWizard()
132+ }
55133 }
56134 }
57135}
qml/Wizard/Pages/data/Desktop_header_bkg.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/Desktop_splash_screen_bkg.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/Phone_header_bkg.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/Phone_splash_screen_bkg.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/Tick@30.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/meet_ubuntu_simcard@30.pngdeleted

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/map.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/pin.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_-1.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_-10.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_-11.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_-2.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_-3.5.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_-3.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_-4.5.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_-4.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_-5.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_-6.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_-7.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_-8.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_-9.5.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_-9.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_0.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_1.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_10.5.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_10.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_11.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_12.75.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_12.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_13.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_2.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_3.5.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_3.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_4.5.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_4.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_5.5.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_5.75.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_5.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_6.5.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_6.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_7.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_8.5.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_8.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_9.5.pngnew file

Binary file not shown.

qml/Wizard/Pages/data/timezonemap/timezone_9.pngnew file

Binary file not shown.

qml/Wizard/Pages/here-terms.qml+71−66
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2014,2015 Canonical, Ltd.
2+ * Copyright (C) 2014-2016 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
@@ -15,8 +15,6 @@
1515 */
1616
1717import QtQuick 2.4
18−import AccountsService 0.1
19−import Qt.labs.folderlistmodel 2.1
2018import Ubuntu.Components 1.3
2119import Ubuntu.Web 0.2
2220import ".." as LocalComponents
@@ -26,86 +24,91 @@ LocalComponents.Page {
2624
2725 title: i18n.tr("Terms & Conditions")
2826 customBack: true
27+ customTitle: true
2928
30− FolderListModel {
31− id: termsModel
32− folder: AccountsService.hereLicensePath
33− nameFilters: ["*.html"]
34− showDirs: false
35− showOnlyReadable: true
36− onCountChanged: loadFileContent()
37− }
38−
39− function makeFileName(lang, country) {
40− return lang + "_" + country + ".html"
41− }
42−
43− function defaultCountryForLanguage(lang) {
44− if (lang === "da") return "DK"
45− if (lang === "en") return "US"
46− if (lang === "ko") return "KR"
47− if (lang === "zh") return "CN"
48− return lang.toUpperCase()
49− }
50−
51− function determineFileName() {
52− var codes = i18n.language.split(".")[0].split("_")
53− var defaultCountry = defaultCountryForLanguage(codes[0])
54− if (codes.count === 1)
55− codes = [codes[0], defaultCountry]
56− var perfectMatch = makeFileName(codes[0], codes[1])
57− var nearMatch = makeFileName(codes[0], defaultCountry)
58− var nearMatchExists = false
59−
60− for (var i = 0; i < termsModel.count; i++) {
61− var fileName = termsModel.get(i, "fileName")
62− if (fileName == perfectMatch) {
63− return perfectMatch
64− } else if (fileName == nearMatch) {
65− nearMatchExists = true
66− }
67− }
68−
69− if (nearMatchExists) {
70− return nearMatch
29+ onBackClicked: {
30+ if (webview.visible) {
31+ showBrowser(false);
7132 } else {
72− return makeFileName("en", "US")
33+ pageStack.prev();
7334 }
7435 }
7536
76− function loadFileContent() {
77− var xhr = new XMLHttpRequest
78− xhr.open("GET", AccountsService.hereLicensePath + "/" + determineFileName())
79− xhr.onreadystatechange = function() {
80− if (xhr.readyState == XMLHttpRequest.DONE) {
81− termsLabel.text = xhr.responseText
82− }
83− }
84− xhr.send()
85− }
86−
87− onBackClicked: {
88− if (webview.visible) {
89− termsLabel.visible = true
37+ function showBrowser(show) {
38+ if (show) {
39+ label1.visible = false;
40+ label2.visible = false;
41+ label3.visible = false;
42+ label4.visible = false;
43+ webview.visible = true;
9044 } else {
91− pageStack.prev()
45+ webview.visible = false;
46+ label1.visible = true;
47+ label2.visible = true;
48+ label3.visible = true;
49+ label4.visible = true;
9250 }
9351 }
9452
9553 Column {
9654 id: column
9755 anchors.fill: content
56+ anchors.leftMargin: staticMargin
57+ anchors.rightMargin: wideMode ? parent.rightMargin : staticMargin
58+ spacing: units.gu(3)
59+
60+ Label {
61+ id: label1
62+ anchors.left: parent.left
63+ anchors.right: parent.right
64+ wrapMode: Text.Wrap
65+ color: textColor
66+ fontSize: "small"
67+ font.weight: Font.Light
68+ lineHeight: 1.2
69+ text: i18n.tr("Your device uses positioning technologies provided by HERE.")
70+ }
71+
72+ Label {
73+ id: label2
74+ anchors.left: parent.left
75+ anchors.right: parent.right
76+ wrapMode: Text.Wrap
77+ color: textColor
78+ fontSize: "small"
79+ font.weight: Font.Light
80+ lineHeight: 1.2
81+ text: i18n.tr("To provide you with positioning services and to improve their quality, HERE collects information about nearby cell towers and Wi-Fi hotspots around your current location whenever your position is being found.")
82+ }
83+
84+ Label {
85+ id: label3
86+ anchors.left: parent.left
87+ anchors.right: parent.right
88+ wrapMode: Text.Wrap
89+ color: textColor
90+ fontSize: "small"
91+ font.weight: Font.Light
92+ lineHeight: 1.2
93+ text: i18n.tr("The information collected is used to analyze the service and to improve the use of service, but not to identify you personally.")
94+ }
9895
9996 Label {
100− id: termsLabel
101− objectName: "termsLabel"
97+ id: label4
10298 anchors.left: parent.left
10399 anchors.right: parent.right
104100 wrapMode: Text.Wrap
105− linkColor: theme.palette.normal.foregroundText
101+ color: textColor
102+ fontSize: "small"
103+ font.weight: Font.Light
104+ lineHeight: 1.2
105+ linkColor: UbuntuColors.orange
106+ text: i18n.tr("By continuing, you agree to the HERE platform %1 and %2.")
107+ .arg("<a href=\"http://here.com/terms/service-terms\">" + i18n.tr("Service Terms") + "</a>")
108+ .arg("<a href=\"http://here.com/privacy/privacy-policy\">" + i18n.tr("Privacy Policy") + "</a>");
106109 onLinkActivated: {
107− webview.url = link
108− termsLabel.visible = false
110+ showBrowser(true);
111+ webview.url = link;
109112 }
110113 }
111114
@@ -114,8 +117,10 @@ LocalComponents.Page {
114117 objectName: "webview"
115118 anchors.left: parent.left
116119 anchors.right: parent.right
120+ anchors.leftMargin: -leftMargin
121+ anchors.rightMargin: -rightMargin
117122 height: parent.height
118− visible: !termsLabel.visible
123+ visible: false
119124 }
120125 }
121126}
qml/Wizard/Pages/passwd-confirm.qml → qml/Wizard/Pages/passcode-confirm.qmlrenamed+13−30
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2014 Canonical, Ltd.
2+ * Copyright (C) 2014-2016 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
@@ -26,11 +26,10 @@ import "../../Components" as UnityComponents
2626 */
2727
2828LocalComponents.Page {
29− id: passwdConfirmPage
30− objectName: "passwdConfirmPage"
31− forwardButtonSourceComponent: forwardButton
32−
33− skip: root.passwordMethod === UbuntuSecurityPrivacyPanel.Swipe
29+ id: passcodeConfirmPage
30+ objectName: "passcodeConfirmPage"
31+ customTitle: true
32+ backButtonText: i18n.tr("Cancel")
3433
3534 // If we are entering this page, clear any saved password and get focus
3635 onEnabledChanged: if (enabled) lockscreen.clear(false)
@@ -38,32 +37,26 @@ LocalComponents.Page {
3837 UnityComponents.Lockscreen {
3938 id: lockscreen
4039 anchors {
41− fill: parent
42− topMargin: topMargin
43− leftMargin: leftMargin
44− rightMargin: rightMargin
45− bottomMargin: buttonMargin
40+ fill: content
4641 }
4742
48− infoText: root.passwordMethod === UbuntuSecurityPrivacyPanel.Passphrase ?
49− i18n.tr("Confirm passphrase") :
50− i18n.tr("Confirm passcode")
43+ infoText: i18n.tr("Confirm passcode")
44+
45+ errorText: i18n.tr("Incorrect passcode.") + "\n" + i18n.ctr("Enter the passcode again", "Please re-enter.")
5146
52− errorText: root.passwordMethod === UbuntuSecurityPrivacyPanel.Passphrase ?
53− i18n.tr("Sorry, incorrect passphrase.") + "\n" + i18n.tr("Please try again.") :
54− i18n.tr("Sorry, incorrect passcode.") + "\n" + i18n.tr("Please try again.")
47+ foregroundColor: textColor
5548
5649 showEmergencyCallButton: false
5750 showCancelButton: false
58− alphaNumeric: root.passwordMethod === UbuntuSecurityPrivacyPanel.Passphrase
51+ alphaNumeric: false
5952 minPinLength: 4
6053 maxPinLength: 4
6154
6255 onEntered: {
6356 if (passphrase === root.password) {
64− confirmTimer.start()
57+ confirmTimer.start();
6558 } else {
66− clear(true)
59+ clear(true);
6760 }
6861 }
6962
@@ -73,14 +66,4 @@ LocalComponents.Page {
7366 onTriggered: pageStack.next()
7467 }
7568 }
76−
77− Component {
78− id: forwardButton
79− LocalComponents.StackButton {
80− visible: root.passwordMethod === UbuntuSecurityPrivacyPanel.Passphrase
81− enabled: root.password === lockscreen.passphrase
82− text: i18n.tr("Continue")
83− onClicked: pageStack.next()
84− }
85− }
8669}
qml/Wizard/Pages/passcode-desktop.qmlnew file+132
@@ -0,0 +1,132 @@
1+/*
2+ * Copyright (C) 2015-2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import QtQuick.Layouts 1.1
19+import Ubuntu.Components 1.3
20+import ".." as LocalComponents
21+
22+/**
23+ * See the main passwd-type page for an explanation of why we don't actually
24+ * directly set the password here.
25+ */
26+
27+LocalComponents.Page {
28+ id: passwdSetPage
29+ objectName: "passcodeDesktopPage"
30+ title: i18n.tr("Lock Screen Passcode")
31+ forwardButtonSourceComponent: forwardButton
32+
33+ readonly property alias password: passwordField.text
34+ readonly property alias password2: password2Field.text
35+
36+ GridLayout {
37+ id: column
38+ columns: 1
39+ rows: 3
40+ anchors.fill: content
41+ anchors.leftMargin: leftMargin
42+ anchors.rightMargin: rightMargin
43+ anchors.topMargin: customMargin
44+ rowSpacing: units.gu(3)
45+
46+ Label {
47+ id: infoLabel
48+ objectName: "infoLabel"
49+ anchors {
50+ left: parent.left
51+ right: parent.right
52+ }
53+ wrapMode: Text.Wrap
54+ font.weight: Font.Light
55+ color: textColor
56+ text: i18n.tr("Enter 4 numbers to setup your passcode")
57+ }
58+
59+ GridLayout {
60+ columns: 2
61+ columnSpacing: units.gu(2)
62+ rowSpacing: units.gu(2)
63+
64+ Label {
65+ text: i18n.tr("Choose passcode")
66+ color: textColor
67+ }
68+ LocalComponents.WizardTextField {
69+ Layout.fillWidth: true
70+ id: passwordField
71+ objectName: "passwordField"
72+ echoMode: TextInput.Password
73+ inputMethodHints: Qt.ImhDigitsOnly
74+ validator: RegExpValidator { regExp: /^\d{4}$/ }
75+ maximumLength: 4
76+ onAccepted: password2Field.forceActiveFocus()
77+ }
78+
79+ Label {
80+ text: i18n.tr("Confirm passcode")
81+ color: textColor
82+ }
83+ LocalComponents.WizardTextField {
84+ Layout.fillWidth: true
85+ id: password2Field
86+ objectName: "password2Field"
87+ echoMode: TextInput.Password
88+ inputMethodHints: Qt.ImhDigitsOnly
89+ validator: RegExpValidator { regExp: /^\d{4}$/ }
90+ maximumLength: 4
91+ }
92+
93+ Label {
94+ Layout.row: 2
95+ Layout.column: 1
96+ id: errorLabel
97+ property bool hasError: password && password != password2
98+ wrapMode: Text.Wrap
99+ color: hasError ? errorColor : UbuntuColors.ash
100+ visible: password && password2
101+ fontSize: "small"
102+ text: {
103+ if (password) {
104+ if (password2.length < password2Field.maximumLength)
105+ return i18n.tr("Passcode too short");
106+ else if (password == password2)
107+ return i18n.tr("Passcodes match");
108+ else if (password2)
109+ return i18n.tr("Passcodes do not match");
110+ }
111+ return "";
112+ }
113+ }
114+ }
115+
116+ Item { // spacer
117+ Layout.fillHeight: true
118+ }
119+ }
120+
121+ Component {
122+ id: forwardButton
123+ LocalComponents.StackButton {
124+ text: i18n.tr("Next")
125+ enabled: password != "" && password == password2
126+ onClicked: {
127+ root.password = password;
128+ pageStack.next();
129+ }
130+ }
131+ }
132+}
qml/Wizard/Pages/passwd-set.qml → qml/Wizard/Pages/passcode-set.qmlrenamed+13−29
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2014 Canonical, Ltd.
2+ * Copyright (C) 2014-2016 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
@@ -26,11 +26,10 @@ import "../../Components" as UnityComponents
2626 */
2727
2828LocalComponents.Page {
29− id: passwdSetPage
30− objectName: "passwdSetPage"
31− forwardButtonSourceComponent: forwardButton
32−
33− skip: root.passwordMethod === UbuntuSecurityPrivacyPanel.Swipe
29+ id: passcodeSetPage
30+ objectName: "passcodeSetPage"
31+ customTitle: true
32+ backButtonText: i18n.tr("Cancel")
3433
3534 // If we are entering this page, clear any saved password and get focus
3635 onEnabledChanged: if (enabled) lockscreen.clear(false)
@@ -43,52 +42,37 @@ LocalComponents.Page {
4342 Timer {
4443 id: confirmTimer
4544 interval: UbuntuAnimation.SnapDuration
46− onTriggered: pageStack.load(Qt.resolvedUrl("passwd-confirm.qml"));
45+ onTriggered: pageStack.load(Qt.resolvedUrl("passcode-confirm.qml"));
4746 }
4847
4948 UnityComponents.Lockscreen {
5049 id: lockscreen
5150 anchors {
52− fill: parent
53− topMargin: topMargin
54− leftMargin: leftMargin
55− rightMargin: rightMargin
56− bottomMargin: buttonMargin
51+ fill: content
5752 }
5853
59− infoText: root.passwordMethod === UbuntuSecurityPrivacyPanel.Passphrase ?
60− i18n.tr("Enter passphrase") :
61− i18n.tr("Choose your passcode")
54+ infoText: i18n.tr("Choose passcode")
55+ foregroundColor: textColor
6256
6357 // Note that the number four comes from PAM settings,
6458 // which we don't have a good way to interrogate. We
6559 // only do this matching instead of PAM because we want
6660 // to set the password via PAM in a different place
6761 // than this page. See comments at top of passwd-type file.
68− errorText: i18n.tr("Passphrase must be 4 characters long")
62+ errorText: i18n.tr("Passcode must be 4 characters long")
6963
7064 showEmergencyCallButton: false
7165 showCancelButton: false
72− alphaNumeric: root.passwordMethod === UbuntuSecurityPrivacyPanel.Passphrase
66+ alphaNumeric: false
7367 minPinLength: 4
7468 maxPinLength: 4
7569
7670 onEntered: {
7771 if (passphrase.length >= 4) {
78− passwdSetPage.confirm();
72+ passcodeSetPage.confirm();
7973 } else {
80− lockscreen.clear(true)
74+ lockscreen.clear(true);
8175 }
8276 }
8377 }
84−
85− Component {
86− id: forwardButton
87− LocalComponents.StackButton {
88− visible: root.passwordMethod === UbuntuSecurityPrivacyPanel.Passphrase
89− enabled: lockscreen.passphrase.length >= 4
90− text: i18n.tr("Continue")
91− onClicked: passwdSetPage.confirm()
92− }
93− }
9478}
qml/Wizard/Pages/password-set.qmlnew file+149
@@ -0,0 +1,149 @@
1+/*
2+ * Copyright (C) 2015-2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import QtQuick.Layouts 1.1
19+import Ubuntu.Components 1.3
20+import ".." as LocalComponents
21+
22+/**
23+ * See the main passwd-type page for an explanation of why we don't actually
24+ * directly set the password here.
25+ */
26+
27+LocalComponents.Page {
28+ id: passwdSetPage
29+ objectName: "passwdSetPage"
30+ title: i18n.tr("Lock Screen Password")
31+ forwardButtonSourceComponent: forwardButton
32+
33+ readonly property alias password: passwordField.text
34+ readonly property alias password2: password2Field.text
35+ readonly property bool passwordsMatching: password == password2 && password.trim().length > 7
36+
37+ Flickable {
38+ id: column
39+ clip: true
40+ flickableDirection: Flickable.VerticalFlick
41+ anchors.fill: content
42+ anchors.leftMargin: parent.leftMargin
43+ anchors.rightMargin: parent.rightMargin
44+ anchors.topMargin: customMargin
45+
46+ bottomMargin: Qt.inputMethod.keyboardRectangle.height - height
47+
48+ Behavior on contentY { UbuntuNumberAnimation {} }
49+
50+ // info label
51+ Label {
52+ id: infoLabel
53+ objectName: "infoLabel"
54+ anchors {
55+ left: parent.left
56+ right: parent.right
57+ }
58+ wrapMode: Text.Wrap
59+ font.weight: Font.Light
60+ color: textColor
61+ text: i18n.tr("Enter at least 8 characters")
62+ }
63+
64+ // password
65+ Label {
66+ id: pass1Label
67+ anchors {
68+ left: parent.left
69+ right: parent.right
70+ top: infoLabel.bottom
71+ topMargin: units.gu(3)
72+ }
73+ text: i18n.tr("Choose password")
74+ color: textColor
75+ }
76+ LocalComponents.WizardTextField {
77+ id: passwordField
78+ anchors {
79+ left: parent.left
80+ right: parent.right
81+ top: pass1Label.bottom
82+ topMargin: units.gu(1)
83+ }
84+ objectName: "passwordField"
85+ echoMode: TextInput.Password
86+ onAccepted: password2Field.forceActiveFocus()
87+ onActiveFocusChanged: {
88+ if (activeFocus) {
89+ column.contentY = pass1Label.y
90+ }
91+ }
92+ }
93+
94+ // password 2
95+ Label {
96+ id: pass2Label
97+ anchors {
98+ left: parent.left
99+ right: parent.right
100+ top: passwordField.bottom
101+ topMargin: units.gu(3)
102+ }
103+ text: i18n.tr("Confirm password")
104+ color: textColor
105+ }
106+ LocalComponents.WizardTextField {
107+ anchors {
108+ left: parent.left
109+ right: parent.right
110+ top: pass2Label.bottom
111+ topMargin: units.gu(1)
112+ }
113+ id: password2Field
114+ objectName: "password2Field"
115+ echoMode: TextInput.Password
116+ onActiveFocusChanged: {
117+ if (activeFocus) {
118+ column.contentY = pass2Label.y
119+ }
120+ }
121+ }
122+
123+ // password meter
124+ LocalComponents.PasswordMeter {
125+ id: passMeter
126+ anchors {
127+ left: parent.left
128+ right: parent.right
129+ top: password2Field.bottom
130+ topMargin: units.gu(1)
131+ }
132+
133+ password: passwordField.text
134+ matching: passwordsMatching ? true : (password2.trim().length > 0 ? false : undefined)
135+ }
136+ }
137+
138+ Component {
139+ id: forwardButton
140+ LocalComponents.StackButton {
141+ text: i18n.tr("Next")
142+ enabled: passwordsMatching
143+ onClicked: {
144+ root.password = password;
145+ pageStack.next();
146+ }
147+ }
148+ }
149+}
qml/Wizard/Pages/sim.qmlnew file+108
@@ -0,0 +1,108 @@
1+/*
2+ * Copyright (C) 2013-2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import MeeGo.QOfono 0.2
19+import Ubuntu.Components 1.3
20+import Ubuntu.Components.Popups 1.3
21+import Unity.Session 0.1
22+import ".." as LocalComponents
23+
24+LocalComponents.Page {
25+ objectName: "simPage"
26+
27+ title: i18n.tr("No SIM card installed")
28+ forwardButtonSourceComponent: forwardButton
29+ customTitle: true
30+ hasBackButton: false
31+
32+ property bool hadSIM: root.simManager0.present || root.simManager1.present
33+
34+ Timer {
35+ id: timer
36+ interval: 300
37+ running: true
38+ onTriggered: {
39+ hadSIM = root.simManager0.present || root.simManager1.present;
40+ }
41+ }
42+
43+ Connections {
44+ target: root.modemManager
45+ onGotSimCardChanged: {
46+ if (!hadSIM && root.modemManager.gotSimCard) { // show the restart dialog in case a SIM gets inserted
47+ restartDialog.visible = true;
48+ }
49+ }
50+ }
51+
52+ Dialog {
53+ id: restartDialog
54+ title: i18n.tr("SIM card added")
55+ text: i18n.tr("You must restart the device to access the mobile network.")
56+
57+ Button {
58+ id: restartButton
59+ text: i18n.tr("Restart")
60+ onClicked: {
61+ DBusUnitySessionService.reboot();
62+ }
63+ }
64+ }
65+
66+ Column {
67+ anchors {
68+ fill: content
69+ leftMargin: staticMargin
70+ rightMargin: staticMargin
71+ }
72+ spacing: units.gu(3)
73+
74+ Label {
75+ anchors.left: parent.left
76+ anchors.right: parent.right
77+ wrapMode: Text.Wrap
78+ text: i18n.tr("You won’t be able to make calls or use text messaging without a SIM.")
79+ fontSize: "small"
80+ font.weight: Font.Light
81+ lineHeight: 1.2
82+ color: restartDialog.visible ? theme.palette.normal.backgroundText : textColor
83+ }
84+
85+ Label {
86+ anchors.left: parent.left
87+ anchors.right: parent.right
88+ wrapMode: Text.Wrap
89+ textFormat: Text.RichText
90+ text: i18n.tr("To proceed with no SIM tap Skip.")
91+ fontSize: "small"
92+ font.weight: Font.Light
93+ lineHeight: 1.2
94+ color: restartDialog.visible ? theme.palette.normal.backgroundText : textColor
95+ }
96+ }
97+
98+ Component {
99+ id: forwardButton
100+ LocalComponents.StackButton {
101+ text: i18n.tr("Skip")
102+ onClicked: {
103+ seenSIMPage = true;
104+ pageStack.next();
105+ }
106+ }
107+ }
108+}
qml/Wizard/PasswordMeter.qmlnew file+106
@@ -0,0 +1,106 @@
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.4
18+import Ubuntu.Components 1.3
19+
20+Item {
21+ property string password: ""
22+ readonly property int passwordScore: scorePassword(password)
23+ property var matching
24+
25+ function scorePassword(pass) {
26+ var score = 0;
27+ if (!pass)
28+ return score;
29+
30+ // award every unique letter until 5 repetitions
31+ var letters = Object();
32+ for (var i=0; i<pass.length; i++) {
33+ letters[pass[i]] = (letters[pass[i]] || 0) + 1;
34+ score += 5.0 / letters[pass[i]];
35+ }
36+
37+ // bonus points for mixing it up
38+ var variations = {
39+ digits: /\d/.test(pass),
40+ lower: /[a-z]/.test(pass),
41+ upper: /[A-Z]/.test(pass),
42+ nonWords: /\W/.test(pass),
43+ }
44+
45+ var variationCount = 0;
46+ for (var check in variations) {
47+ variationCount += (variations[check] === true) ? 1 : 0;
48+ }
49+ score += (variationCount - 1) * 10;
50+
51+ return parseInt(score);
52+ }
53+
54+ Rectangle {
55+ id: passwordStrengthMeter
56+ anchors {
57+ left: parent.left
58+ right: parent.right
59+ }
60+ width: parent.width
61+ height: units.gu(0.5)
62+ color: {
63+ if (passwordScore > 80)
64+ return okColor;
65+ else if (passwordScore > 60)
66+ return "#f9c00f";
67+ else if (passwordScore >= 30)
68+ return errorColor;
69+
70+ return errorColor;
71+ }
72+ visible: password.length > 0
73+ }
74+
75+ Label {
76+ id: passwordStrengthInfo
77+ anchors {
78+ left: parent.left
79+ right: parent.right
80+ top: passwordStrengthMeter.bottom
81+ topMargin: units.gu(.5)
82+ }
83+ wrapMode: Text.Wrap
84+ text: {
85+ if (matching !== undefined) {
86+ if (matching)
87+ return i18n.tr("Passwords match");
88+ else if (!matching)
89+ return i18n.tr("Passwords do not match");
90+ }
91+
92+ if (passwordScore > 80)
93+ return i18n.tr("Strong password");
94+ else if (passwordScore > 60)
95+ return i18n.tr("Fair password");
96+ else if (passwordScore >= 30)
97+ return i18n.tr("Weak password");
98+
99+ return i18n.tr("Very weak password");
100+ }
101+ color: UbuntuColors.ash
102+ fontSize: "small"
103+ font.weight: Font.Light
104+ visible: password.length > 0
105+ }
106+}
qml/Wizard/StackButton.qml+7−12
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2014,2015 Canonical, Ltd.
2+ * Copyright (C) 2014-2016 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
@@ -25,23 +25,18 @@ AbstractButton {
2525 property bool backArrow: false
2626
2727 width: label.width
28− height: label.height + units.gu(4)
28+ height: units.gu(5)
2929
3030 Label {
3131 id: label
3232 anchors.verticalCenter: parent.verticalCenter
3333 anchors.left: parent.left
3434 anchors.right: parent.right
35− color: enabled ? theme.palette.normal.backgroundText : Qt.darker(theme.palette.normal.backgroundText, 1.5)
36− text: {
37− if (backArrow) {
38− // Translators: This is the arrow for "Back" buttons
39− return i18n.tr("〈 %1".arg(stackButton.text))
40− } else {
41− // Translators: This is the arrow for "Forward" buttons
42− return i18n.tr("%1 〉".arg(stackButton.text))
43− }
44− }
35+ color: "#333333"
36+ opacity: enabled ? 1.0 : 0.5
37+ fontSize: "medium"
38+ font.weight: backArrow ? Font.Light : Font.DemiBold
39+ text: stackButton.text
4540 horizontalAlignment: backArrow ? Text.AlignLeft : Text.AlignRight
4641 }
4742}
qml/Wizard/Wizard.qml−9
@@ -22,9 +22,6 @@ import "../Components"
2222Showable {
2323 id: root
2424
25− // The background wallpaper to use
26− property string background
27−
2825 // This is a bool instead of an alias because Loader classes like to emit
2926 // changed signals for 'active' during startup even if they aren't actually
3027 // changing values. Having it cached as a proper Qml bool property prevents
@@ -46,12 +43,6 @@ Showable {
4643 active: System.wizardEnabled
4744 source: "Pages.qml"
4845
49− Binding {
50− target: loader.item
51− property: "background"
52− value: root.background
53− }
54−
5546 Connections {
5647 target: loader.item
5748 onQuit: root.hide()
qml/Tutorial/TutorialLeftFinish.qml → qml/Wizard/WizardTextField.qmlrenamed+5−21
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2014 Canonical, Ltd.
2+ * Copyright (C) 2015-2016 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
@@ -16,26 +16,10 @@
1616
1717import QtQuick 2.4
1818import Ubuntu.Components 1.3
19−import "." as LocalComponents
19+import Ubuntu.Components.Themes.Ambiance 1.3
2020
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− ]
21+TextField {
22+ id: styledItem
23+ style: TextFieldStyle {
4024 }
4125}
src/MouseTouchAdaptor.cpp+53−9
@@ -71,6 +71,8 @@ using QTest::QTouchEventSequence;
7171namespace {
7272MouseTouchAdaptor *g_instance = nullptr;
7373
74+const Qt::KeyboardModifiers TRI_PRESS_MODIFIER = Qt::ShiftModifier|Qt::ControlModifier|Qt::AltModifier;
75+
7476Qt::MouseButton translateMouseButton(xcb_button_t detail)
7577{
7678 switch (detail) {
@@ -81,10 +83,25 @@ Qt::MouseButton translateMouseButton(xcb_button_t detail)
8183 default: return Qt::NoButton;
8284 }
8385}
86+
87+Qt::KeyboardModifiers translateMofidier(uint32_t mod)
88+{
89+ Qt::KeyboardModifiers qtMod = Qt::NoModifier;
90+
91+ if (mod & 0x01) qtMod |= Qt::ShiftModifier;
92+ if (mod & 0x04) qtMod |= Qt::ControlModifier;
93+ if (mod & 0x08) qtMod |= Qt::AltModifier;
94+ if (mod & 0x80) qtMod |= Qt::MetaModifier;
95+
96+ return qtMod;
97+}
8498} // end of anonymous namespace
8599
86100MouseTouchAdaptor::MouseTouchAdaptor()
87− : QObject(nullptr), m_leftButtonIsPressed(false), m_enabled(true)
101+ : QObject(nullptr)
102+ , m_leftButtonIsPressed(false)
103+ , m_triPressModifier(false)
104+ , m_enabled(true)
88105{
89106 QCoreApplication::instance()->installNativeEventFilter(this);
90107
@@ -192,17 +209,20 @@ bool MouseTouchAdaptor::xi2HandleEvent(xcb_ge_event_t *event)
192209 return handleButtonPress(
193210 static_cast<WId>(xiDeviceEvent->event),
194211 xiDeviceEvent->detail,
212+ xiDeviceEvent->mods.base_mods,
195213 fixed1616ToReal(xiDeviceEvent->event_x),
196214 fixed1616ToReal(xiDeviceEvent->event_y));
197215 case XI_ButtonRelease:
198216 return handleButtonRelease(
199217 static_cast<WId>(xiDeviceEvent->event),
200218 xiDeviceEvent->detail,
219+ xiDeviceEvent->mods.base_mods,
201220 fixed1616ToReal(xiDeviceEvent->event_x),
202221 fixed1616ToReal(xiDeviceEvent->event_y));
203222 case XI_Motion:
204223 return handleMotionNotify(
205224 static_cast<WId>(xiDeviceEvent->event),
225+ xiDeviceEvent->mods.base_mods,
206226 fixed1616ToReal(xiDeviceEvent->event_x),
207227 fixed1616ToReal(xiDeviceEvent->event_y));
208228 return true;
@@ -232,17 +252,18 @@ bool MouseTouchAdaptor::nativeEventFilter(const QByteArray & eventType,
232252 switch (xcbEvent->response_type & ~0x80) {
233253 case XCB_BUTTON_PRESS: {
234254 auto pressEvent = reinterpret_cast<xcb_button_press_event_t *>(xcbEvent);
235− return handleButtonPress(static_cast<WId>(pressEvent->event), pressEvent->detail,
255+ return handleButtonPress(static_cast<WId>(pressEvent->event), pressEvent->detail, 0,
236256 pressEvent->event_x, pressEvent->event_y);
237257 }
238258 case XCB_BUTTON_RELEASE: {
239259 auto releaseEvent = reinterpret_cast<xcb_button_release_event_t *>(xcbEvent);
240− return handleButtonRelease(static_cast<WId>(releaseEvent->event), releaseEvent->detail,
260+ return handleButtonRelease(static_cast<WId>(releaseEvent->event), releaseEvent->detail, 0,
241261 releaseEvent->event_x, releaseEvent->event_y);
242262 }
243263 case XCB_MOTION_NOTIFY: {
244264 auto motionEvent = reinterpret_cast<xcb_motion_notify_event_t *>(xcbEvent);
245− return handleMotionNotify(static_cast<WId>(motionEvent->event), motionEvent->event_x, motionEvent->event_y);
265+ return handleMotionNotify(static_cast<WId>(motionEvent->event), 0,
266+ motionEvent->event_x, motionEvent->event_y);
246267 }
247268 case XCB_GE_GENERIC:
248269 if (m_xi2Enabled) {
@@ -255,9 +276,10 @@ bool MouseTouchAdaptor::nativeEventFilter(const QByteArray & eventType,
255276 };
256277}
257278
258−bool MouseTouchAdaptor::handleButtonPress(WId windowId, uint32_t detail, int x, int y)
279+bool MouseTouchAdaptor::handleButtonPress(WId windowId, uint32_t detail, uint32_t modifiers, int x, int y)
259280{
260281 Qt::MouseButton button = translateMouseButton(detail);
282+ Qt::KeyboardModifiers qtMod = translateMofidier(modifiers);
261283
262284 // Just eat the event if it wasn't a left mouse press
263285 if (button != Qt::LeftButton)
@@ -270,19 +292,24 @@ bool MouseTouchAdaptor::handleButtonPress(WId windowId, uint32_t detail, int x,
270292 QTouchEventSequence touchEvent = QTest::touchEvent(targetWindow, m_touchDevice,
271293 false /* autoCommit */);
272294 touchEvent.press(0 /* touchId */, windowPos);
295+ if (qtMod == TRI_PRESS_MODIFIER) {
296+ touchEvent.press(1, windowPos);
297+ touchEvent.press(2, windowPos);
298+ m_triPressModifier = true;
299+ }
273300 touchEvent.commit(false /* processEvents */);
274301
275302 m_leftButtonIsPressed = true;
276303 return true;
277304}
278305
279−bool MouseTouchAdaptor::handleButtonRelease(WId windowId, uint32_t detail, int x, int y)
306+bool MouseTouchAdaptor::handleButtonRelease(WId windowId, uint32_t detail, uint32_t, int x, int y)
280307{
281308 Qt::MouseButton button = translateMouseButton(detail);
282309
283− // Just eat the event if it wasn't a left mouse release
310+ // Don't eat the event if it wasn't a left mouse press
284311 if (button != Qt::LeftButton)
285− return true;
312+ return false;
286313
287314 QWindow *targetWindow = findQWindowWithXWindowID(windowId);
288315
@@ -291,17 +318,23 @@ bool MouseTouchAdaptor::handleButtonRelease(WId windowId, uint32_t detail, int x
291318 QTouchEventSequence touchEvent = QTest::touchEvent(targetWindow, m_touchDevice,
292319 false /* autoCommit */);
293320 touchEvent.release(0 /* touchId */, windowPos);
321+ if (m_triPressModifier) {
322+ touchEvent.release(1, windowPos);
323+ touchEvent.release(2, windowPos);
324+ }
294325 touchEvent.commit(false /* processEvents */);
295326
296327 m_leftButtonIsPressed = false;
328+ m_triPressModifier = false;
297329 return true;
298330}
299331
300−bool MouseTouchAdaptor::handleMotionNotify(WId windowId, int x, int y)
332+bool MouseTouchAdaptor::handleMotionNotify(WId windowId, uint32_t modifiers, int x, int y)
301333{
302334 if (!m_leftButtonIsPressed) {
303335 return true;
304336 }
337+ Qt::KeyboardModifiers qtMod = translateMofidier(modifiers);
305338
306339 QWindow *targetWindow = findQWindowWithXWindowID(windowId);
307340
@@ -310,6 +343,17 @@ bool MouseTouchAdaptor::handleMotionNotify(WId windowId, int x, int y)
310343 QTouchEventSequence touchEvent = QTest::touchEvent(targetWindow, m_touchDevice,
311344 false /* autoCommit */);
312345 touchEvent.move(0 /* touchId */, windowPos);
346+ if (m_triPressModifier) {
347+ if (qtMod == TRI_PRESS_MODIFIER) {
348+ touchEvent.move(1, windowPos);
349+ touchEvent.move(2, windowPos);
350+ } else {
351+ // released modifiers
352+ touchEvent.release(1, windowPos);
353+ touchEvent.release(2, windowPos);
354+ m_triPressModifier = false;
355+ }
356+ }
313357 touchEvent.commit(false /* processEvents */);
314358
315359 return true;
src/MouseTouchAdaptor.h+5−3
@@ -51,13 +51,15 @@ private:
5151 void fetchXInput2Info();
5252 bool xi2HandleEvent(xcb_ge_event_t *event);
5353
54− bool handleButtonPress(WId windowId, uint32_t detail, int x, int y);
55− bool handleButtonRelease(WId windowId, uint32_t detail, int x, int y);
56− bool handleMotionNotify(WId windowId, int x, int y);
54+ bool handleButtonPress(WId windowId, uint32_t detail, uint32_t modifiers, int x, int y);
55+ bool handleButtonRelease(WId windowId, uint32_t detail, uint32_t modifiers, int x, int y);
56+ bool handleMotionNotify(WId windowId, uint32_t modifiers, int x, int y);
5757 QWindow *findQWindowWithXWindowID(WId windowId);
5858
5959 QTouchDevice *m_touchDevice;
6060 bool m_leftButtonIsPressed;
61+ bool m_triPressModifier;
62+
6163
6264 bool m_enabled;
6365
tests/autopilot/unity8/shell/emulators.py−2
@@ -36,7 +36,6 @@ __all__ = [
3636 'greeter',
3737 'launcher',
3838 'main_window',
39− 'tutorial',
4039 'UnityEmulatorException',
4140]
4241
@@ -46,7 +45,6 @@ from unity8 import (
4645 dash,
4746 launcher,
4847 shell as main_window,
49− tutorial,
5048 UnityException as UnityEmulatorException
5149)
5250from unity8.shell import create_interactive_notification
tests/autopilot/unity8/shell/tests/test_tutorial.pydeleted−57
@@ -1,57 +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, 2015 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 helpers custom proxy objects.
28−from unity8 import tutorial as tutorial_helpers # NOQA
29−
30−
31−class TutorialTestCase(tests.UnityTestCase):
32−
33− def setUp(self):
34− super().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/autopilot/unity8/tutorial.pydeleted−73
@@ -1,73 +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−
22−import ubuntuuitoolkit
23−
24−import autopilot
25−from autopilot import introspection
26−
27−
28−logger = logging.getLogger(__name__)
29−
30−
31−class TutorialPage(
32− ubuntuuitoolkit.UbuntuUIToolkitCustomProxyObjectBase):
33−
34− @classmethod
35− def validate_dbus_object(cls, path, state):
36− name = introspection.get_classname_from_path(path)
37− return name in (b'TutorialPage', b'TutorialLeft',
38− b'TutorialLeftFinish', b'TutorialRight',
39− b'TutorialBottom', b'TutorialBottomFinish')
40−
41− @autopilot.logging.log_action(logger.info)
42− def short_swipe_right(self):
43− self.shown.wait_for(True)
44− x, y, width, height = self.globalRect
45− start_x = x
46− stop_x = x + width // 3
47− start_y = stop_y = y + height // 2
48− self.pointing_device.drag(start_x, start_y, stop_x, stop_y)
49−
50− @autopilot.logging.log_action(logger.info)
51− def swipe_left(self):
52− self.shown.wait_for(True)
53− x, y, width, height = self.globalRect
54− start_x = width
55− stop_x = x
56− start_y = stop_y = y + height // 2
57− self.pointing_device.drag(start_x, start_y, stop_x, stop_y)
58−
59− @autopilot.logging.log_action(logger.info)
60− def swipe_up(self):
61− self.shown.wait_for(True)
62− x, y, width, height = self.globalRect
63− start_y = height
64− stop_y = y
65− start_x = stop_x = x + width // 2
66− self.pointing_device.drag(start_x, start_y, stop_x, stop_y)
67−
68− @autopilot.logging.log_action(logger.info)
69− def tap(self):
70− """Tap the tick button to complete this step."""
71− self.shown.wait_for(True)
72− button = self.wait_select_single(objectName="tick")
73− self.pointing_device.click_object(button)
tests/libs/UbuntuGestures/tst_TouchRegistry.cpp+59
@@ -42,6 +42,10 @@ public:
4242 QSet<int> ownedTouches;
4343 QSet<int> lostTouches;
4444 QList<TouchMemento> unownedTouchEvents;
45+
46+Q_SIGNALS:
47+ void gainedOwnership();
48+ void lostOwnership();
4549};
4650
4751class tst_TouchRegistry : public QObject
@@ -68,6 +72,7 @@ private Q_SLOTS:
6872 void removeOldUndecidedCandidates();
6973 void interimOwnerWontGetUnownedTouchEvents();
7074 void candidateVanishes();
75+ void candicateOwnershipReentrace();
7176
7277private:
7378 TouchRegistry *touchRegistry;
@@ -864,6 +869,58 @@ void tst_TouchRegistry::candidateVanishes()
864869 QVERIFY(mainCandidate.ownedTouches.contains(0));
865870}
866871
872+/*
873+ Regression test for canidate reentrance
874+
875+ Bug caused by candidates getting removed during ownership resolution
876+ */
877+void tst_TouchRegistry::candicateOwnershipReentrace()
878+{
879+ DummyCandidate mainCandidate;
880+ DummyCandidate candicate2;
881+ DummyCandidate candicate3;
882+
883+ {
884+ QList<QTouchEvent::TouchPoint> touchPoints;
885+ touchPoints.append(QTouchEvent::TouchPoint(0));
886+ touchPoints[0].setState(Qt::TouchPointPressed);
887+ QTouchEvent touchEvent(QEvent::TouchBegin,
888+ 0 /* device */,
889+ Qt::NoModifier,
890+ Qt::TouchPointPressed,
891+ touchPoints);
892+ touchRegistry->update(&touchEvent);
893+ }
894+
895+ // Re-entrance!
896+ connect(&candicate2, &DummyCandidate::lostOwnership, this, [&]() {
897+ touchRegistry->removeCandidateOwnerForTouch(0, &candicate2);
898+ });
899+
900+ touchRegistry->addCandidateOwnerForTouch(0, &mainCandidate);
901+ touchRegistry->addCandidateOwnerForTouch(0, &candicate2);
902+ touchRegistry->addCandidateOwnerForTouch(0, &candicate3);
903+
904+ {
905+ QList<QTouchEvent::TouchPoint> touchPoints;
906+ touchPoints.append(QTouchEvent::TouchPoint(0));
907+ touchPoints[0].setState(Qt::TouchPointMoved);
908+ QTouchEvent touchEvent(QEvent::TouchUpdate,
909+ 0 /* device */,
910+ Qt::NoModifier,
911+ Qt::TouchPointMoved,
912+ touchPoints);
913+ touchRegistry->update(&touchEvent);
914+ }
915+
916+ touchRegistry->requestTouchOwnership(0, &mainCandidate);
917+
918+ QCOMPARE(mainCandidate.ownedTouches.count(), 1);
919+ QCOMPARE(candicate2.lostTouches.count(), 1);
920+ QCOMPARE(candicate3.lostTouches.count(), 1);
921+}
922+
923+
867924////////////// TouchMemento //////////
868925
869926TouchMemento::TouchMemento(const QTouchEvent *touchEvent)
@@ -897,8 +954,10 @@ bool DummyCandidate::event(QEvent *e)
897954
898955 if (touchOwnershipEvent->gained()) {
899956 ownedTouches.insert(touchOwnershipEvent->touchId());
957+ Q_EMIT gainedOwnership();
900958 } else {
901959 lostTouches.insert(touchOwnershipEvent->touchId());
960+ Q_EMIT lostOwnership();
902961 }
903962 return true;
904963 } else if (e->type() == UnownedTouchEvent::unownedTouchEventType()) {
tests/mocks/AccountsService/AccountsService.cpp+60−3
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013 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
@@ -12,8 +12,6 @@
1212 *
1313 * You should have received a copy of the GNU General Public License
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− *
16− * Author: Michael Terry <michael.terry@canonical.com>
1715 */
1816
1917#include "AccountsService.h"
@@ -28,6 +26,7 @@ AccountsService::AccountsService(QObject* parent)
2826 m_statsWelcomeScreen(true),
2927 m_failedLogins(0),
3028 m_demoEdges(false),
29+ m_demoEdgesCompleted(),
3130 m_hereEnabled(false),
3231 m_hereLicensePath("")
3332{
@@ -56,6 +55,25 @@ void AccountsService::setDemoEdges(bool demoEdges)
5655 Q_EMIT demoEdgesChanged();
5756}
5857
58+QStringList AccountsService::demoEdgesCompleted() const
59+{
60+ return m_demoEdgesCompleted;
61+}
62+
63+void AccountsService::markDemoEdgeCompleted(const QString &edge)
64+{
65+ if (!m_demoEdgesCompleted.contains(edge)) {
66+ m_demoEdgesCompleted << edge;
67+ Q_EMIT demoEdgesCompletedChanged();
68+ }
69+}
70+
71+void AccountsService::setDemoEdgesCompleted(const QStringList &demoEdgesCompleted)
72+{
73+ m_demoEdgesCompleted = demoEdgesCompleted;
74+ Q_EMIT demoEdgesCompletedChanged();
75+}
76+
5977bool AccountsService::enableLauncherWhileLocked() const
6078{
6179 return m_enableLauncherWhileLocked;
@@ -152,3 +170,42 @@ bool AccountsService::hereLicensePathValid() const
152170{
153171 return !m_hereLicensePath.isNull();
154172}
173+
174+QString AccountsService::realName() const
175+{
176+ return m_realName;
177+}
178+
179+void AccountsService::setRealName(const QString &realName)
180+{
181+ m_realName = realName;
182+ Q_EMIT realNameChanged();
183+}
184+
185+QString AccountsService::email() const
186+{
187+ return m_email;
188+}
189+
190+void AccountsService::setEmail(const QString &email)
191+{
192+ m_email = email;
193+ Q_EMIT emailChanged();
194+}
195+
196+QStringList AccountsService::keymaps() const
197+{
198+ if (!m_kbdMap.isEmpty()) {
199+ return m_kbdMap;
200+ }
201+
202+ return {QStringLiteral("us")};
203+}
204+
205+void AccountsService::setKeymaps(const QStringList &keymaps)
206+{
207+ if (keymaps != m_kbdMap) {
208+ m_kbdMap = keymaps;
209+ Q_EMIT keymapsChanged();
210+ }
211+}
tests/mocks/AccountsService/AccountsService.h+30−5
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013 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
@@ -12,9 +12,6 @@
1212 *
1313 * You should have received a copy of the GNU General Public License
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− *
16− * Authors: Gerry Boland <gerry.boland@canonical.com>
17− * Michael Terry <michael.terry@canonical.com>
1815 */
1916
2017#ifndef UNITY_MOCK_ACCOUNTSSERVICE_H
@@ -22,6 +19,7 @@
2219
2320#include <QObject>
2421#include <QString>
22+#include <QStringList>
2523#include <QVariant>
2624
2725class AccountsService: public QObject
@@ -36,6 +34,10 @@ class AccountsService: public QObject
3634 READ demoEdges
3735 WRITE setDemoEdges
3836 NOTIFY demoEdgesChanged)
37+ Q_PROPERTY (QStringList demoEdgesCompleted
38+ READ demoEdgesCompleted
39+ WRITE setDemoEdgesCompleted // only available in mock
40+ NOTIFY demoEdgesCompletedChanged)
3941 Q_PROPERTY (bool enableLauncherWhileLocked
4042 READ enableLauncherWhileLocked
4143 WRITE setEnableLauncherWhileLocked // only available in mock
@@ -70,11 +72,17 @@ class AccountsService: public QObject
7072 Q_PROPERTY(bool hereLicensePathValid // qml sees a null string as "", so we use proxy setting for that
7173 READ hereLicensePathValid
7274 NOTIFY hereLicensePathChanged)
75+ Q_PROPERTY(QString realName READ realName WRITE setRealName NOTIFY realNameChanged)
76+ Q_PROPERTY(QString email READ email WRITE setEmail NOTIFY emailChanged)
77+ Q_PROPERTY(QStringList keymaps
78+ READ keymaps
79+ WRITE setKeymaps // only in mock
80+ NOTIFY keymapsChanged)
7381
7482public:
7583 enum PasswordDisplayHint {
7684 Keyboard,
77− Numeric,
85+ Numeric
7886 };
7987
8088 explicit AccountsService(QObject *parent = 0);
@@ -83,6 +91,9 @@ public:
8391 void setUser(const QString &user);
8492 bool demoEdges() const;
8593 void setDemoEdges(bool demoEdges);
94+ QStringList demoEdgesCompleted() const;
95+ void setDemoEdgesCompleted(const QStringList &demoEdges);
96+ Q_INVOKABLE void markDemoEdgeCompleted(const QString &edge);
8697 bool enableLauncherWhileLocked() const;
8798 void setEnableLauncherWhileLocked(bool enableLauncherWhileLocked);
8899 bool enableIndicatorsWhileLocked() const;
@@ -99,10 +110,17 @@ public:
99110 QString hereLicensePath() const;
100111 void setHereLicensePath(const QString &path);
101112 bool hereLicensePathValid() const;
113+ QString realName() const;
114+ void setRealName(const QString &realName);
115+ QString email() const;
116+ void setEmail(const QString &email);
117+ QStringList keymaps() const;
118+ void setKeymaps(const QStringList &keymaps);
102119
103120Q_SIGNALS:
104121 void userChanged();
105122 void demoEdgesChanged();
123+ void demoEdgesCompletedChanged();
106124 void enableLauncherWhileLockedChanged();
107125 void enableIndicatorsWhileLockedChanged();
108126 void backgroundFileChanged();
@@ -111,6 +129,9 @@ Q_SIGNALS:
111129 void failedLoginsChanged();
112130 void hereEnabledChanged();
113131 void hereLicensePathChanged();
132+ void realNameChanged();
133+ void emailChanged();
134+ void keymapsChanged();
114135
115136private:
116137 bool m_enableLauncherWhileLocked;
@@ -120,8 +141,12 @@ private:
120141 bool m_statsWelcomeScreen;
121142 uint m_failedLogins;
122143 bool m_demoEdges;
144+ QStringList m_demoEdgesCompleted;
123145 bool m_hereEnabled;
124146 QString m_hereLicensePath;
147+ QString m_realName;
148+ QStringList m_kbdMap;
149+ QString m_email;
125150};
126151
127152#endif
tests/mocks/AccountsService/AccountsService.qmltypes+5
@@ -23,6 +23,7 @@ Module {
2323 }
2424 Property { name: "user"; type: "string" }
2525 Property { name: "demoEdges"; type: "bool" }
26+ Property { name: "demoEdgesCompleted"; type: "QStringList" }
2627 Property { name: "enableLauncherWhileLocked"; type: "bool" }
2728 Property { name: "enableIndicatorsWhileLocked"; type: "bool" }
2829 Property { name: "backgroundFile"; type: "string" }
@@ -32,5 +33,9 @@ Module {
3233 Property { name: "hereEnabled"; type: "bool" }
3334 Property { name: "hereLicensePath"; type: "string" }
3435 Property { name: "hereLicensePathValid"; type: "bool"; isReadonly: true }
36+ Method {
37+ name: "markDemoEdgeCompleted"
38+ Parameter { name: "edge"; type: "string" }
39+ }
3540 }
3641}
tests/mocks/MeeGo/QOfono/MockOfonoSimManager.qml+1
@@ -23,6 +23,7 @@ Item {
2323 property string modemPath
2424 readonly property alias present: d.present
2525 readonly property alias ready: d.ready
26+ readonly property var preferredLanguages: ["de", "fr", "it", "en"]
2627
2728 QtObject {
2829 id: d
tests/mocks/QMenuModel/QDBusActionGroup.qml+5−5
@@ -1,8 +1,5 @@
11/*
2− * Copyright (C) 2013 Canonical, Ltd.
3− *
4− * Authors:
5− * Daniel d'Andrada <daniel.dandrada@canonical.com>
2+ * Copyright (C) 2013-2016 Canonical, Ltd.
63 *
74 * This program is free software; you can redistribute it and/or modify
85 * it under the terms of the GNU General Public License as published by
@@ -18,7 +15,6 @@
1815 */
1916
2017import QtQuick 2.4
21−import Ubuntu.Settings.Menus 0.1 as Menus
2218import QMenuModel 0.1
2319
2420QtObject {
@@ -47,6 +43,10 @@ QtObject {
4743 function activate() {
4844 activated();
4945 }
46+
47+ function updateState(newState) {
48+ state = newState;
49+ }
5050 }", actionGroup);
5151 }
5252}
tests/mocks/Ubuntu/Connectivity/plugin.cpp+1
@@ -36,6 +36,7 @@ QmlConnectivityNetworkingPlugin::registerTypes(const char *uri)
3636{
3737 // @uri Ubuntu.Connectivity
3838 qmlRegisterSingletonType<NetworkingStatus>(uri, 1, 0, "NetworkingStatus", networkingStatusSingletonProvider);
39+ qmlRegisterSingletonType<NetworkingStatus>(uri, 1, 0, "Connectivity", networkingStatusSingletonProvider);
3940}
4041
4142void
tests/mocks/Ubuntu/SystemSettings/CMakeLists.txt+2
@@ -1,2 +1,4 @@
11add_subdirectory(LanguagePlugin)
22add_subdirectory(SecurityPrivacy)
3+add_subdirectory(TimeDate)
4+add_subdirectory(Diagnostics)
tests/mocks/Ubuntu/SystemSettings/Diagnostics/CMakeLists.txtnew file+1
@@ -0,0 +1 @@
1+add_unity8_mock(Ubuntu.SystemSettings.Diagnostics 1.0 Ubuntu/SystemSettings/Diagnostics)
qml/Tutorial/Tick.qml → tests/mocks/Ubuntu/SystemSettings/Diagnostics/MockDiagnostics.qmlrenamed+4−9
@@ -15,15 +15,10 @@
1515 */
1616
1717import QtQuick 2.4
18−import Ubuntu.Components 1.3
1918
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)
19+Item {
20+ property bool reportCrashes: true
21+ function setReportCrashes(report) {
22+ reportCrashes = report;
2823 }
2924}
tests/mocks/Ubuntu/SystemSettings/Diagnostics/qmldirnew file+2
@@ -0,0 +1,2 @@
1+module Ubuntu.SystemSettings.Diagnostics
2+UbuntuDiagnostics 1.0 MockDiagnostics.qml
tests/mocks/Ubuntu/SystemSettings/LanguagePlugin/MockKeyboardPlugin.qmlnew file+21
@@ -0,0 +1,21 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+
19+Item {
20+ function setCurrentLayout(language) {}
21+}
tests/mocks/Ubuntu/SystemSettings/LanguagePlugin/MockLanguagePlugin.qml+2−2
@@ -17,7 +17,7 @@
1717import QtQuick 2.4
1818
1919Item {
20− property var languageNames: ["English", "French", "Spanish"]
21− property var languageCodes: ["en", "fr", "es"]
20+ property var languageNames: ["English (United States)", "French (France)", "Spanish (Spain)"]
21+ property var languageCodes: ["en_US", "fr_FR", "es_ES"]
2222 property int currentLanguage: 0
2323}
tests/mocks/Ubuntu/SystemSettings/LanguagePlugin/qmldir+1
@@ -1,2 +1,3 @@
11module Ubuntu.SystemSettings.LanguagePlugin
22UbuntuLanguagePlugin 1.0 MockLanguagePlugin.qml
3+OnScreenKeyboardPlugin 1.0 MockKeyboardPlugin.qml
tests/mocks/Ubuntu/SystemSettings/SecurityPrivacy/MockSecurityPrivacy.cpp+1−1
@@ -31,5 +31,5 @@ QString MockSecurityPrivacy::setSecurity(const QString &oldPasswd, const QString
3131{
3232 m_type = newType;
3333 Q_EMIT setSecurityCalled(oldPasswd, newPasswd, newType);
34− return "";
34+ return QString();
3535}
tests/mocks/Ubuntu/SystemSettings/TimeDate/CMakeLists.txtnew file+1
@@ -0,0 +1 @@
1+add_unity8_mock(Ubuntu.SystemSettings.TimeDate 1.0 Ubuntu/SystemSettings/TimeDate)
tests/mocks/Ubuntu/SystemSettings/TimeDate/MockTimeDate.qmlnew file+25
@@ -0,0 +1,25 @@
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.4
18+
19+Item {
20+ id: root
21+ property string timeZone: "Europe/Prague"
22+ onTimeZoneChanged: root.timeZoneChangedCalled(root.timeZone);
23+
24+ signal timeZoneChangedCalled(string tz);
25+}
tests/mocks/Ubuntu/SystemSettings/TimeDate/qmldirnew file+2
@@ -0,0 +1,2 @@
1+module Ubuntu.SystemSettings.TimeDate
2+UbuntuTimeDatePanel 1.0 MockTimeDate.qml
tests/mocks/Unity/Application/ApplicationInfo.cpp+21−3
@@ -44,6 +44,7 @@ ApplicationInfo::ApplicationInfo(const QString &appId, QObject *parent)
4444 , m_isTouchApp(true)
4545 , m_exemptFromLifecycle(false)
4646 , m_manualSurfaceCreation(false)
47+ , m_shellChrome(Mir::NormalChrome)
4748{
4849}
4950
@@ -63,6 +64,7 @@ ApplicationInfo::ApplicationInfo(QObject *parent)
6364 , m_isTouchApp(true)
6465 , m_exemptFromLifecycle(false)
6566 , m_manualSurfaceCreation(false)
67+ , m_shellChrome(Mir::NormalChrome)
6668{
6769}
6870
@@ -102,9 +104,11 @@ void ApplicationInfo::setSession(Session* session)
102104 if (m_session) {
103105 m_session->setApplication(this);
104106 m_session->setParent(this);
107+ m_session->setFullscreen(m_fullscreen);
105108 SessionManager::singleton()->registerSession(m_session);
106109 connect(m_session, &Session::surfaceAdded,
107110 this, &ApplicationInfo::onSessionSurfaceAdded);
111+ connect(m_session, &Session::fullscreenChanged, this, &ApplicationInfo::fullscreenChanged);
108112
109113 if (!m_manualSurfaceCreation) {
110114 QTimer::singleShot(500, m_session, &Session::createSurface);
@@ -191,12 +195,17 @@ void ApplicationInfo::setFocused(bool value)
191195
192196void ApplicationInfo::setFullscreen(bool value)
193197{
194− if (value != m_fullscreen) {
195− m_fullscreen = value;
196− Q_EMIT fullscreenChanged(value);
198+ m_fullscreen = value;
199+ if (m_session) {
200+ m_session->setFullscreen(value);
197201 }
198202}
199203
204+bool ApplicationInfo::fullscreen() const
205+{
206+ return m_session ? m_session->fullscreen() : m_fullscreen;
207+}
208+
200209void ApplicationInfo::setManualSurfaceCreation(bool value)
201210{
202211 if (value != m_manualSurfaceCreation) {
@@ -262,6 +271,7 @@ void ApplicationInfo::onSessionSurfaceAdded(MirSurface* surface)
262271 } else {
263272 setState(Suspended);
264273 }
274+ surface->setShellChrome(m_shellChrome);
265275 }
266276}
267277
@@ -291,3 +301,11 @@ void ApplicationInfo::setInitialSurfaceSize(const QSize &size)
291301 Q_EMIT initialSurfaceSizeChanged(m_initialSurfaceSize);
292302 }
293303}
304+
305+void ApplicationInfo::setShellChrome(Mir::ShellChrome shellChrome)
306+{
307+ m_shellChrome = shellChrome;
308+ if (m_session && m_session->lastSurface()) {
309+ m_session->lastSurface()->setShellChrome(shellChrome);
310+ }
311+}
tests/mocks/Unity/Application/ApplicationInfo.h+6−2
@@ -24,6 +24,7 @@ class Session;
2424
2525// unity-api
2626#include <unity/shell/application/ApplicationInfoInterface.h>
27+#include <unity/shell/application/Mir.h>
2728
2829using namespace unity::shell::application;
2930
@@ -59,7 +60,7 @@ public:
5960
6061 QUrl icon() const override { return m_icon; }
6162
62− void setStage(Stage value);
63+ Q_INVOKABLE void setStage(Stage value); // invokable only for mock
6364 Stage stage() const override { return m_stage; }
6465
6566 Q_INVOKABLE void setState(State value);
@@ -78,7 +79,7 @@ public:
7879 QString screenshot() const { return m_screenshotFileName; }
7980
8081 void setFullscreen(bool value);
81− bool fullscreen() const { return m_fullscreen; }
82+ bool fullscreen() const;
8283
8384 Qt::ScreenOrientations supportedOrientations() const override;
8485 void setSupportedOrientations(Qt::ScreenOrientations orientations);
@@ -97,6 +98,8 @@ public:
9798
9899 QSize initialSurfaceSize() const override;
99100 void setInitialSurfaceSize(const QSize &size) override;
101+
102+ Q_INVOKABLE void setShellChrome(Mir::ShellChrome shellChrome);
100103public:
101104 void setSession(Session* session);
102105 Session* session() const { return m_session; }
@@ -134,6 +137,7 @@ private:
134137 QSize m_initialSurfaceSize;
135138
136139 bool m_manualSurfaceCreation;
140+ Mir::ShellChrome m_shellChrome;
137141};
138142
139143Q_DECLARE_METATYPE(ApplicationInfo*)
tests/mocks/Unity/Application/ApplicationManager.cpp+13−26
@@ -151,10 +151,6 @@ void ApplicationManager::add(ApplicationInfo *application) {
151151
152152 beginInsertRows(QModelIndex(), m_runningApplications.size(), m_runningApplications.size());
153153 m_runningApplications.append(application);
154− endInsertRows();
155− Q_EMIT applicationAdded(application->appId());
156− Q_EMIT countChanged();
157− if (count() == 1) Q_EMIT emptyChanged(isEmpty()); // was empty but not anymore
158154
159155 connect(application, &ApplicationInfo::sessionChanged, this, [application, this]() {
160156 QModelIndex appIndex = findIndex(application);
@@ -171,6 +167,16 @@ void ApplicationManager::add(ApplicationInfo *application) {
171167 if (!appIndex.isValid()) return;
172168 Q_EMIT dataChanged(appIndex, appIndex, QVector<int>() << ApplicationManager::RoleState);
173169 });
170+ connect(application, &ApplicationInfo::stageChanged, this, [application, this]() {
171+ QModelIndex appIndex = findIndex(application);
172+ if (!appIndex.isValid()) return;
173+ Q_EMIT dataChanged(appIndex, appIndex, QVector<int>() << ApplicationManager::RoleStage);
174+ });
175+
176+ endInsertRows();
177+ Q_EMIT applicationAdded(application->appId());
178+ Q_EMIT countChanged();
179+ if (count() == 1) Q_EMIT emptyChanged(isEmpty()); // was empty but not anymore
174180}
175181
176182void ApplicationManager::remove(ApplicationInfo *application) {
@@ -202,23 +208,11 @@ void ApplicationManager::move(int from, int to) {
202208
203209ApplicationInfo* ApplicationManager::startApplication(const QString &appId,
204210 const QStringList &arguments)
205−{
206− return startApplication(appId, NoFlag, arguments);
207−}
208−
209−ApplicationInfo* ApplicationManager::startApplication(const QString &appId,
210− ExecFlags flags,
211− const QStringList &arguments)
212211{
213212 Q_UNUSED(arguments)
214213 ApplicationInfo *application = add(appId);
215214 if (!application)
216215 return 0;
217−
218− if (flags.testFlag(ApplicationManager::ForceMainStage)
219− && application->stage() == ApplicationInfo::SideStage) {
220− application->setStage(ApplicationInfo::MainStage);
221− }
222216 application->setState(ApplicationInfo::Starting);
223217
224218 return application;
@@ -317,7 +311,6 @@ void ApplicationManager::buildListOfAvailableApplications()
317311 application->setAppId("unity8-dash");
318312 application->setName("Unity 8 Mock Dash");
319313 application->setScreenshotId("unity8-dash");
320− application->setStage(ApplicationInfo::MainStage);
321314 application->setSupportedOrientations(Qt::PrimaryOrientation);
322315 m_availableApplications.append(application);
323316
@@ -326,7 +319,6 @@ void ApplicationManager::buildListOfAvailableApplications()
326319 application->setName("Dialer");
327320 application->setScreenshotId("dialer");
328321 application->setIconId("dialer-app");
329− application->setStage(ApplicationInfo::SideStage);
330322 application->setSupportedOrientations(Qt::PortraitOrientation
331323 | Qt::InvertedPortraitOrientation);
332324 m_availableApplications.append(application);
@@ -336,6 +328,7 @@ void ApplicationManager::buildListOfAvailableApplications()
336328 application->setName("Camera");
337329 application->setScreenshotId("camera");
338330 application->setIconId("camera");
331+ application->setShellChrome(Mir::LowChrome);
339332 application->setFullscreen(true);
340333 application->setSupportedOrientations(Qt::PortraitOrientation
341334 | Qt::LandscapeOrientation
@@ -349,7 +342,7 @@ void ApplicationManager::buildListOfAvailableApplications()
349342 application->setName("Gallery");
350343 application->setScreenshotId("gallery");
351344 application->setIconId("gallery");
352− application->setFullscreen(true);
345+ application->setShellChrome(Mir::LowChrome);
353346 application->setStage(ApplicationInfo::MainStage);
354347 m_availableApplications.append(application);
355348
@@ -358,12 +351,11 @@ void ApplicationManager::buildListOfAvailableApplications()
358351 application->setName("Facebook");
359352 application->setScreenshotId("facebook");
360353 application->setIconId("facebook");
361− application->setStage(ApplicationInfo::SideStage);
362354 m_availableApplications.append(application);
363355
364356 application = new ApplicationInfo(this);
365357 application->setAppId("webbrowser-app");
366− application->setFullscreen(true);
358+ application->setShellChrome(Mir::LowChrome);
367359 application->setName("Browser");
368360 application->setScreenshotId("browser");
369361 application->setIconId("browser");
@@ -374,7 +366,6 @@ void ApplicationManager::buildListOfAvailableApplications()
374366 application->setName("Twitter");
375367 application->setScreenshotId("twitter");
376368 application->setIconId("twitter");
377− application->setStage(ApplicationInfo::SideStage);
378369 m_availableApplications.append(application);
379370
380371 application = new ApplicationInfo(this);
@@ -389,7 +380,6 @@ void ApplicationManager::buildListOfAvailableApplications()
389380 application->setName("GMail");
390381 application->setIconId("gmail");
391382 application->setScreenshotId("gmail-webapp.svg");
392− application->setFullscreen(false);
393383 application->setStage(ApplicationInfo::MainStage);
394384 application->setSupportedOrientations(Qt::PortraitOrientation
395385 | Qt::LandscapeOrientation
@@ -402,7 +392,6 @@ void ApplicationManager::buildListOfAvailableApplications()
402392 application->setName("Music");
403393 application->setIconId("soundcloud");
404394 application->setScreenshotId("music");
405− application->setFullscreen(false);
406395 application->setStage(ApplicationInfo::MainStage);
407396 application->setSupportedOrientations(Qt::PortraitOrientation
408397 | Qt::LandscapeOrientation
@@ -423,14 +412,12 @@ void ApplicationManager::buildListOfAvailableApplications()
423412 application->setAppId("notes-app");
424413 application->setName("Notepad");
425414 application->setIconId("notepad");
426− application->setStage(ApplicationInfo::SideStage);
427415 m_availableApplications.append(application);
428416
429417 application = new ApplicationInfo(this);
430418 application->setAppId("calendar-app");
431419 application->setName("Calendar");
432420 application->setIconId("calendar");
433− application->setStage(ApplicationInfo::SideStage);
434421 m_availableApplications.append(application);
435422
436423 application = new ApplicationInfo(this);
tests/mocks/Unity/Application/ApplicationManager.h−8
@@ -46,11 +46,6 @@ class ApplicationManager : public ApplicationManagerInterface {
4646 RoleSession = RoleExemptFromLifecycle+1,
4747 RoleFullscreen,
4848 };
49− enum Flag {
50− NoFlag = 0x0,
51− ForceMainStage = 0x1,
52− };
53− Q_DECLARE_FLAGS(ExecFlags, Flag)
5449
5550 // QAbstractItemModel methods.
5651 int rowCount(const QModelIndex& parent = QModelIndex()) const override;
@@ -65,7 +60,6 @@ class ApplicationManager : public ApplicationManagerInterface {
6560 Q_INVOKABLE bool focusApplication(const QString &appId) override;
6661 Q_INVOKABLE void unfocusCurrentApplication() override;
6762 Q_INVOKABLE ApplicationInfo *startApplication(const QString &appId, const QStringList &arguments = QStringList()) override;
68− Q_INVOKABLE ApplicationInfo *startApplication(const QString &appId, ExecFlags flags, const QStringList &arguments = QStringList());
6963 Q_INVOKABLE bool stopApplication(const QString &appId) override;
7064
7165 QString focusedApplicationId() const override;
@@ -98,8 +92,6 @@ class ApplicationManager : public ApplicationManagerInterface {
9892 static ApplicationManager *the_application_manager;
9993};
10094
101−Q_DECLARE_OPERATORS_FOR_FLAGS(ApplicationManager::ExecFlags)
102−
10395Q_DECLARE_METATYPE(ApplicationManager*)
10496
10597#endif // APPLICATION_MANAGER_H
tests/mocks/Unity/Application/MirSurface.cpp+32
@@ -38,6 +38,7 @@ MirSurface::MirSurface(const QString& name,
3838 , m_width(-1)
3939 , m_height(-1)
4040 , m_slowToResize(false)
41+ , m_shellChrome(Mir::NormalChrome)
4142{
4243// qDebug() << "MirSurface::MirSurface() " << name;
4344 QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership);
@@ -133,7 +134,38 @@ void MirSurface::setOrientationAngle(Mir::OrientationAngle angle)
133134 Q_EMIT orientationAngleChanged(angle);
134135}
135136
137+Mir::ShellChrome MirSurface::shellChrome() const
138+{
139+ return m_shellChrome;
140+}
141+
142+void MirSurface::setShellChrome(Mir::ShellChrome shellChrome)
143+{
144+ if (shellChrome == m_shellChrome)
145+ return;
136146
147+ m_shellChrome = shellChrome;
148+ Q_EMIT shellChromeChanged(shellChrome);
149+}
150+
151+QString MirSurface::keymapLayout() const
152+{
153+ return m_keyMap.first;
154+}
155+
156+QString MirSurface::keymapVariant() const
157+{
158+ return m_keyMap.second;
159+}
160+
161+void MirSurface::setKeymap(const QString &layout, const QString &variant)
162+{
163+ if (layout.isEmpty()) {
164+ return;
165+ }
166+ m_keyMap = qMakePair(layout, variant);
167+ Q_EMIT keymapChanged(layout, variant);
168+}
137169
138170void MirSurface::registerView(qintptr viewId)
139171{
tests/mocks/Unity/Application/MirSurface.h+10−3
@@ -74,10 +74,16 @@ public:
7474 int widthIncrement() const override { return m_widthIncrement; }
7575 int heightIncrement() const override { return m_heightIncrement; }
7676
77+ Mir::ShellChrome shellChrome() const override;
78+ QString keymapLayout() const override;
79+ QString keymapVariant() const override;
80+ Q_INVOKABLE void setKeymap(const QString &layout, const QString &variant) override;
81+
7782 ////
7883 // API for tests
7984
8085 Q_INVOKABLE void setLive(bool live);
86+ Q_INVOKABLE void setShellChrome(Mir::ShellChrome shellChrome);
8187
8288 void registerView(qintptr viewId);
8389 void unregisterView(qintptr viewId);
@@ -109,9 +115,6 @@ public:
109115 void setActiveFocus(bool);
110116
111117Q_SIGNALS:
112− void stateChanged(Mir::State);
113− void liveChanged(bool live);
114− void orientationAngleChanged(Mir::OrientationAngle angle);
115118 void widthChanged();
116119 void heightChanged();
117120 void slowToResizeChanged();
@@ -152,10 +155,14 @@ private:
152155 QSize m_delayedResize;
153156 QSize m_pendingResize;
154157
158+ Mir::ShellChrome m_shellChrome;
159+
155160 struct View {
156161 bool visible;
157162 };
158163 QHash<qintptr, View> m_views;
164+
165+ QPair<QString,QString> m_keyMap; // pair of layout+variant
159166};
160167
161168#endif // MOCK_MIR_SURFACE_H
tests/mocks/Unity/Application/MirSurfaceItem.cpp+28−5
@@ -20,6 +20,7 @@
2020
2121#include <QGuiApplication>
2222#include <QQuickView>
23+#include <QQmlContext>
2324#include <QQmlProperty>
2425#include <QQmlEngine>
2526#include <QString>
@@ -100,6 +101,15 @@ bool MirSurfaceItem::live() const
100101 }
101102}
102103
104+Mir::ShellChrome MirSurfaceItem::shellChrome() const
105+{
106+ if (m_qmlSurface) {
107+ return m_qmlSurface->shellChrome();
108+ } else {
109+ return Mir::NormalChrome;
110+ }
111+}
112+
103113Mir::OrientationAngle MirSurfaceItem::orientationAngle() const
104114{
105115 if (m_qmlSurface) {
@@ -156,6 +166,10 @@ void MirSurfaceItem::createQmlContentItem()
156166
157167void MirSurfaceItem::touchEvent(QTouchEvent * event)
158168{
169+ if (event->type() == QEvent::TouchBegin) {
170+ m_touchTrail.clear();
171+ }
172+
159173 if (event->touchPointStates() & Qt::TouchPointPressed) {
160174 ++m_touchPressCount;
161175 Q_EMIT touchPressCountChanged(m_touchPressCount);
@@ -163,6 +177,19 @@ void MirSurfaceItem::touchEvent(QTouchEvent * event)
163177 ++m_touchReleaseCount;
164178 Q_EMIT touchReleaseCountChanged(m_touchReleaseCount);
165179 }
180+
181+ Q_FOREACH(QTouchEvent::TouchPoint touchPoint, event->touchPoints()) {
182+ QString id(touchPoint.id());
183+ QVariantList list = m_touchTrail[id].toList();
184+ list.append(QVariant::fromValue(touchPoint.pos()));
185+ if (list.count() > 100) list.pop_front();
186+ m_touchTrail[id] = list;
187+ }
188+
189+ if (m_qmlItem) {
190+ QQmlProperty touchTrail(m_qmlItem, "touchTrail");
191+ touchTrail.write(m_touchTrail);
192+ }
166193}
167194
168195void MirSurfaceItem::mousePressEvent(QMouseEvent * event)
@@ -220,10 +247,6 @@ void MirSurfaceItem::setSurface(MirSurfaceInterface* surface)
220247 connect(m_qmlSurface, &MirSurface::liveChanged, this, &MirSurfaceItem::liveChanged);
221248 connect(m_qmlSurface, &MirSurface::stateChanged, this, &MirSurfaceItem::surfaceStateChanged);
222249
223− // The assumptions I make here really should hold.
224− QQuickView *quickView =
225− qobject_cast<QQuickView*>(QGuiApplication::topLevelWindows()[0]);
226−
227250 QUrl qmlComponentFilePath;
228251 if (!m_qmlSurface->qmlFilePath().isEmpty()) {
229252 qmlComponentFilePath = m_qmlSurface->qmlFilePath();
@@ -231,7 +254,7 @@ void MirSurfaceItem::setSurface(MirSurfaceInterface* surface)
231254 qmlComponentFilePath = QUrl("qrc:///Unity/Application/MirSurfaceItem.qml");
232255 }
233256
234− m_qmlContentComponent = new QQmlComponent(quickView->engine(), qmlComponentFilePath);
257+ m_qmlContentComponent = new QQmlComponent(QQmlEngine::contextForObject(parent())->engine(), qmlComponentFilePath);
235258
236259 switch (m_qmlContentComponent->status()) {
237260 case QQmlComponent::Ready:
tests/mocks/Unity/Application/MirSurfaceItem.h+2
@@ -47,6 +47,7 @@ public:
4747 Mir::Type type() const override;
4848 QString name() const override;
4949 bool live() const override;
50+ Mir::ShellChrome shellChrome() const override;
5051
5152 Mir::State surfaceState() const override;
5253 void setSurfaceState(Mir::State) override {}
@@ -123,6 +124,7 @@ private:
123124 int m_touchReleaseCount;
124125 int m_mousePressCount;
125126 int m_mouseReleaseCount;
127+ QVariantMap m_touchTrail;
126128
127129 FillMode m_fillMode{Stretch};
128130
tests/mocks/Unity/Application/Session.cpp+26−1
@@ -34,6 +34,7 @@ Session::Session(const QString &name,
3434 , m_surface(nullptr)
3535 , m_parentSession(nullptr)
3636 , m_children(new SessionModel(this))
37+ , m_fullscreen(false)
3738{
3839// qDebug() << "Session::Session() " << this->name();
3940
@@ -71,6 +72,25 @@ void Session::release()
7172 deleteLater();
7273}
7374
75+void Session::updateFullscreenProperty()
76+{
77+ if (m_surfaces.rowCount() > 0) {
78+ // TODO: Figure out something better
79+ setFullscreen(lastSurface()->state() == Mir::FullscreenState);
80+ } else {
81+ // Keep the current value of the fullscreen property until we get a new
82+ // surface
83+ }
84+}
85+
86+void Session::setFullscreen(bool fullscreen)
87+{
88+ if (m_fullscreen != fullscreen) {
89+ m_fullscreen = fullscreen;
90+ Q_EMIT fullscreenChanged(m_fullscreen);
91+ }
92+}
93+
7494void Session::setApplication(ApplicationInfo* application)
7595{
7696 if (m_application == application)
@@ -82,15 +102,18 @@ void Session::setApplication(ApplicationInfo* application)
82102
83103void Session::appendSurface(MirSurface* surface)
84104{
85− // qDebug() << "Session::appendSurface - session=" << name() << "surface=" << surface;
105+ qDebug() << "Session::appendSurface - session=" << name() << "surface=" << surface;
86106
87107 m_surfaces.insert(m_surfaces.rowCount(), surface);
88108
109+ connect(surface, &MirSurfaceInterface::stateChanged, this, &Session::updateFullscreenProperty);
89110 connect(surface, &QObject::destroyed,
90111 this, [this, surface]() { this->removeSurface(surface); });
91112
92113 Q_EMIT lastSurfaceChanged(surface);
93114 Q_EMIT surfaceAdded(surface);
115+
116+ updateFullscreenProperty();
94117}
95118
96119void Session::removeSurface(MirSurface* surface)
@@ -99,6 +122,8 @@ void Session::removeSurface(MirSurface* surface)
99122 if (m_surfaces.contains(surface)) {
100123 m_surfaces.remove(surface);
101124 }
125+
126+ updateFullscreenProperty();
102127}
103128
104129void Session::setScreenshot(const QUrl& screenshot)
tests/mocks/Unity/Application/Session.h+7
@@ -47,6 +47,7 @@ public:
4747 //getters
4848 QString name() const { return m_name; }
4949 bool live() const { return m_live; }
50+ bool fullscreen() const { return m_fullscreen; }
5051 ApplicationInfo* application() const { return m_application; }
5152 MirSurface *lastSurface() const;
5253 ObjectListModel<MirSurface>* surfaces() const;
@@ -57,6 +58,7 @@ public:
5758 void removeSurface(MirSurface* surface);
5859 void setScreenshot(const QUrl& m_screenshot);
5960 void setLive(bool live);
61+ void setFullscreen(bool fullscreen);
6062
6163 Q_INVOKABLE void addChildSession(Session* session);
6264 void insertChildSession(uint index, Session* session);
@@ -68,6 +70,7 @@ Q_SIGNALS:
6870 void liveChanged(bool live);
6971 void surfaceAdded(MirSurface *surface);
7072 void lastSurfaceChanged(MirSurface *surface);
73+ void fullscreenChanged(bool fullscreen);
7174
7275 // internal mock use
7376 void deregister();
@@ -75,6 +78,9 @@ Q_SIGNALS:
7578public Q_SLOTS:
7679 Q_INVOKABLE void createSurface();
7780
81+private Q_SLOTS:
82+ void updateFullscreenProperty();
83+
7884private:
7985 SessionModel* childSessions() const;
8086 void setParentSession(Session* session);
@@ -87,6 +93,7 @@ private:
8793 Session* m_parentSession;
8894 SessionModel* m_children;
8995 ObjectListModel<MirSurface> m_surfaces;
96+ bool m_fullscreen;
9097
9198 friend class ApplicationTestInterface;
9299};
tests/mocks/Unity/Application/resources/MirSurfaceItem.qml+59
@@ -29,6 +29,9 @@ Rectangle {
2929 property alias screenshotSource: screenshotImage.source
3030 property int orientationAngle
3131
32+ property var touchTrail
33+ onTouchTrailChanged: canvas.requestPaint()
34+
3235 Image {
3336 id: screenshotImage
3437 anchors.fill: parent
@@ -67,4 +70,60 @@ Rectangle {
6770 width: (rotation == 0 || rotation == 180 ? parent.width : parent.height)
6871 height:(rotation == 0 || rotation == 180 ? parent.height : parent.width)
6972 }
73+
74+ Canvas {
75+ id: canvas
76+ anchors.fill: parent
77+ onPaint: {
78+ // get context to draw with
79+ var ctx = getContext("2d")
80+ ctx.reset(0, 0, canvas.width, canvas.height);
81+ ctx.lineWidth = 20;
82+
83+ if (touchTrail === undefined) return;
84+
85+ var i = 0;
86+ for (var prop in touchTrail) {
87+ var trail = touchTrail[prop];
88+
89+ if (trail.length > 0) {
90+ ctx.fillStyle = Qt.rgba(0, 0, 1, 0.3);
91+ ctx.strokeStyle = Qt.rgba(0, 0, 1, 0.3);
92+ ctx.beginPath();
93+ ctx.moveTo(trail[0].x, trail[0].y);
94+ ctx.ellipse(trail[0].x-1, trail[0].y-1, 2, 2);
95+ ctx.stroke();
96+
97+ ctx.beginPath();
98+ if (i == 0) {
99+ ctx.strokeStyle = Qt.rgba(1, 1, 0, 0.3);
100+ } else if (i == 1) {
101+ ctx.strokeStyle = Qt.rgba(0, 1, 1, 0.3);
102+
103+ } else if (i == 2) {
104+ ctx.strokeStyle = Qt.rgba(1, 0, 1, 0.3);
105+
106+ } else if (i == 3) {
107+ ctx.strokeStyle = Qt.rgba(0, 0.5, 0.5, 0.3);
108+
109+ } else {
110+ ctx.strokeStyle = Qt.rgba(0.7, 0.5, 0, 0.3);
111+ }
112+
113+ for (var i = 1; i < trail.length; i++) {
114+ ctx.lineTo(trail[i].x, trail[i].y)
115+ }
116+ ctx.stroke();
117+
118+ ctx.beginPath();
119+ ctx.fillStyle = Qt.rgba(1, 0, 0, 0.3);
120+ ctx.strokeStyle = Qt.rgba(1, 0, 0, 0.3);
121+ ctx.ellipse(trail[i-1].x-1, trail[i-1].y-1, 2, 2);
122+ ctx.stroke();
123+ }
124+ i++;
125+ }
126+
127+ }
128+ }
70129}
tests/mocks/Unity/CMakeLists.txt+13
@@ -29,15 +29,28 @@ set(UnityQML_SOURCES
2929 fake_resultsmodel.cpp
3030 fake_previewmodel.cpp
3131 fake_previewwidgetmodel.cpp
32+ fake_filters.cpp
33+ fake_optionselectorfilter.cpp
34+ fake_optionselectoroptions.cpp
35+ fake_rangeinputfilter.cpp
36+ fake_valuesliderfilter.cpp
37+ fake_valueslidervalues.cpp
3238 fake_unity_plugin.cpp
3339 ${SCOPES_API_INCLUDEDIR}/unity/shell/scopes/CategoriesInterface.h
40+ ${SCOPES_API_INCLUDEDIR}/unity/shell/scopes/FilterBaseInterface.h
41+ ${SCOPES_API_INCLUDEDIR}/unity/shell/scopes/FiltersInterface.h
3442 ${SCOPES_API_INCLUDEDIR}/unity/shell/scopes/NavigationInterface.h
43+ ${SCOPES_API_INCLUDEDIR}/unity/shell/scopes/OptionSelectorFilterInterface.h
44+ ${SCOPES_API_INCLUDEDIR}/unity/shell/scopes/OptionSelectorOptionsInterface.h
3545 ${SCOPES_API_INCLUDEDIR}/unity/shell/scopes/PreviewModelInterface.h
3646 ${SCOPES_API_INCLUDEDIR}/unity/shell/scopes/PreviewWidgetModelInterface.h
47+ ${SCOPES_API_INCLUDEDIR}/unity/shell/scopes/RangeInputFilterInterface.h
3748 ${SCOPES_API_INCLUDEDIR}/unity/shell/scopes/ResultsModelInterface.h
3849 ${SCOPES_API_INCLUDEDIR}/unity/shell/scopes/ScopeInterface.h
3950 ${SCOPES_API_INCLUDEDIR}/unity/shell/scopes/ScopesInterface.h
4051 ${SCOPES_API_INCLUDEDIR}/unity/shell/scopes/SettingsModelInterface.h
52+ ${SCOPES_API_INCLUDEDIR}/unity/shell/scopes/ValueSliderFilterInterface.h
53+ ${SCOPES_API_INCLUDEDIR}/unity/shell/scopes/ValueSliderValuesInterface.h
4154)
4255
4356# Workaround for gcc failure LP: #1417664
tests/mocks/Unity/Indicators/ModelActionRootState.qml+26−4
@@ -33,17 +33,39 @@ Item {
3333 property string submenuAction: {
3434 if (!menu) return "";
3535 var ext = menu.get(0, "ext");
36− var submenuVar = ext ? ext["submenu-action"] : undefined;
37− return submenuVar ? submenuVar : ""
36+ var action = ext ? ext["submenu-action"] : undefined;
37+ return action ? action : ""
3838 }
39+
40+ property string secondaryAction: {
41+ if (!menu) return "";
42+ var ext = menu.get(0, "ext");
43+ var action = ext ? ext["x-canonical-secondary-action"] : undefined;
44+ return action ? action : ""
45+ }
46+
47+ property string scrollAction: {
48+ if (!menu) return "";
49+ var ext = menu.get(0, "ext");
50+ var action = ext ? ext["x-canonical-scroll-action"] : undefined;
51+ return action ? action : ""
52+ }
53+
3954 Connections {
4055 target: menu
4156 onModelDataChanged: {
4257 cachedState = menu.get(0, "actionState");
4358
4459 var ext = menu.get(0, "ext");
45− var submenuVar = ext ? ext["submenu-action"] : undefined;
46− submenuAction = submenuVar ? submenuVar : ""
60+
61+ var action = ext ? ext["submenu-action"] : undefined;
62+ submenuAction = action ? action : ""
63+
64+ action = ext ? ext["x-canonical-secondary-action"] : undefined;
65+ secondaryAction = action ? action : ""
66+
67+ action = ext ? ext["x-canonical-scroll-action"] : undefined;
68+ scrollAction = action ? action : ""
4769 }
4870 }
4971
tests/mocks/Unity/fake_filters.cppnew file+125
@@ -0,0 +1,125 @@
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 "fake_filters.h"
18+
19+#include "fake_optionselectorfilter.h"
20+#include "fake_rangeinputfilter.h"
21+#include "fake_valuesliderfilter.h"
22+#include "fake_scope.h"
23+
24+Filters::Filters(Scope* parent)
25+ : unity::shell::scopes::FiltersInterface(parent)
26+{
27+ addFilter(new FakeOptionSelectorFilter("OSF1", "Tag1", "Which Cake you like More", false, QStringList() << "cheese" << "carrot" << "chocolate", this));
28+
29+ FakeRangeInputFilter *rif = new FakeRangeInputFilter("RIF1", "Tag3", this);
30+ rif->setTitle("How much do you want to walk?");
31+ rif->setCentralLabel("to");
32+ rif->setStartPostfixLabel("m");
33+ rif->setEndPostfixLabel("m");
34+ addFilter(rif);
35+
36+ QMap<double, QString> labels;
37+ labels[15] = "Default";
38+ labels[50] = "50";
39+ labels[100] = "100";
40+ FakeValueSliderFilter *vsf = new FakeValueSliderFilter("VS1", "Tag4", 15, 10, 150, labels, this);
41+ addFilter(vsf);
42+
43+ addFilter(new FakeOptionSelectorFilter("OSF2", "Tag2", "Which Countries have you been to?", true, QStringList() << "Germany" << "UK" << "New Zealand", this));
44+}
45+
46+void Filters::addFilter(unity::shell::scopes::FilterBaseInterface *f)
47+{
48+ switch (f->filterType()) {
49+ case FiltersInterface::OptionSelectorFilter: {
50+ FakeOptionSelectorFilter *osf = static_cast<FakeOptionSelectorFilter *>(f);
51+ connect(osf, &FakeOptionSelectorFilter::isActiveChanged, this, &Filters::activeFiltersCountChanged);
52+ }
53+ break;
54+
55+ case FiltersInterface::RangeInputFilter: {
56+ FakeRangeInputFilter *rif = static_cast<FakeRangeInputFilter *>(f);
57+ connect(rif, &FakeRangeInputFilter::isActiveChanged, this, &Filters::activeFiltersCountChanged);
58+ }
59+ break;
60+
61+ case FiltersInterface::ValueSliderFilter: {
62+ // Not counting value slider as active in the mock
63+ }
64+
65+ case Invalid:
66+ break;
67+ }
68+ m_filters << f;
69+}
70+
71+int Filters::rowCount(const QModelIndex &parent) const
72+{
73+ if (parent.isValid())
74+ return 0;
75+
76+ return m_filters.count();
77+}
78+
79+QVariant Filters::data(const QModelIndex &index, int role) const
80+{
81+ const int row = index.row();
82+ if (row < 0 || row >= m_filters.count())
83+ return QVariant();
84+
85+ unity::shell::scopes::FilterBaseInterface *filter = m_filters[row];
86+
87+ switch (role) {
88+ case RoleFilterId:
89+ return filter->filterId();
90+ case RoleFilterType:
91+ return filter->filterType();
92+ case RoleFilter:
93+ return QVariant::fromValue<unity::shell::scopes::FilterBaseInterface *>(filter);
94+ default:
95+ return QVariant();
96+ }
97+}
98+
99+int Filters::activeFiltersCount() const
100+{
101+ int active = 0;
102+ Q_FOREACH(unity::shell::scopes::FilterBaseInterface *f, m_filters) {
103+ switch (f->filterType()) {
104+ case FiltersInterface::OptionSelectorFilter: {
105+ FakeOptionSelectorFilter *osf = static_cast<FakeOptionSelectorFilter *>(f);
106+ if (osf->isActive()) ++active;
107+ }
108+ break;
109+
110+ case FiltersInterface::RangeInputFilter: {
111+ FakeRangeInputFilter *rif = static_cast<FakeRangeInputFilter *>(f);
112+ if (rif->isActive()) ++active;
113+ }
114+ break;
115+
116+ case FiltersInterface::ValueSliderFilter: {
117+ // Not counting value slider as active in the mock
118+ }
119+
120+ case Invalid:
121+ break;
122+ }
123+ }
124+ return active;
125+}
tests/mocks/Unity/fake_filters.hnew file+46
@@ -0,0 +1,46 @@
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 FAKE_FILTERS_H
18+#define FAKE_FILTERS_H
19+
20+#include <unity/shell/scopes/FiltersInterface.h>
21+#include <unity/shell/scopes/FilterBaseInterface.h>
22+
23+class Scope;
24+
25+class Filters : public unity::shell::scopes::FiltersInterface
26+{
27+ Q_OBJECT
28+
29+public:
30+ Filters(Scope* parent);
31+
32+ int rowCount(const QModelIndex &parent) const override;
33+ QVariant data(const QModelIndex &index, int role) const override;
34+
35+ int activeFiltersCount() const;
36+
37+Q_SIGNALS:
38+ void activeFiltersCountChanged();
39+
40+private:
41+ void addFilter(unity::shell::scopes::FilterBaseInterface* f);
42+
43+ QVector<unity::shell::scopes::FilterBaseInterface*> m_filters;
44+};
45+
46+#endif
tests/mocks/Unity/fake_navigation.cpp+9−10
@@ -32,7 +32,6 @@ Navigation::Navigation(const QString& navigationId, const QString& label, const
3232{
3333 QTimer::singleShot(1500, this, &Navigation::slotLoaded);
3434 connect(scope, &Scope::currentNavigationIdChanged, this, &Navigation::slotCurrentNavigationChanged);
35− connect(scope, &Scope::currentAltNavigationIdChanged, this, &Navigation::slotCurrentNavigationChanged);
3635}
3736
3837QString Navigation::navigationId() const
@@ -78,17 +77,17 @@ int Navigation::count() const
7877
7978bool Navigation::isRoot() const
8079{
81− return m_navigationId == "root" || m_navigationId == "altroot";
80+ return m_navigationId == "root";
8281}
8382
8483bool Navigation::hidden() const
8584{
86− return m_navigationId == "altroot";
85+ return false;
8786}
8887
8988int Navigation::rowCount(const QModelIndex & /*parent*/) const
9089{
91− if (!m_loaded || m_navigationId.startsWith("child") || m_navigationId.startsWith("altrootChild") || m_navigationId == "middle3")
90+ if (!m_loaded ||(m_navigationId.startsWith("child") && !m_navigationId.startsWith("childmiddle4")) || m_navigationId == "middle3")
9291 return 0;
9392 else
9493 return 8;
@@ -102,16 +101,16 @@ QVariant Navigation::data(const QModelIndex &index, int role) const
102101 return QString("middle%1").arg(index.row());
103102 else if (m_navigationId.startsWith("middle"))
104103 return QString("child%1%2").arg(m_navigationId).arg(index.row());
104+ else if (m_navigationId.startsWith("childmiddle"))
105+ return QString("grandchild%1%2").arg(m_navigationId).arg(index.row());
105106 case RoleLabel:
106107 return QString("%1Child%2").arg(m_navigationId).arg(index.row());
108+ case RoleAllLabel:
109+ return QString("all%1Child%2").arg(m_navigationId).arg(index.row());
107110 case RoleHasChildren:
108− return m_navigationId == "root" && index.row() != 3;
111+ return (m_navigationId == "root" && index.row() != 3) || (m_navigationId == "middle4");
109112 case RoleIsActive:
110− if (m_navigationId.startsWith("alt")) {
111− return m_scope->currentAltNavigationId() == data(index, RoleNavigationId);
112− } else {
113− return m_scope->currentNavigationId() == data(index, RoleNavigationId);
114− }
113+ return m_scope->currentNavigationId() == data(index, RoleNavigationId);
115114 }
116115 return QVariant();
117116}
tests/mocks/Unity/fake_optionselectorfilter.cppnew file+65
@@ -0,0 +1,65 @@
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 "fake_optionselectorfilter.h"
18+
19+#include "fake_optionselectoroptions.h"
20+
21+FakeOptionSelectorFilter::FakeOptionSelectorFilter(const QString &id, const QString &tag, const QString &label, bool multiselect, const QStringList &optionLabels, QObject* parent)
22+ : unity::shell::scopes::OptionSelectorFilterInterface(parent),
23+ m_filterId(id),
24+ m_filterTag(tag),
25+ m_label(label),
26+ m_multiSelect(multiselect)
27+{
28+ m_options = new FakeOptionSelectorOptions(optionLabels, this);
29+ connect(m_options, &FakeOptionSelectorOptions::anyCheckedChanged, this, &FakeOptionSelectorFilter::isActiveChanged);
30+}
31+
32+QString FakeOptionSelectorFilter::filterId() const
33+{
34+ return m_filterId;
35+}
36+
37+QString FakeOptionSelectorFilter::filterTag() const
38+{
39+ return m_filterTag;
40+}
41+
42+QString FakeOptionSelectorFilter::title() const
43+{
44+ return m_title;
45+}
46+
47+QString FakeOptionSelectorFilter::label() const
48+{
49+ return m_label;
50+}
51+
52+bool FakeOptionSelectorFilter::multiSelect() const
53+{
54+ return m_multiSelect;
55+}
56+
57+unity::shell::scopes::OptionSelectorOptionsInterface* FakeOptionSelectorFilter::options() const
58+{
59+ return m_options;
60+}
61+
62+bool FakeOptionSelectorFilter::isActive() const
63+{
64+ return m_options->anyChecked();
65+}
tests/mocks/Unity/fake_optionselectorfilter.hnew file+52
@@ -0,0 +1,52 @@
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 FAKE_OPTIONSELECTORFILTER_H
18+#define FAKE_OPTIONSELECTORFILTER_H
19+
20+#include <unity/shell/scopes/OptionSelectorFilterInterface.h>
21+
22+class FakeOptionSelectorOptions;
23+
24+class FakeOptionSelectorFilter : public unity::shell::scopes::OptionSelectorFilterInterface
25+{
26+ Q_OBJECT
27+
28+public:
29+ FakeOptionSelectorFilter(const QString &id, const QString &tag, const QString &label, bool multiselect, const QStringList &optionLabels, QObject* parent);
30+
31+ QString filterId() const override;
32+ QString filterTag() const override;
33+ QString title() const override;
34+ QString label() const override;
35+ bool multiSelect() const override;
36+ unity::shell::scopes::OptionSelectorOptionsInterface* options() const override;
37+
38+ bool isActive() const;
39+
40+Q_SIGNALS:
41+ void isActiveChanged();
42+
43+private:
44+ QString m_filterId;
45+ QString m_filterTag;
46+ QString m_title;
47+ QString m_label;
48+ bool m_multiSelect;
49+ FakeOptionSelectorOptions* m_options;
50+};
51+
52+#endif
tests/mocks/Unity/fake_optionselectoroptions.cppnew file+77
@@ -0,0 +1,77 @@
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 "fake_optionselectoroptions.h"
18+
19+#include "fake_optionselectorfilter.h"
20+
21+FakeOptionSelectorOptions::FakeOptionSelectorOptions(const QStringList &optionLabels, FakeOptionSelectorFilter* parent)
22+ : unity::shell::scopes::OptionSelectorOptionsInterface(parent),
23+ m_optionLabels(optionLabels)
24+{
25+}
26+
27+int FakeOptionSelectorOptions::rowCount(const QModelIndex &parent) const
28+{
29+ if (parent.isValid())
30+ return 0;
31+
32+ return m_optionLabels.count();
33+}
34+
35+QVariant FakeOptionSelectorOptions::data(const QModelIndex &index, int role) const
36+{
37+ const int row = index.row();
38+ if (row < 0 || row >= m_optionLabels.count())
39+ return QVariant();
40+
41+ switch (role) {
42+ case RoleOptionId: {
43+ const QString id = static_cast<FakeOptionSelectorFilter*>(parent())->filterId() + QString::number(row);
44+ return id;
45+ }
46+ case RoleOptionLabel:
47+ return m_optionLabels[row];
48+ case RoleOptionChecked:
49+ return m_checkedIndexes.contains(row);
50+ default:
51+ return QVariant();
52+ }
53+
54+}
55+
56+void FakeOptionSelectorOptions::setChecked(int row, bool checked)
57+{
58+ if (checked) {
59+ if (!static_cast<FakeOptionSelectorFilter*>(parent())->multiSelect()) {
60+ if (!m_checkedIndexes.isEmpty()) {
61+ setChecked(*m_checkedIndexes.begin(), false);
62+ }
63+ Q_ASSERT(m_checkedIndexes.isEmpty());
64+ }
65+ m_checkedIndexes << row;
66+ } else {
67+ m_checkedIndexes.remove(row);
68+ }
69+ const QModelIndex idx = index(row, 0);
70+ Q_EMIT dataChanged(idx, idx, QVector<int>() << RoleOptionChecked);
71+ Q_EMIT anyCheckedChanged();
72+}
73+
74+bool FakeOptionSelectorOptions::anyChecked() const
75+{
76+ return !m_checkedIndexes.isEmpty();
77+}
tests/mocks/Unity/fake_optionselectoroptions.hnew file+47
@@ -0,0 +1,47 @@
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 FAKE_OPTIONSELECTOROPTIONS_H
18+#define FAKE_OPTIONSELECTOROPTIONS_H
19+
20+#include <unity/shell/scopes/OptionSelectorFilterInterface.h>
21+
22+#include <QSet>
23+
24+class FakeOptionSelectorFilter;
25+
26+class FakeOptionSelectorOptions : public unity::shell::scopes::OptionSelectorOptionsInterface
27+{
28+ Q_OBJECT
29+
30+public:
31+ FakeOptionSelectorOptions(const QStringList &optionLabels, FakeOptionSelectorFilter* parent);
32+
33+ int rowCount(const QModelIndex &parent) const override;
34+ QVariant data(const QModelIndex &index, int role) const override;
35+ Q_INVOKABLE void setChecked(int index, bool checked) override;
36+
37+ bool anyChecked() const;
38+
39+Q_SIGNALS:
40+ void anyCheckedChanged();
41+
42+private:
43+ QStringList m_optionLabels;
44+ QSet<int> m_checkedIndexes;
45+};
46+
47+#endif
tests/mocks/Unity/fake_rangeinputfilter.cppnew file+167
@@ -0,0 +1,167 @@
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 "fake_rangeinputfilter.h"
18+
19+FakeRangeInputFilter::FakeRangeInputFilter(const QString &id, const QString &tag, QObject* parent)
20+ : unity::shell::scopes::RangeInputFilterInterface(parent),
21+ m_filterId(id),
22+ m_filterTag(tag),
23+ m_hasStartValue(false),
24+ m_hasEndValue(false),
25+ m_startValue(-1),
26+ m_endValue(-1)
27+{
28+ connect(this, &FakeRangeInputFilter::hasStartValueChanged, this, &FakeRangeInputFilter::isActiveChanged);
29+ connect(this, &FakeRangeInputFilter::hasEndValueChanged, this, &FakeRangeInputFilter::isActiveChanged);
30+}
31+
32+QString FakeRangeInputFilter::filterId() const
33+{
34+ return m_filterId;
35+}
36+
37+QString FakeRangeInputFilter::filterTag() const
38+{
39+ return m_filterTag;
40+}
41+
42+QString FakeRangeInputFilter::title() const
43+{
44+ return m_title;
45+}
46+
47+double FakeRangeInputFilter::startValue() const
48+{
49+ return m_startValue;
50+}
51+
52+double FakeRangeInputFilter::endValue() const
53+{
54+ return m_endValue;
55+}
56+
57+void FakeRangeInputFilter::setStartValue(double value)
58+{
59+ if (m_startValue != value) {
60+ m_startValue = value;
61+ Q_EMIT startValueChanged();
62+ }
63+ if (!m_hasStartValue) {
64+ m_hasStartValue = true;
65+ Q_EMIT hasStartValueChanged();
66+ }
67+}
68+
69+void FakeRangeInputFilter::setEndValue(double value)
70+{
71+ if (m_endValue != value) {
72+ m_endValue = value;
73+ Q_EMIT endValueChanged();
74+ }
75+ if (!m_hasEndValue) {
76+ m_hasEndValue = true;
77+ Q_EMIT hasEndValueChanged();
78+ }
79+}
80+
81+QString FakeRangeInputFilter::startPrefixLabel() const
82+{
83+ return m_startPrefixLabel;
84+}
85+
86+QString FakeRangeInputFilter::startPostfixLabel() const
87+{
88+ return m_startPostfixLabel;
89+}
90+
91+QString FakeRangeInputFilter::centralLabel() const
92+{
93+ return m_centralLabel;
94+}
95+
96+QString FakeRangeInputFilter::endPrefixLabel() const
97+{
98+ return m_endPrefixLabel;
99+}
100+
101+QString FakeRangeInputFilter::endPostfixLabel() const
102+{
103+ return m_endPostfixLabel;
104+}
105+
106+bool FakeRangeInputFilter::hasStartValue() const
107+{
108+ return m_hasStartValue;
109+}
110+
111+bool FakeRangeInputFilter::hasEndValue() const
112+{
113+ return m_hasEndValue;
114+}
115+
116+void FakeRangeInputFilter::eraseStartValue()
117+{
118+ m_hasStartValue = false;
119+ Q_EMIT hasStartValueChanged();
120+}
121+
122+void FakeRangeInputFilter::eraseEndValue()
123+{
124+ m_hasEndValue = false;
125+ Q_EMIT hasEndValueChanged();
126+}
127+
128+bool FakeRangeInputFilter::isActive() const
129+{
130+ return hasStartValue() && hasEndValue();
131+}
132+
133+void FakeRangeInputFilter::setTitle(const QString &title)
134+{
135+ m_title = title;
136+ Q_EMIT titleChanged();
137+}
138+
139+void FakeRangeInputFilter::setStartPrefixLabel(const QString &startPrefixLabel)
140+{
141+ m_startPrefixLabel = startPrefixLabel;
142+ Q_EMIT startPrefixLabelChanged();
143+}
144+
145+void FakeRangeInputFilter::setStartPostfixLabel(const QString &startPostfixLabel)
146+{
147+ m_startPostfixLabel = startPostfixLabel;
148+ Q_EMIT startPostfixLabelChanged();
149+}
150+
151+void FakeRangeInputFilter::setCentralLabel(const QString &centralLabel)
152+{
153+ m_centralLabel = centralLabel;
154+ Q_EMIT centralLabelChanged();
155+}
156+
157+void FakeRangeInputFilter::setEndPrefixLabel(const QString &endPrefixLabel)
158+{
159+ m_endPrefixLabel = endPrefixLabel;
160+ Q_EMIT endPrefixLabelChanged();
161+}
162+
163+void FakeRangeInputFilter::setEndPostfixLabel(const QString &endPostfixLabel)
164+{
165+ m_endPostfixLabel = endPostfixLabel;
166+ Q_EMIT endPostfixLabelChanged();
167+}
tests/mocks/Unity/fake_rangeinputfilter.hnew file+79
@@ -0,0 +1,79 @@
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 FAKE_RANGEINPUTFILTER_H
18+#define FAKE_RANGEINPUTFILTER_H
19+
20+#include <unity/shell/scopes/RangeInputFilterInterface.h>
21+
22+class FakeRangeInputFilter : public unity::shell::scopes::RangeInputFilterInterface
23+{
24+ Q_OBJECT
25+
26+public:
27+ FakeRangeInputFilter(const QString &id, const QString &tag, QObject* parent);
28+
29+ QString filterId() const override;
30+ QString filterTag() const override;
31+ QString title() const override;
32+
33+ double startValue() const override;
34+ double endValue() const override;
35+ void setStartValue(double value) override;
36+ void setEndValue(double value) override;
37+ QString startPrefixLabel() const override;
38+ QString startPostfixLabel() const override;
39+ QString centralLabel() const override;
40+ QString endPrefixLabel() const override;
41+ QString endPostfixLabel() const override;
42+ bool hasStartValue() const override;
43+ bool hasEndValue() const override;
44+
45+ void eraseStartValue() override;
46+ void eraseEndValue() override;
47+
48+ // Not part of the iface, for mock/testing purposes
49+ bool isActive() const;
50+
51+ void setTitle(const QString &title);
52+ void setStartPrefixLabel(const QString &startPrefixLabel);
53+ void setStartPostfixLabel(const QString &startPostfixLabel);
54+ void setCentralLabel(const QString &centralLabel);
55+ void setEndPrefixLabel(const QString &endPrefixLabel);
56+ void setEndPostfixLabel(const QString &endPostfixLabel);
57+
58+Q_SIGNALS:
59+ void isActiveChanged();
60+
61+private:
62+ QString m_filterId;
63+ QString m_filterTag;
64+ QString m_title;
65+
66+ bool m_hasStartValue;
67+ bool m_hasEndValue;
68+
69+ double m_startValue;
70+ double m_endValue;
71+
72+ QString m_startPrefixLabel;
73+ QString m_startPostfixLabel;
74+ QString m_centralLabel;
75+ QString m_endPrefixLabel;
76+ QString m_endPostfixLabel;
77+};
78+
79+#endif
tests/mocks/Unity/fake_scope.cpp+52−27
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013, 2014 Canonical, Ltd.
2+ * Copyright (C) 2013, 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
@@ -19,7 +19,9 @@
1919
2020#include "fake_scope.h"
2121
22+#include "fake_filters.h"
2223#include "fake_navigation.h"
24+#include "fake_optionselectorfilter.h"
2325#include "fake_resultsmodel.h"
2426#include "fake_scopes.h"
2527#include "fake_settingsmodel.h"
@@ -37,14 +39,18 @@ Scope::Scope(QString const& id, QString const& name, bool favorite, Scopes* pare
3739 , m_searching(false)
3840 , m_favorite(favorite)
3941 , m_isActive(false)
42+ , m_hasNavigation(true)
43+ , m_hasPrimaryFilter(true)
4044 , m_currentNavigationId("root")
41− , m_currentAltNavigationId("altrootChild1")
4245 , m_previewRendererName("preview-generic")
4346 , m_categories(new Categories(categories, this))
4447 , m_openScope(nullptr)
4548 , m_settings(new SettingsModel(this))
49+ , m_filters(new Filters(this))
4650 , m_returnNullPreview(returnNullPreview)
4751{
52+ m_primaryNavigationFilter = new FakeOptionSelectorFilter("OSF3", "PFTag", "Which food you like More", false, QStringList() << "meat" << "vegetables", this);
53+ connect(m_filters, &Filters::activeFiltersCountChanged, this, &Scope::activeFiltersCountChanged);
4854}
4955
5056QString Scope::id() const
@@ -229,17 +235,13 @@ QString Scope::currentNavigationId() const
229235
230236bool Scope::hasNavigation() const
231237{
232− return true;
238+ return m_hasNavigation;
233239}
234240
235−QString Scope::currentAltNavigationId() const
241+void Scope::setHasNavigation(bool hasNavigation)
236242{
237− return m_currentAltNavigationId;
238−}
239−
240−bool Scope::hasAltNavigation() const
241−{
242− return true;
243+ m_hasNavigation = hasNavigation;
244+ Q_EMIT hasNavigationChanged();
243245}
244246
245247Scope::Status Scope::status() const
@@ -276,6 +278,11 @@ void Scope::refresh()
276278 Q_EMIT refreshed();
277279}
278280
281+void Scope::resetFilters()
282+{
283+ qWarning() << "Scope::resetFilters is unimplemented";
284+}
285+
279286void Scope::activateAction(QVariant const& /*result*/, QString const& /*categoryId*/, QString const& /*actionId*/)
280287{
281288 qFatal("Using Scope::activateAction");
@@ -298,31 +305,49 @@ unity::shell::scopes::NavigationInterface* Scope::getNavigation(const QString& i
298305 return new Navigation(id, id, "all"+id, parentId, parentLabel, this);
299306}
300307
301−unity::shell::scopes::NavigationInterface* Scope::getAltNavigation(QString const& id)
308+void Scope::setNavigationState(const QString &navigationId)
302309{
303− if (id.isEmpty())
304− return nullptr;
310+ m_currentNavigationId = navigationId;
311+ Q_EMIT currentNavigationIdChanged();
312+ Q_EMIT primaryNavigationTagChanged();
313+}
305314
306− QString parentId;
307− QString parentLabel;
308− if (id != "altroot") {
309− parentId = "altroot";
310− parentLabel = "altroot";
311− }
312− return new Navigation(id, id, "all"+id, parentId, parentLabel, this);
315+unity::shell::scopes::FilterBaseInterface* Scope::primaryNavigationFilter() const
316+{
317+ return m_hasPrimaryFilter ? m_primaryNavigationFilter : nullptr;
313318}
314319
315−void Scope::setNavigationState(const QString &navigationId, bool isAltNavigation)
320+unity::shell::scopes::FiltersInterface* Scope::filters() const
316321{
317− if (isAltNavigation) {
318− m_currentAltNavigationId = navigationId;
319− Q_EMIT currentAltNavigationIdChanged();
320− } else {
321− m_currentNavigationId = navigationId;
322− Q_EMIT currentNavigationIdChanged();
322+ return m_filters;
323+}
324+
325+QString Scope::primaryNavigationTag() const
326+{
327+ if (m_currentNavigationId == "root")
328+ return QString();
329+ else
330+ return const_cast<Scope*>(this)->getNavigation(m_currentNavigationId)->label();
331+}
332+
333+int Scope::activeFiltersCount() const
334+{
335+ return m_filters->activeFiltersCount();
336+}
337+
338+void Scope::resetPrimaryNavigationTag()
339+{
340+ if (m_currentNavigationId != "root") {
341+ setNavigationState("root");
323342 }
324343}
325344
345+void Scope::setHasPrimaryFilter(bool hasPrimaryFilter)
346+{
347+ m_hasPrimaryFilter = hasPrimaryFilter;
348+ Q_EMIT primaryNavigationFilterChanged();
349+}
350+
326351void Scope::performQuery(const QString& query)
327352{
328353 Q_EMIT queryPerformed(query);
tests/mocks/Unity/fake_scope.h+20−7
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013, 2014 Canonical, Ltd.
2+ * Copyright (C) 2013, 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
@@ -24,7 +24,9 @@
2424
2525#include <QTimer>
2626
27+class Filters;
2728class Scopes;
29+class FakeOptionSelectorFilter;
2830
2931class Scope : public unity::shell::scopes::ScopeInterface
3032{
@@ -68,11 +70,17 @@ public:
6870
6971 QString currentNavigationId() const override;
7072 bool hasNavigation() const override;
71− QString currentAltNavigationId() const override;
72− bool hasAltNavigation() const override;
7373 Q_INVOKABLE unity::shell::scopes::NavigationInterface* getNavigation(QString const& navigationId) override;
74− Q_INVOKABLE unity::shell::scopes::NavigationInterface* getAltNavigation(QString const& altNavigationId) override;
75− Q_INVOKABLE void setNavigationState(const QString &navigationId, bool isAltNavigation) override;
74+ Q_INVOKABLE void setNavigationState(const QString &navigationId) override;
75+ Q_INVOKABLE void setHasNavigation(bool hasNavigation); // This is not invokable in the Interface, here for testing benefits
76+
77+ unity::shell::scopes::FilterBaseInterface* primaryNavigationFilter() const override;
78+ unity::shell::scopes::FiltersInterface* filters() const override;
79+ QString primaryNavigationTag() const override;
80+ int activeFiltersCount() const override;
81+ Q_INVOKABLE void resetPrimaryNavigationTag() override;
82+ Q_INVOKABLE void setHasPrimaryFilter(bool hasPrimaryFilter); // This is not invokable in the Interface, here for testing benefits
83+
7684 void performQuery(const QString& query) override;
7785
7886 Status status() const override;
@@ -80,7 +88,9 @@ public:
8088
8189 Q_INVOKABLE void refresh() override;
8290
83− Q_INVOKABLE virtual void activateAction(QVariant const& result, QString const& categoryId, QString const& actionId) override;
91+ Q_INVOKABLE void resetFilters() override;
92+
93+ Q_INVOKABLE void activateAction(QVariant const& result, QString const& categoryId, QString const& actionId) override;
8494
8595Q_SIGNALS:
8696 // These are not in the Interface, here for testing benefits
@@ -99,14 +109,17 @@ protected:
99109 bool m_searching;
100110 bool m_favorite;
101111 bool m_isActive;
112+ bool m_hasNavigation;
113+ bool m_hasPrimaryFilter;
102114 QString m_currentNavigationId;
103− QString m_currentAltNavigationId;
104115
105116 QString m_previewRendererName;
106117
107118 unity::shell::scopes::CategoriesInterface* m_categories;
108119 unity::shell::scopes::ScopeInterface* m_openScope;
109120 unity::shell::scopes::SettingsModelInterface* m_settings;
121+ Filters* m_filters;
122+ FakeOptionSelectorFilter* m_primaryNavigationFilter;
110123
111124 bool m_returnNullPreview;
112125};
tests/mocks/Unity/fake_unity_plugin.cpp+7
@@ -22,11 +22,14 @@
2222// local
2323#include "fake_scopes.h"
2424#include "fake_categories.h"
25+#include "fake_filters.h"
2526#include "fake_navigation.h"
27+#include "fake_optionselectoroptions.h"
2628#include "fake_previewmodel.h"
2729#include "fake_previewwidgetmodel.h"
2830#include "fake_resultsmodel.h"
2931#include "fake_settingsmodel.h"
32+#include "fake_valueslidervalues.h"
3033
3134// External
3235#include <glib-object.h>
@@ -52,4 +55,8 @@ void FakeUnityPlugin::registerTypes(const char *uri)
5255 qmlRegisterType<ResultsModel>(uri, 0, 2, "FakeResultsModel");
5356 qmlRegisterType<PreviewModel>(uri, 0, 2, "FakePreviewModel");
5457 qmlRegisterUncreatableType<PreviewWidgetModel>(uri, 0, 2, "PreviewWidgetModel", "Can't create new PreviewWidgetModel in QML. Get them from PreviewModel instance.");
58+ qmlRegisterUncreatableType<unity::shell::scopes::FiltersInterface>(uri, 0, 2, "Filters", "Can't create Filters object in QML. Get them from Scope instance.");
59+ qmlRegisterUncreatableType<unity::shell::scopes::FilterBaseInterface>(uri, 0, 2, "Filter", "Can't create Filter object in QML. Get them from Scope instance.");
60+ qmlRegisterUncreatableType<unity::shell::scopes::OptionSelectorOptionsInterface>(uri, 0, 2, "OptionSelectorOptions", "Can't create OptionSelectorOptions object in QML");
61+ qmlRegisterUncreatableType<unity::shell::scopes::ValueSliderValuesInterface>(uri, 0, 2, "ValueSliderValues", "Can't create ValueSliderValuesInterface object in QML");
5562}
tests/mocks/Unity/fake_valuesliderfilter.cppnew file+84
@@ -0,0 +1,84 @@
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 "fake_valuesliderfilter.h"
18+#include "fake_valueslidervalues.h"
19+
20+FakeValueSliderFilter::FakeValueSliderFilter(const QString &id, const QString &tag, double value, double minValue, double maxValue, const QMap<double, QString> &labels, QObject* parent)
21+ : unity::shell::scopes::ValueSliderFilterInterface(parent),
22+ m_filterId(id),
23+ m_filterTag(tag),
24+ m_value(value),
25+ m_minValue(minValue),
26+ m_maxValue(maxValue),
27+ m_values(new FakeValueSliderValues(labels, this))
28+{
29+}
30+
31+QString FakeValueSliderFilter::filterId() const
32+{
33+ return m_filterId;
34+}
35+
36+QString FakeValueSliderFilter::filterTag() const
37+{
38+ return m_filterTag;
39+}
40+
41+QString FakeValueSliderFilter::title() const
42+{
43+ return m_title;
44+}
45+
46+double FakeValueSliderFilter::value() const
47+{
48+ return m_value;
49+}
50+
51+void FakeValueSliderFilter::setValue(double value)
52+{
53+ if (value != m_value) {
54+ m_value = value;
55+ Q_EMIT valueChanged();
56+ }
57+}
58+
59+double FakeValueSliderFilter::minValue() const
60+{
61+ return m_minValue;
62+}
63+
64+double FakeValueSliderFilter::maxValue() const
65+{
66+ return m_maxValue;
67+}
68+
69+unity::shell::scopes::ValueSliderValuesInterface* FakeValueSliderFilter::values() const
70+{
71+ return m_values;
72+}
73+
74+bool FakeValueSliderFilter::isActive() const
75+{
76+ // Doesn't really matter
77+ return false;
78+}
79+
80+void FakeValueSliderFilter::setTitle(const QString &title)
81+{
82+ m_title = title;
83+ Q_EMIT titleChanged();
84+}
tests/mocks/Unity/fake_valuesliderfilter.hnew file+55
@@ -0,0 +1,55 @@
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 FAKE_VALUESLIDERFILTER_H
18+#define FAKE_VALUESLIDERFILTER_H
19+
20+#include <unity/shell/scopes/ValueSliderFilterInterface.h>
21+
22+class FakeValueSliderFilter : public unity::shell::scopes::ValueSliderFilterInterface
23+{
24+ Q_OBJECT
25+
26+public:
27+ FakeValueSliderFilter(const QString &id, const QString &tag, double value, double minValue, double maxValue, const QMap<double, QString> &labels, QObject* parent);
28+
29+ QString filterId() const override;
30+ QString filterTag() const override;
31+ QString title() const override;
32+
33+ double value() const override;
34+ void setValue(double value) override;
35+ double minValue() const override;
36+ double maxValue() const override;
37+ unity::shell::scopes::ValueSliderValuesInterface* values() const override;
38+
39+ // Not part of the iface, for mock/testing purposes
40+ bool isActive() const;
41+
42+ void setTitle(const QString &title);
43+
44+private:
45+ QString m_filterId;
46+ QString m_filterTag;
47+ QString m_title;
48+
49+ double m_value;
50+ double m_minValue;
51+ double m_maxValue;
52+ unity::shell::scopes::ValueSliderValuesInterface *m_values;
53+};
54+
55+#endif
tests/mocks/Unity/fake_valueslidervalues.cppnew file+51
@@ -0,0 +1,51 @@
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 "fake_valueslidervalues.h"
18+
19+FakeValueSliderValues::FakeValueSliderValues(const QMap<double, QString> &labels, QObject* parent)
20+ : unity::shell::scopes::ValueSliderValuesInterface(parent)
21+ , m_labels(labels)
22+{
23+}
24+
25+int FakeValueSliderValues::rowCount(const QModelIndex &parent) const
26+{
27+ if (parent.isValid())
28+ return 0;
29+
30+ return m_labels.count();
31+}
32+
33+QVariant FakeValueSliderValues::data(const QModelIndex &index, int role) const
34+{
35+ if (!index.isValid())
36+ return QVariant();
37+
38+ const int row = index.row();
39+ auto it = m_labels.begin();
40+ it += row;
41+
42+ switch (role) {
43+ case RoleValue:
44+ return it.key();
45+
46+ case RoleLabel:
47+ return it.value();
48+ }
49+
50+ return QVariant();
51+}
tests/mocks/Unity/fake_valueslidervalues.hnew file+36
@@ -0,0 +1,36 @@
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 FAKE_VALUESLIDERVALUES_H
18+#define FAKE_VALUESLIDERVALUES_H
19+
20+#include <unity/shell/scopes/ValueSliderValuesInterface.h>
21+
22+class FakeValueSliderValues : public unity::shell::scopes::ValueSliderValuesInterface
23+{
24+ Q_OBJECT
25+
26+public:
27+ FakeValueSliderValues(const QMap<double, QString> &labels, QObject* parent);
28+
29+ int rowCount(const QModelIndex &parent) const override;
30+ QVariant data(const QModelIndex &index, int role) const override;
31+
32+private:
33+ const QMap<double, QString> m_labels;
34+};
35+
36+#endif
tests/mocks/Utils/CMakeLists.txt+3−2
@@ -12,18 +12,19 @@ set(QMLPLUGIN_SRC
1212 ${CMAKE_SOURCE_DIR}/plugins/Utils/qlimitproxymodelqml.cpp
1313 ${CMAKE_SOURCE_DIR}/plugins/Utils/unitysortfilterproxymodelqml.cpp
1414 ${CMAKE_SOURCE_DIR}/plugins/Utils/unitymenumodelpaths.cpp
15− ${CMAKE_SOURCE_DIR}/plugins/Utils/windowkeysfilter.cpp
15+ ${CMAKE_SOURCE_DIR}/plugins/Utils/windowinputfilter.cpp
1616 ${CMAKE_SOURCE_DIR}/plugins/Utils/windowscreenshotprovider.cpp
1717 ${CMAKE_SOURCE_DIR}/plugins/Utils/easingcurve.cpp
1818 ${CMAKE_SOURCE_DIR}/plugins/Utils/inputwatcher.cpp
19+ ${CMAKE_SOURCE_DIR}/plugins/Utils/timezoneFormatter.cpp
1920 ${CMAKE_SOURCE_DIR}/plugins/Utils/applicationsfiltermodel.cpp
2021 ${CMAKE_SOURCE_DIR}/plugins/Utils/inputeventgenerator.cpp
2122 ${CMAKE_SOURCE_DIR}/plugins/Utils/deviceconfigparser.cpp
23+ ${CMAKE_SOURCE_DIR}/plugins/Utils/globalfunctions.cpp
2224 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationManagerInterface.h
2325 constants.cpp
2426 plugin.cpp
2527 windowstatestorage.cpp
26− plugin.cpp
2728 )
2829
2930add_library(FakeUtils-qml SHARED
tests/mocks/Utils/Utils.qmltypes+2−2
@@ -145,10 +145,10 @@ Module {
145145 }
146146 }
147147 Component {
148− name: "WindowKeysFilter"
148+ name: "WindowInputFilter"
149149 defaultProperty: "data"
150150 prototype: "QQuickItem"
151− exports: ["Utils/WindowKeysFilter 0.1"]
151+ exports: ["Utils/WindowInputFilter 0.1"]
152152 exportMetaObjectRevisions: [0]
153153 }
154154 Component {
tests/mocks/Utils/plugin.cpp+14−2
@@ -32,12 +32,14 @@
3232#include <qlimitproxymodelqml.h>
3333#include <unitysortfilterproxymodelqml.h>
3434#include <unitymenumodelpaths.h>
35−#include <windowkeysfilter.h>
35+#include <windowinputfilter.h>
3636#include <windowscreenshotprovider.h>
3737#include <easingcurve.h>
38+#include <timezoneFormatter.h>
3839#include <applicationsfiltermodel.h>
3940#include <inputeventgenerator.h>
4041#include <deviceconfigparser.h>
42+#include <globalfunctions.h>
4143
4244static QObject *createWindowStateStorage(QQmlEngine *engine, QJSEngine *scriptEngine)
4345{
@@ -53,6 +55,13 @@ static QObject *createConstants(QQmlEngine *engine, QJSEngine *scriptEngine)
5355 return new Constants();
5456}
5557
58+static QObject *createGlobalFunctions(QQmlEngine *engine, QJSEngine *scriptEngine)
59+{
60+ Q_UNUSED(engine)
61+ Q_UNUSED(scriptEngine)
62+ return new GlobalFunctions();
63+}
64+
5665void FakeUtilsPlugin::registerTypes(const char *uri)
5766{
5867 Q_ASSERT(uri == QLatin1String("Utils"));
@@ -60,15 +69,18 @@ void FakeUtilsPlugin::registerTypes(const char *uri)
6069 qmlRegisterType<QLimitProxyModelQML>(uri, 0, 1, "LimitProxyModel");
6170 qmlRegisterType<UnitySortFilterProxyModelQML>(uri, 0, 1, "UnitySortFilterProxyModel");
6271 qmlRegisterType<UnityMenuModelPaths>(uri, 0, 1, "UnityMenuModelPaths");
63− qmlRegisterType<WindowKeysFilter>(uri, 0, 1, "WindowKeysFilter");
72+ qmlRegisterType<WindowInputFilter>(uri, 0, 1, "WindowInputFilter");
6473 qmlRegisterType<EasingCurve>(uri, 0, 1, "EasingCurve");
6574 qmlRegisterSingletonType<WindowStateStorage>(uri, 0, 1, "WindowStateStorage", createWindowStateStorage);
6675 qmlRegisterType<InputWatcher>(uri, 0, 1, "InputWatcher");
6776 qmlRegisterSingletonType<Constants>(uri, 0, 1, "Constants", createConstants);
77+ qmlRegisterSingletonType<TimezoneFormatter>(uri, 0, 1, "TimezoneFormatter",
78+ [](QQmlEngine*, QJSEngine*) -> QObject* { return new TimezoneFormatter; });
6879 qmlRegisterType<ActiveFocusLogger>(uri, 0, 1, "ActiveFocusLogger");
6980 qmlRegisterType<ApplicationsFilterModel>(uri, 0, 1, "ApplicationsFilterModel");
7081 qmlRegisterType<InputEventGenerator>(uri, 0, 1, "InputEventGenerator");
7182 qmlRegisterType<DeviceConfigParser>(uri, 0, 1, "DeviceConfigParser");
83+ qmlRegisterSingletonType<GlobalFunctions>(uri, 0, 1, "Functions", createGlobalFunctions);
7284}
7385
7486void FakeUtilsPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
tests/mocks/Utils/windowstatestorage.cpp+13
@@ -16,6 +16,8 @@
1616
1717#include "windowstatestorage.h"
1818
19+#include <unity/shell/application/ApplicationInfoInterface.h>
20+
1921WindowStateStorage::WindowStateStorage(QObject *parent):
2022 QObject(parent)
2123{
@@ -45,10 +47,21 @@ QRect WindowStateStorage::getGeometry(const QString &windowId, const QRect &defa
4547 return m_geometry.value(windowId).toRect();
4648}
4749
50+void WindowStateStorage::saveStage(const QString &appId, int stage)
51+{
52+ m_stage[appId] = stage;
53+}
54+
55+int WindowStateStorage::getStage(const QString &appId) const
56+{
57+ return m_stage.value(appId, unity::shell::application::ApplicationInfoInterface::MainStage);
58+}
59+
4860void WindowStateStorage::clear()
4961{
5062 m_state.clear();
5163 m_geometry.clear();
64+ m_stage.clear();
5265}
5366
5467void WindowStateStorage::saveState(const QString &windowId, WindowState state)
tests/mocks/Utils/windowstatestorage.h+4
@@ -36,6 +36,9 @@ public:
3636 Q_INVOKABLE void saveGeometry(const QString &windowId, const QRect &rect);
3737 Q_INVOKABLE QRect getGeometry(const QString &windowId, const QRect &defaultValue);
3838
39+ Q_INVOKABLE void saveStage(const QString &appId, int stage);
40+ Q_INVOKABLE int getStage(const QString &appId) const;
41+
3942 // Only in the mock, to easily restore a fresh state
4043 Q_INVOKABLE void clear();
4144
@@ -47,5 +50,6 @@ private:
4750 QVariantMap geometry() const;
4851
4952 QHash<QString, WindowState> m_state;
53+ QHash<QString, int> m_stage;
5054 QVariantMap m_geometry;
5155};
tests/mocks/Wizard/CMakeLists.txt+6−1
@@ -1,4 +1,5 @@
11include_directories(
2+ ${GLIB_INCLUDE_DIRS} ${GEONAMES_INCLUDE_DIRS}
23 ${CMAKE_SOURCE_DIR}/plugins/Wizard
34)
45
@@ -6,7 +7,11 @@ add_library(MockWizard-qml MODULE
67 mockplugin.cpp
78 MockSystem.cpp
89 ${CMAKE_SOURCE_DIR}/plugins/Wizard/PageList.cpp
10+ ${CMAKE_SOURCE_DIR}/plugins/Wizard/timezonemodel.cpp
11+ ${CMAKE_SOURCE_DIR}/plugins/Wizard/LocalePlugin.cpp
12+ ${CMAKE_SOURCE_DIR}/plugins/Wizard/Status.cpp
913)
1014
11−qt5_use_modules(MockWizard-qml Qml)
15+qt5_use_modules(MockWizard-qml DBus Qml Concurrent)
16+target_link_libraries(MockWizard-qml ${GLIB_LDFLAGS} ${GEONAMES_LDFLAGS})
1217add_unity8_mock(Wizard 0.1 Wizard TARGETS MockWizard-qml)
tests/mocks/Wizard/MockSystem.cpp+4−2
@@ -14,6 +14,8 @@
1414 * with this program. If not, see <http://www.gnu.org/licenses/>.
1515 */
1616
17+#include <QDebug>
18+
1719#include "MockSystem.h"
1820
1921MockSystem::MockSystem()
@@ -33,7 +35,7 @@ void MockSystem::setWizardEnabled(bool enabled)
3335 Q_EMIT wizardEnabledChanged();
3436}
3537
36−void MockSystem::updateSessionLanguage(const QString &locale)
38+void MockSystem::updateSessionLocale(const QString &locale)
3739{
38− Q_EMIT updateSessionLanguageCalled(locale);
40+ Q_EMIT updateSessionLocaleCalled(locale);
3941}
tests/mocks/Wizard/MockSystem.h+2−2
@@ -32,11 +32,11 @@ public:
3232 void setWizardEnabled(bool enabled);
3333
3434public Q_SLOTS:
35− void updateSessionLanguage(const QString &locale);
35+ void updateSessionLocale(const QString &locale);
3636
3737Q_SIGNALS:
3838 void wizardEnabledChanged();
39− void updateSessionLanguageCalled(const QString &locale); // only in mock
39+ void updateSessionLocaleCalled(const QString &locale); // only in mock
4040
4141private:
4242 Q_DISABLE_COPY(MockSystem)
tests/mocks/Wizard/mockplugin.cpp+7−8
@@ -17,19 +17,18 @@
1717#include "mockplugin.h"
1818#include "MockSystem.h"
1919#include "PageList.h"
20+#include "timezonemodel.h"
21+#include "LocalePlugin.h"
22+#include "Status.h"
2023
2124#include <QtQml/qqml.h>
2225
23−static QObject *system_provider(QQmlEngine *engine, QJSEngine *scriptEngine)
24−{
25− Q_UNUSED(engine)
26− Q_UNUSED(scriptEngine)
27− return new MockSystem();
28−}
29−
3026void MockWizardPlugin::registerTypes(const char *uri)
3127{
3228 Q_ASSERT(uri == QLatin1String("Wizard"));
3329 qmlRegisterType<PageList>(uri, 0, 1, "PageList");
34− qmlRegisterSingletonType<MockSystem>(uri, 0, 1, "System", system_provider);
30+ qmlRegisterSingletonType<MockSystem>(uri, 0, 1, "System", [](QQmlEngine*, QJSEngine*) -> QObject* { return new MockSystem; });
31+ qmlRegisterSingletonType<Status>(uri, 0, 1, "Status", [](QQmlEngine*, QJSEngine*) -> QObject* { return new Status; });
32+ qmlRegisterType<TimeZoneLocationModel>(uri, 0, 1, "TimeZoneModel");
33+ qmlRegisterType<LocalePlugin>(uri, 0, 1, "LocalePlugin");
3534}
tests/plugins/AccountsService/PropertiesServer.cpp+10
@@ -25,10 +25,16 @@
2525#include <QDBusMessage>
2626#include <QDBusMetaType>
2727
28+using StringMap = QMap<QString,QString>;
29+using StringMapList = QList<StringMap>;
30+Q_DECLARE_METATYPE(StringMapList)
31+
2832PropertiesServer::PropertiesServer(QObject *parent)
2933 : QObject(parent)
3034{
3135 qDBusRegisterMetaType<QList<QVariantMap>>();
36+ qDBusRegisterMetaType<StringMap>();
37+ qDBusRegisterMetaType<StringMapList>();
3238 Reset();
3339}
3440
@@ -63,6 +69,8 @@ void PropertiesServer::Set(const QString &interface, const QString &property, co
6369 if (interface == QStringLiteral("com.canonical.unity.AccountsService") &&
6470 property == QStringLiteral("LauncherItems")) {
6571 newValue = QVariant::fromValue(qdbus_cast<QList<QVariantMap>>(newValue.value<QDBusArgument>()));
72+ } else if (interface == "org.freedesktop.Accounts.User" && property == "InputSources") {
73+ newValue = QVariant::fromValue(qdbus_cast<StringMapList>(newValue.value<QDBusArgument>()));
6674 }
6775
6876 oldValue = newValue;
@@ -89,6 +97,7 @@ void PropertiesServer::Set(const QString &interface, const QString &property, co
8997void PropertiesServer::Reset()
9098{
9199 m_properties["com.canonical.unity.AccountsService"]["demo-edges"] = false;
100+ m_properties["com.canonical.unity.AccountsService"]["DemoEdgesCompleted"] = QStringList();
92101 m_properties["com.canonical.unity.AccountsService"]["LauncherItems"] = QVariant::fromValue(QList<QVariantMap>());
93102 m_properties["com.canonical.unity.AccountsService.Private"]["FailedLogins"] = 0;
94103 m_properties["com.ubuntu.touch.AccountsService.SecurityPrivacy"]["StatsWelcomeScreen"] = true;
@@ -100,4 +109,5 @@ void PropertiesServer::Reset()
100109 m_properties["com.ubuntu.location.providers.here.AccountsService"]["LicenseBasePath"] = "";
101110 m_properties["org.freedesktop.Accounts.User"]["BackgroundFile"] = "";
102111 m_properties["org.freedesktop.Accounts.User"]["RealName"] = "";
112+ m_properties["org.freedesktop.Accounts.User"]["InputSources"] = QVariant::fromValue(StringMapList());
103113}
tests/plugins/AccountsService/PropertiesServer.h−1
@@ -20,7 +20,6 @@
2020#ifndef UNITY_PROPERTIESSERVER_H
2121#define UNITY_PROPERTIESSERVER_H
2222
23−#include "PropertiesServer.h"
2423#include <QDBusContext>
2524#include <QDBusVariant>
2625#include <QObject>
tests/plugins/AccountsService/client.cpp+67
@@ -23,6 +23,11 @@
2323#include <QTest>
2424#include <QDebug>
2525#include <QDBusReply>
26+#include <QDBusMetaType>
27+
28+using StringMap = QMap<QString,QString>;
29+using StringMapList = QList<StringMap>;
30+Q_DECLARE_METATYPE(StringMapList)
2631
2732template <class T>
2833QVariant dbusVariant(const T& value) { return QVariant::fromValue(QDBusVariant(value)); }
@@ -52,6 +57,9 @@ public:
5257
5358 QObject::connect(m_uscInputInterface, SIGNAL(setMousePrimaryButtonCalled(int)),
5459 this, SIGNAL(setMousePrimaryButtonCalled(int)));
60+
61+ qDBusRegisterMetaType<StringMap>();
62+ qDBusRegisterMetaType<StringMapList>();
5563 }
5664
5765private Q_SLOTS:
@@ -117,6 +125,43 @@ private Q_SLOTS:
117125 QCOMPARE(session.hereEnabled(), true);
118126 }
119127
128+ void testMarkDemoEdgeCompleted()
129+ {
130+ AccountsService session(this, QTest::currentTestFunction());
131+ QSignalSpy changedSpy(&session, &AccountsService::demoEdgesCompletedChanged);
132+
133+ QCOMPARE(changedSpy.count(), 0);
134+ QCOMPARE(session.demoEdgesCompleted(), QStringList());
135+
136+ session.markDemoEdgeCompleted("testedge");
137+ QCOMPARE(changedSpy.count(), 1);
138+ QCOMPARE(session.demoEdgesCompleted(), QStringList() << "testedge");
139+
140+ session.markDemoEdgeCompleted("testedge");
141+ QCOMPARE(changedSpy.count(), 1);
142+ QCOMPARE(session.demoEdgesCompleted(), QStringList() << "testedge");
143+
144+ session.markDemoEdgeCompleted("testedge2");
145+ QCOMPARE(changedSpy.count(), 2);
146+ QCOMPARE(session.demoEdgesCompleted(), QStringList() << "testedge" << "testedge2");
147+ }
148+
149+ void testAsynchronousChangeForDemoEdgesCompleted()
150+ {
151+ AccountsService session(this, QTest::currentTestFunction());
152+ QSignalSpy changedSpy(&session, &AccountsService::demoEdgesCompletedChanged);
153+
154+ QCOMPARE(changedSpy.count(), 0);
155+ QCOMPARE(session.demoEdgesCompleted(), QStringList());
156+
157+ ASSERT_DBUS_CALL(m_userInterface->call("Set",
158+ "com.canonical.unity.AccountsService",
159+ "DemoEdgesCompleted",
160+ dbusVariant(QStringList() << "testedge")));
161+ QTRY_COMPARE(changedSpy.count(), 1);
162+ QCOMPARE(session.demoEdgesCompleted(), QStringList() << "testedge");
163+ }
164+
120165 void testAsynchronousChangeForDemoEdges()
121166 {
122167 AccountsService session(this, QTest::currentTestFunction());
@@ -266,6 +311,28 @@ private Q_SLOTS:
266311 QCOMPARE(arguments.at(0).toInt(), 0);
267312 }
268313
314+ void testAsynchronousChangeForKeymaps()
315+ {
316+ AccountsService session(this, QTest::currentTestFunction());
317+
318+ QCOMPARE(session.keymaps(), {"us"});
319+
320+ StringMapList inputSources;
321+ StringMap map1;
322+ map1.insert("xkb", "cz+qwerty");
323+ inputSources.append(map1);
324+ StringMap map2;
325+ map2.insert("xkb", "fr");
326+ inputSources.append(map2);
327+
328+ ASSERT_DBUS_CALL(m_userInterface->asyncCall("Set",
329+ "org.freedesktop.Accounts.User",
330+ "InputSources",
331+ QVariant::fromValue(QDBusVariant(QVariant::fromValue(inputSources)))));
332+ QStringList result = {"cz+qwerty", "fr"};
333+ QTRY_COMPARE(session.keymaps(), result);
334+ }
335+
269336Q_SIGNALS:
270337 void propertiesChanged(const QString &interface, const QVariantMap &changed, const QStringList &invalid);
271338 void setMousePrimaryButtonCalled(int button);
tests/plugins/Dash/tst_ScopeStyle.qml+1−1
@@ -137,7 +137,7 @@ Rectangle {
137137
138138 function test_headerBackground_data() {
139139 return [
140− { tag: "default", index: 0, headerBackground: "color:///#f5f5f5" },
140+ { tag: "default", index: 0, headerBackground: "color:///#ffffff" },
141141 { tag: "black", index: 3, headerBackground: "gradient:///white/blue" },
142142 ];
143143 }
tests/plugins/Ubuntu/Gestures/CMakeLists.txt+7−1
@@ -20,6 +20,11 @@ macro(build_gesture_test CLASSNAME)
2020 target_link_libraries(${CLASSNAME}TestExec UbuntuGesturesQml UbuntuGestures)
2121endmacro()
2222
23+set(UNITY_IMPORT_PATHS
24+ ${CMAKE_BINARY_DIR}/tests/utils/modules
25+ ${UNITY_PLUGINPATH}
26+)
27+
2328macro(add_gesture_unit_test CLASSNAME)
2429 build_gesture_test(${CLASSNAME})
2530 add_unity8_unittest(${CLASSNAME} ${CLASSNAME}TestExec
@@ -36,7 +41,7 @@ macro(add_gesture_ui_test CLASSNAME)
3641 DEPENDS UbuntuGesturesTest-qmlfiles
3742 ${ARGN}
3843 )
39− add_manual_qml_test(. ${CLASSNAME} IMPORT_PATHS ${UNITY_PLUGINPATH})
44+ add_manual_qml_test(. ${CLASSNAME} IMPORT_PATHS ${UNITY_IMPORT_PATHS})
4045endmacro()
4146
4247add_gesture_ui_test(DirectionalDragArea)
@@ -46,3 +51,4 @@ add_gesture_ui_test(TouchDispatcher)
4651add_gesture_ui_test(TouchGate)
4752add_gesture_unit_test(Damper)
4853add_gesture_unit_test(AxisVelocityCalculator)
54+add_gesture_ui_test(TouchGestureArea)
tests/plugins/Ubuntu/Gestures/GestureTest.cpp+44
@@ -17,6 +17,7 @@
1717#include "GestureTest.h"
1818
1919#include <qpa/qwindowsysteminterface.h>
20+#include <private/qquickwindow_p.h>
2021#include <QQmlEngine>
2122#include <QQuickView>
2223#include <QtTest>
@@ -72,6 +73,49 @@ void GestureTest::cleanup()
7273 m_view = nullptr;
7374}
7475
76+void GestureTest::sendTouchPress(qint64 timestamp, int id, QPointF pos)
77+{
78+ sendTouch(timestamp, id, pos, Qt::TouchPointPressed, QEvent::TouchBegin);
79+}
80+
81+void GestureTest::sendTouchUpdate(qint64 timestamp, int id, QPointF pos)
82+{
83+ sendTouch(timestamp, id, pos, Qt::TouchPointMoved, QEvent::TouchUpdate);
84+}
85+
86+void GestureTest::sendTouchRelease(qint64 timestamp, int id, QPointF pos)
87+{
88+ sendTouch(timestamp, id, pos, Qt::TouchPointReleased, QEvent::TouchEnd);
89+}
90+
91+void GestureTest::sendTouch(qint64 timestamp, int id, QPointF pos,
92+ Qt::TouchPointState pointState, QEvent::Type eventType)
93+{
94+ m_fakeTimerFactory->updateTime(timestamp);
95+
96+ QTouchEvent::TouchPoint point;
97+
98+ point.setState(pointState);
99+ point.setId(id);
100+ point.setScenePos(pos);
101+ point.setPos(pos);
102+
103+ QList<QTouchEvent::TouchPoint> points;
104+ points << point;
105+
106+ QTouchEvent touchEvent(eventType, m_device, Qt::NoModifier, Qt::TouchPointPressed, points);
107+ QCoreApplication::sendEvent(m_view, &touchEvent);
108+
109+ QQuickWindowPrivate *windowPrivate = QQuickWindowPrivate::get(m_view);
110+ windowPrivate->flushDelayedTouchEvent();
111+}
112+
113+void GestureTest::passTime(qint64 timeSpanMs)
114+{
115+ qint64 finalTime = m_fakeTimerFactory->timeSource()->msecsSinceReference() + timeSpanMs;
116+ m_fakeTimerFactory->updateTime(finalTime);
117+}
118+
75119////////////////////////// TouchMemento /////////////////////////////
76120
77121TouchMemento::TouchMemento(const QTouchEvent *touchEvent)
tests/plugins/Ubuntu/Gestures/GestureTest.h+13−1
@@ -72,7 +72,7 @@ class GestureTest : public QObject
7272{
7373 Q_OBJECT
7474public:
75− // \param qmlFilename name of the qml file to be loaded by the QQuickView
75+ // \param qmlFilename name of the qml file to be loaded by the QQuickView
7676 GestureTest(const QString &qmlFilename);
7777
7878protected Q_SLOTS:
@@ -80,6 +80,18 @@ protected Q_SLOTS:
8080 virtual void init(); // called right before each and every test function is executed
8181 virtual void cleanup(); // called right after each and every test function is executed
8282
83+protected:
84+ // QTest::touchEvent takes QPoint instead of QPointF and I don't want to
85+ // lose precision due to rounding.
86+ // Besides, those helper functions lead to more compact code.
87+ void sendTouchPress(qint64 timestamp, int id, QPointF pos);
88+ void sendTouchUpdate(qint64 timestamp, int id, QPointF pos);
89+ void sendTouchRelease(qint64 timestamp, int id, QPointF pos);
90+ void sendTouch(qint64 timestamp, int id, QPointF pos,
91+ Qt::TouchPointState pointState, QEvent::Type eventType);
92+
93+ void passTime(qint64 timeSpanMs);
94+
8395protected:
8496 QTouchDevice *m_device;
8597 QQuickView *m_view;
tests/plugins/Ubuntu/Gestures/tst_DirectionalDragArea.cpp−56
@@ -20,7 +20,6 @@
2020#include <QtQml/QQmlEngine>
2121#include <QPointer>
2222#include <private/qquickmousearea_p.h>
23−#include <private/qquickwindow_p.h>
2423
2524
2625#include <DirectionalDragArea.h>
@@ -109,18 +108,6 @@ private Q_SLOTS:
109108 void makoRightEdgeDrag_verticalDownwards();
110109 void makoLeftEdgeDrag_slowStart();
111110 void makoLeftEdgeDrag_movesSlightlyBackwardsOnStart();
112−
113−private:
114− // QTest::touchEvent takes QPoint instead of QPointF and I don't want to
115− // lose precision due to rounding.
116− // Besides, those helper functions lead to more compact code.
117− void sendTouchPress(qint64 timestamp, int id, QPointF pos);
118− void sendTouchUpdate(qint64 timestamp, int id, QPointF pos);
119− void sendTouchRelease(qint64 timestamp, int id, QPointF pos);
120− void sendTouch(qint64 timestamp, int id, QPointF pos,
121− Qt::TouchPointState pointState, QEvent::Type eventType);
122−
123− void passTime(qint64 timeSpanMs);
124111};
125112
126113tst_DirectionalDragArea::tst_DirectionalDragArea()
@@ -141,49 +128,6 @@ void tst_DirectionalDragArea::init()
141128 QTRY_COMPARE(m_view->height(), (int)m_view->rootObject()->height());
142129}
143130
144−void tst_DirectionalDragArea::sendTouchPress(qint64 timestamp, int id, QPointF pos)
145−{
146− sendTouch(timestamp, id, pos, Qt::TouchPointPressed, QEvent::TouchBegin);
147−}
148−
149−void tst_DirectionalDragArea::sendTouchUpdate(qint64 timestamp, int id, QPointF pos)
150−{
151− sendTouch(timestamp, id, pos, Qt::TouchPointMoved, QEvent::TouchUpdate);
152−}
153−
154−void tst_DirectionalDragArea::sendTouchRelease(qint64 timestamp, int id, QPointF pos)
155−{
156− sendTouch(timestamp, id, pos, Qt::TouchPointReleased, QEvent::TouchEnd);
157−}
158−
159−void tst_DirectionalDragArea::sendTouch(qint64 timestamp, int id, QPointF pos,
160− Qt::TouchPointState pointState, QEvent::Type eventType)
161−{
162− m_fakeTimerFactory->updateTime(timestamp);
163−
164− QTouchEvent::TouchPoint point;
165−
166− point.setState(pointState);
167− point.setId(id);
168− point.setScenePos(pos);
169− point.setPos(pos);
170−
171− QList<QTouchEvent::TouchPoint> points;
172− points << point;
173−
174− QTouchEvent touchEvent(eventType, m_device, Qt::NoModifier, Qt::TouchPointPressed, points);
175− QCoreApplication::sendEvent(m_view, &touchEvent);
176−
177− QQuickWindowPrivate *windowPrivate = QQuickWindowPrivate::get(m_view);
178− windowPrivate->flushDelayedTouchEvent();
179−}
180−
181−void tst_DirectionalDragArea::passTime(qint64 timeSpanMs)
182−{
183− qint64 finalTime = m_fakeTimerFactory->timeSource()->msecsSinceReference() + timeSpanMs;
184− m_fakeTimerFactory->updateTime(finalTime);
185−}
186−
187131namespace {
188132QPointF calculateInitialTouchPos(DirectionalDragArea *edgeDragArea)
189133{
tests/plugins/Ubuntu/Gestures/tst_TouchGestureArea.cppnew file+270
@@ -0,0 +1,270 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "GestureTest.h"
18+
19+#include <TouchGestureArea.h>
20+
21+#include <QQuickItem>
22+#include <QQuickView>
23+#include <QtTest>
24+
25+namespace {
26+QPointF calculateInitialTouchPos(TouchGestureArea *edgeDragArea)
27+{
28+ QPointF localCenter(edgeDragArea->width() / 2., edgeDragArea->height() / 2.);
29+ return edgeDragArea->mapToScene(localCenter);
30+}
31+}
32+
33+class tst_TouchGestureArea: public GestureTest
34+{
35+ Q_OBJECT
36+public:
37+ tst_TouchGestureArea();
38+private Q_SLOTS:
39+ void init() override; // called right before each and every test function is executed
40+
41+ void minimumTouchPoints();
42+ void maximumTouchPoints();
43+ void minimumAndMaximumTouchPoints();
44+ void rejectGestureAfterRecognitionPeriod();
45+ void releaseAndPressRecognisedGestureDoesNotRejectForPeriod();
46+ void topAreaReceivesOwnershipFirstWithEqualPoints();
47+ void topAreaReceivesOwnershipFirstWithMorePoints();
48+
49+private:
50+ void initGestureComponent(TouchGestureArea *area);
51+
52+ QQuickItem *m_blueRect;
53+ TouchGestureArea *m_gestureBottom;
54+ TouchGestureArea *m_gestureMiddle;
55+ TouchGestureArea *m_gestureTop;
56+};
57+
58+tst_TouchGestureArea::tst_TouchGestureArea()
59+ : GestureTest(QStringLiteral("tst_TouchGestureArea.qml"))
60+{
61+}
62+
63+inline void tst_TouchGestureArea::initGestureComponent(TouchGestureArea* area)
64+{
65+ area->setRecognitionTimer(m_fakeTimerFactory->createTimer(area));
66+ area->setMinimumTouchPoints(1);
67+ area->setMaximumTouchPoints(INT_MAX);
68+ area->setRecognitionPeriod(50);
69+ area->setReleaseRejectPeriod(100);
70+ // start tests with area disabled (enable as desired)
71+ area->setEnabled(false);
72+}
73+
74+void tst_TouchGestureArea::init()
75+{
76+ GestureTest::init();
77+
78+ m_blueRect = m_view->rootObject()->findChild<QQuickItem*>("blueRect");
79+ Q_ASSERT(m_blueRect != nullptr);
80+
81+ m_gestureBottom =
82+ m_view->rootObject()->findChild<TouchGestureArea*>("touchGestureAreaBottom");
83+ Q_ASSERT(m_gestureBottom != nullptr);
84+
85+ m_gestureMiddle =
86+ m_view->rootObject()->findChild<TouchGestureArea*>("touchGestureAreaMiddle");
87+ Q_ASSERT(m_gestureMiddle != nullptr);
88+
89+ m_gestureTop =
90+ m_view->rootObject()->findChild<TouchGestureArea*>("touchGestureAreaTop");
91+ Q_ASSERT(m_gestureTop != nullptr);
92+
93+ initGestureComponent(m_gestureBottom);
94+ initGestureComponent(m_gestureMiddle);
95+ initGestureComponent(m_gestureTop);
96+}
97+
98+void tst_TouchGestureArea::minimumTouchPoints()
99+{
100+ m_gestureBottom->setEnabled(true);
101+ m_gestureBottom->setMinimumTouchPoints(4);
102+
103+ QPointF touchPoint = calculateInitialTouchPos(m_gestureBottom);
104+
105+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::WaitingForTouch);
106+ QTest::touchEvent(m_view, m_device).press(0, touchPoint.toPoint());
107+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Undecided);
108+ QTest::touchEvent(m_view, m_device).stationary(0)
109+ .press(1, touchPoint.toPoint());
110+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Undecided);
111+ QTest::touchEvent(m_view, m_device).stationary(0)
112+ .stationary(1)
113+ .press(2, touchPoint.toPoint());
114+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Undecided);
115+ QTest::touchEvent(m_view, m_device).stationary(0)
116+ .stationary(1)
117+ .stationary(2)
118+ .press(3, touchPoint.toPoint());
119+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Recognized);
120+ // test minimum overflow
121+ QTest::touchEvent(m_view, m_device).stationary(0)
122+ .stationary(1)
123+ .stationary(2)
124+ .stationary(3)
125+ .press(4, touchPoint.toPoint());
126+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Recognized);
127+}
128+
129+void tst_TouchGestureArea::maximumTouchPoints()
130+{
131+ m_gestureBottom->setEnabled(true);
132+ m_gestureBottom->setMaximumTouchPoints(2);
133+
134+ QPointF touchPoint = calculateInitialTouchPos(m_gestureBottom);
135+
136+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::WaitingForTouch);
137+ QTest::touchEvent(m_view, m_device).press(0, touchPoint.toPoint());
138+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Recognized);
139+ QTest::touchEvent(m_view, m_device).stationary(0)
140+ .press(1, touchPoint.toPoint());
141+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Recognized);
142+
143+ // test maximum overflow
144+ QTest::touchEvent(m_view, m_device).stationary(0)
145+ .stationary(1)
146+ .press(2, touchPoint.toPoint());
147+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Rejected);
148+ // test still rejected
149+ QTest::touchEvent(m_view, m_device).stationary(0)
150+ .stationary(1)
151+ .press(3, touchPoint.toPoint());
152+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Rejected);
153+}
154+
155+void tst_TouchGestureArea::minimumAndMaximumTouchPoints()
156+{
157+ m_gestureBottom->setEnabled(true);
158+ m_gestureBottom->setMinimumTouchPoints(2);
159+ m_gestureBottom->setMaximumTouchPoints(2);
160+
161+ QPointF touchPoint = calculateInitialTouchPos(m_gestureBottom);
162+
163+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::WaitingForTouch);
164+ QTest::touchEvent(m_view, m_device).press(0, touchPoint.toPoint());
165+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Undecided);
166+ QTest::touchEvent(m_view, m_device).stationary(0)
167+ .press(1, touchPoint.toPoint());
168+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Recognized);
169+
170+ // test maximum overflow
171+ QTest::touchEvent(m_view, m_device).stationary(0)
172+ .stationary(1)
173+ .press(2, touchPoint.toPoint());
174+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Rejected);
175+}
176+
177+void tst_TouchGestureArea::rejectGestureAfterRecognitionPeriod()
178+{
179+ m_gestureBottom->setEnabled(true);
180+ m_gestureBottom->setMinimumTouchPoints(2);
181+ m_gestureBottom->setMaximumTouchPoints(2);
182+
183+ QPointF touchPoint = calculateInitialTouchPos(m_gestureBottom);
184+
185+ QTest::touchEvent(m_view, m_device).press(0, touchPoint.toPoint());
186+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Undecided); // Recognition period is 50.
187+ passTime(40);
188+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Undecided);
189+ passTime(10);
190+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Rejected);
191+}
192+
193+void tst_TouchGestureArea::releaseAndPressRecognisedGestureDoesNotRejectForPeriod()
194+{
195+ m_gestureBottom->setEnabled(true);
196+ m_gestureBottom->setMinimumTouchPoints(2);
197+ m_gestureBottom->setMaximumTouchPoints(2);
198+
199+ bool wasRejected = false;
200+ connect(m_gestureBottom, &TouchGestureArea::statusChanged,
201+ this, [&wasRejected](int status) {
202+ if (status == TouchGestureArea::Rejected) wasRejected = true;
203+ });
204+
205+ QPointF touchPoint = calculateInitialTouchPos(m_gestureBottom);
206+
207+ QTest::touchEvent(m_view, m_device).press(0, touchPoint.toPoint())
208+ .press(1, touchPoint.toPoint());
209+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Recognized);
210+ passTime(5);
211+ QTest::touchEvent(m_view, m_device).stationary(0)
212+ .release(1, touchPoint.toPoint()); // Release period is 100.
213+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Recognized);
214+ passTime(70);
215+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Recognized);
216+ QCOMPARE(wasRejected, false);
217+ passTime(29);
218+ QCOMPARE(wasRejected, false);
219+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Recognized);
220+ passTime(1);
221+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::Rejected);
222+}
223+
224+void tst_TouchGestureArea::topAreaReceivesOwnershipFirstWithEqualPoints()
225+{
226+ m_gestureBottom->setEnabled(true);
227+ m_gestureMiddle->setEnabled(true);
228+ m_gestureTop->setEnabled(true);
229+
230+ m_gestureBottom->setMinimumTouchPoints(1);
231+ m_gestureMiddle->setMinimumTouchPoints(1);
232+ m_gestureTop->setMinimumTouchPoints(1);
233+
234+ QPointF touchPoint = calculateInitialTouchPos(m_gestureBottom);
235+
236+ QTest::touchEvent(m_view, m_device).press(0, touchPoint.toPoint());
237+
238+ QCOMPARE((int)m_gestureTop->status(), (int)TouchGestureArea::Recognized);
239+ // Lower items will not recieve the touch events
240+ QCOMPARE((int)m_gestureMiddle->status(), (int)TouchGestureArea::WaitingForTouch);
241+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::WaitingForTouch);
242+}
243+
244+void tst_TouchGestureArea::topAreaReceivesOwnershipFirstWithMorePoints()
245+{
246+ m_gestureBottom->setEnabled(true);
247+ m_gestureMiddle->setEnabled(true);
248+ m_gestureTop->setEnabled(true);
249+
250+ m_gestureBottom->setMinimumTouchPoints(1);
251+ m_gestureMiddle->setMinimumTouchPoints(1);
252+ m_gestureTop->setMinimumTouchPoints(2);
253+
254+ QPointF touchPoint = calculateInitialTouchPos(m_gestureBottom);
255+
256+ QTest::touchEvent(m_view, m_device).press(0, touchPoint.toPoint());
257+ QCOMPARE((int)m_gestureTop->status(), (int)TouchGestureArea::Undecided);
258+ QCOMPARE((int)m_gestureMiddle->status(), (int)TouchGestureArea::Undecided);
259+ // The middle item will accept the events; so the bottom item will not get a chance.
260+ QCOMPARE((int)m_gestureBottom->status(), (int)TouchGestureArea::WaitingForTouch);
261+
262+ QTest::touchEvent(m_view, m_device).stationary(0)
263+ .press(1, touchPoint.toPoint());
264+ QCOMPARE((int)m_gestureTop->status(), (int)TouchGestureArea::Recognized);
265+ QCOMPARE((int)m_gestureMiddle->status(), (int)TouchGestureArea::Rejected);
266+}
267+
268+QTEST_MAIN(tst_TouchGestureArea)
269+
270+#include "tst_TouchGestureArea.moc"
tests/plugins/Ubuntu/Gestures/tst_TouchGestureArea.qmlnew file+90
@@ -0,0 +1,90 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import Ubuntu.Components 1.3
19+import Ubuntu.Gestures 0.1
20+import Unity.Test 0.1
21+
22+Rectangle {
23+ width: units.gu(60)
24+ height: units.gu(60)
25+ color: "white"
26+
27+ Binding {
28+ target: MouseTouchAdaptor
29+ property: "enabled"
30+ value: true
31+ }
32+
33+ Rectangle {
34+ id: blueRect
35+ objectName: "blueRect"
36+ color: "blue"
37+ width: units.gu(50)
38+ height: units.gu(50)
39+
40+ Timer {
41+ id: resetColorTimer
42+ interval: 300
43+ repeat: false
44+ onTriggered: {
45+ blueRect.color = "blue";
46+ }
47+ }
48+
49+ function blink(color) {
50+ blueRect.color = color;
51+ resetColorTimer.start();
52+ }
53+
54+ TouchGestureArea {
55+ anchors.fill: parent
56+ objectName: "touchGestureAreaBottom"
57+ minimumTouchPoints: 1
58+
59+ onStatusChanged: {
60+ if (status == TouchGestureArea.Recognized) {
61+ blueRect.blink("red");
62+ }
63+ }
64+ }
65+
66+ TouchGestureArea {
67+ anchors.fill: parent
68+ objectName: "touchGestureAreaMiddle"
69+ minimumTouchPoints: 2
70+
71+ onStatusChanged: {
72+ if (status == TouchGestureArea.Recognized) {
73+ blueRect.blink("yellow");
74+ }
75+ }
76+ }
77+
78+ TouchGestureArea {
79+ anchors.fill: parent
80+ objectName: "touchGestureAreaTop"
81+ minimumTouchPoints: 3
82+
83+ onStatusChanged: {
84+ if (status == TouchGestureArea.Recognized) {
85+ blueRect.blink("green");
86+ }
87+ }
88+ }
89+ }
90+}
tests/plugins/Unity/Launcher/launchermodeltest.cpp+1
@@ -48,6 +48,7 @@ public:
4848 QString comment() const override { return "this is a mock"; }
4949 QUrl icon() const override { return QUrl(); }
5050 ApplicationInfoInterface::Stage stage() const override { return ApplicationInfoInterface::MainStage; }
51+ void setStage(ApplicationInfoInterface::Stage) override {}
5152 ApplicationInfoInterface::State state() const override { return ApplicationInfoInterface::Running; }
5253 bool focused() const override { return m_focused; }
5354 QString splashTitle() const override { return QString(); }
tests/qmltests/CMakeLists.txt+4
@@ -53,6 +53,10 @@ add_unity8_qmltest(Dash/Previews PreviewTextSummary)
5353add_unity8_qmltest(Dash/Previews PreviewVideoPlayback)
5454add_unity8_qmltest(Dash/Previews PreviewWidgetFactory)
5555add_unity8_qmltest(Dash/Previews PreviewZoomableImage)
56+add_unity8_qmltest(Dash/Filters FilterOptionSelector)
57+add_unity8_qmltest(Dash/Filters FilterRangeInput)
58+add_unity8_qmltest(Dash/Filters FilterValueSlider)
59+add_unity8_qmltest(Dash/Filters FilterWidgetFactory)
5660add_unity8_qmltest(Dash/ScopeSettings ScopeSettingBoolean)
5761add_unity8_qmltest(Dash/ScopeSettings ScopeSettingList)
5862add_unity8_qmltest(Dash/ScopeSettings ScopeSettingNumber)
tests/qmltests/Components/tst_PhysicalKeysMapper.qml+3−3
@@ -26,9 +26,9 @@ Item {
2626
2727 property var physicalKeysMapper: loader.item
2828
29− WindowKeysFilter {
30− Keys.onPressed: physicalKeysMapper.onKeyPressed(event, currentEventTimestamp);
31− Keys.onReleased: physicalKeysMapper.onKeyReleased(event, currentEventTimestamp);
29+ WindowInputFilter {
30+ Keys.onPressed: physicalKeysMapper.onKeyPressed(event, lastInputTimestamp);
31+ Keys.onReleased: physicalKeysMapper.onKeyReleased(event, lastInputTimestamp);
3232 }
3333
3434 Loader {
tests/qmltests/Dash/Filters/tst_FilterOptionSelector.qmlnew 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 General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import QtTest 1.0
19+import "../../../../qml/Dash/Filters"
20+import Unity.Test 0.1 as UT
21+import Unity 0.2
22+import Ubuntu.Components 1.3
23+
24+Rectangle {
25+ id: root
26+ width: units.gu(60)
27+ height: units.gu(80)
28+ color: theme.palette.selected.background
29+
30+ property var singleSelectionWidgetData
31+
32+ ListModel {
33+ id: optionsSingleSelect
34+ ListElement {
35+ label: "A"
36+ checked: false
37+ }
38+ ListElement {
39+ label: "B"
40+ checked: false
41+ }
42+ ListElement {
43+ label: "C"
44+ checked: false
45+ }
46+
47+ function setChecked(index, checked) {
48+ for (var i = 0; i < optionsSingleSelect.count; ++i)
49+ optionsSingleSelect.setProperty(i, "checked", false);
50+ optionsSingleSelect.setProperty(index, "checked", checked);
51+ }
52+ }
53+
54+ Component.onCompleted: {
55+ singleSelectionWidgetData = { label: "Hola", options: optionsSingleSelect }
56+ }
57+
58+ FilterWidgetFactory {
59+ id: factory
60+ widgetId: "testOptionSelectorFilter"
61+ widgetType: Filters.OptionSelectorFilter
62+ widgetData: singleSelectionWidgetData
63+ anchors {
64+ left: parent.left
65+ right: parent.right
66+ }
67+ }
68+
69+ UT.UnityTestCase {
70+ name: "FilterOptionSelector"
71+ when: windowShown
72+
73+ function test_optionSelector() {
74+ var expandingItem = findChild(factory, "expandingItem");
75+ // Open the selector
76+ mouseClick(factory);
77+ // wait for it to stop growing
78+ tryCompare(factory, "implicitHeight", expandingItem.expandedHeight);
79+
80+ // Check the first option
81+ var option0 = findChild(factory, "testOptionSelectorFilterlabel0");
82+ mouseClick(option0);
83+ verify(optionsSingleSelect.get(0).checked);
84+
85+ // Check the second option
86+ var option1 = findChild(factory, "testOptionSelectorFilterlabel1");
87+ mouseClick(option1);
88+ verify(!optionsSingleSelect.get(0).checked);
89+ verify(optionsSingleSelect.get(1).checked);
90+
91+ // Uncheck the second option
92+ mouseClick(option1);
93+ verify(!optionsSingleSelect.get(1).checked);
94+ }
95+ }
96+}
tests/qmltests/Dash/Filters/tst_FilterRangeInput.qmlnew file+247
@@ -0,0 +1,247 @@
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 General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import QtTest 1.0
19+import Ubuntu.Components 1.3
20+import "../../../../qml/Dash/Filters"
21+import Unity.Test 0.1 as UT
22+import Unity 0.2
23+
24+Rectangle {
25+ id: root
26+ width: units.gu(60)
27+ height: units.gu(80)
28+ color: theme.palette.selected.background
29+
30+ QtObject {
31+ id: widgetData1
32+
33+ property string title
34+ property real startValue
35+ property real endValue
36+ property bool hasStartValue
37+ property bool hasEndValue
38+ property string startPrefixLabel
39+ property string startPostfixLabel
40+ property string centralLabel
41+ property string endPrefixLabel
42+ property string endPostfixLabel
43+
44+ onStartValueChanged: {
45+ hasStartValue = true;
46+ }
47+
48+ onEndValueChanged: {
49+ hasEndValue = true;
50+ }
51+
52+ function eraseStartValue() {
53+ hasStartValue = false;
54+ }
55+
56+ function eraseEndValue() {
57+ hasEndValue = false;
58+ }
59+ }
60+
61+ Component.onCompleted: {
62+ generateData();
63+ }
64+
65+ function generateData() {
66+ widgetData1.title = title.text
67+ widgetData1.startValue = startValue.text;
68+ widgetData1.endValue = endValue.text;
69+ widgetData1.hasStartValue = startValueHasValue.checked;
70+ widgetData1.hasEndValue = endValueHasValue.checked;
71+ widgetData1.startPrefixLabel = startValuePrefixLabel.text;
72+ widgetData1.startPostfixLabel = startValuePostfixLabel.text;
73+ widgetData1.centralLabel = centralLabel.text;
74+ widgetData1.endPrefixLabel = endValuePrefixLabel.text;
75+ widgetData1.endPostfixLabel = endValuePostfixLabel.text;
76+
77+ factory.widgetData = widgetData1;
78+ }
79+
80+ FilterWidgetFactory {
81+ id: factory
82+ widgetId: "testRangeInputFilter"
83+ widgetType: Filters.RangeInputFilter
84+ anchors {
85+ left: parent.left
86+ right: parent.right
87+ }
88+ clip: true
89+ }
90+
91+ Column {
92+ anchors.top: factory.bottom
93+ width: parent.width
94+ spacing: units.gu(1)
95+
96+ Text {
97+ text: "Values"
98+ }
99+
100+ Row {
101+ spacing: units.gu(1)
102+ Text {
103+ text: "Title"
104+ verticalAlignment: Text.AlignVCenter
105+ height: parent.height
106+ }
107+ TextField {
108+ id: title
109+ text: ""
110+ }
111+ }
112+
113+ Row {
114+ spacing: units.gu(1)
115+ Text {
116+ text: "Start Value Prefix Label"
117+ verticalAlignment: Text.AlignVCenter
118+ height: parent.height
119+ }
120+ TextField {
121+ id: startValuePrefixLabel
122+ text: "Pre1"
123+ }
124+ }
125+
126+ Row {
127+ spacing: units.gu(1)
128+ Text {
129+ text: "Start Value Postfix Label"
130+ verticalAlignment: Text.AlignVCenter
131+ height: parent.height
132+ }
133+ TextField {
134+ id: startValuePostfixLabel
135+ text: "Post1"
136+ }
137+ }
138+
139+ Row {
140+ spacing: units.gu(1)
141+ Text {
142+ text: "Central Label"
143+ verticalAlignment: Text.AlignVCenter
144+ height: parent.height
145+ }
146+ TextField {
147+ id: centralLabel
148+ text: "to"
149+ }
150+ }
151+
152+ Row {
153+ spacing: units.gu(1)
154+ Text {
155+ text: "End Value Prefix Label"
156+ verticalAlignment: Text.AlignVCenter
157+ height: parent.height
158+ }
159+ TextField {
160+ id: endValuePrefixLabel
161+ text: "Pre2"
162+ }
163+ }
164+
165+ Row {
166+ spacing: units.gu(1)
167+ Text {
168+ text: "End Value Postfix Label"
169+ verticalAlignment: Text.AlignVCenter
170+ height: parent.height
171+ }
172+ TextField {
173+ id: endValuePostfixLabel
174+ text: "Post2"
175+ }
176+ }
177+
178+ Row {
179+ spacing: units.gu(1)
180+ Text {
181+ text: "Start Value"
182+ verticalAlignment: Text.AlignVCenter
183+ height: parent.height
184+ }
185+ TextField {
186+ id: startValue
187+ text: "-1"
188+ }
189+ CheckBox {
190+ id: startValueHasValue
191+ }
192+ }
193+
194+
195+ Row {
196+ spacing: units.gu(1)
197+ Text {
198+ text: "End Value"
199+ verticalAlignment: Text.AlignVCenter
200+ height: parent.height
201+ }
202+ TextField {
203+ id: endValue
204+ text: "5"
205+ }
206+ CheckBox {
207+ id: endValueHasValue
208+ checked: true
209+ }
210+ }
211+
212+ Button {
213+ text: "Set Values"
214+ onClicked: root.generateData();
215+ }
216+ }
217+
218+ UT.UnityTestCase {
219+ name: "FilterRangeInput"
220+ when: windowShown
221+
222+ function init() {
223+ root.generateData();
224+ }
225+
226+ function test_initialStatus() {
227+ var startValueField = findChild(factory, "startValueField");
228+ compare(startValueField.text, "");
229+
230+ var endValueField = findChild(factory, "endValueField");
231+ compare(endValueField.text, "5");
232+ }
233+
234+ function test_dotStays() {
235+ var startValueField = findChild(factory, "startValueField");
236+ compare(startValueField.text, "");
237+
238+ startValueField.text = "4.5";
239+ compare(startValueField.text, "4.5");
240+ compare(widgetData1.startValue, 4.5);
241+
242+ startValueField.text = "4.";
243+ compare(startValueField.text, "4.");
244+ compare(widgetData1.startValue, 4);
245+ }
246+ }
247+}
tests/qmltests/Dash/Filters/tst_FilterValueSlider.qmlnew file+101
@@ -0,0 +1,101 @@
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 General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import QtTest 1.0
19+import Ubuntu.Components 1.3
20+import "../../../../qml/Dash/Filters"
21+import Unity.Test 0.1 as UT
22+import Unity 0.2
23+
24+Rectangle {
25+ id: root
26+ width: units.gu(60)
27+ height: units.gu(80)
28+ color: theme.palette.selected.background
29+
30+ QtObject {
31+ id: widgetData1
32+
33+ property string title
34+ property int value: 30
35+ property int minValue: 10
36+ property int maxValue: 150
37+
38+ property ListModel values: ListModel {
39+ ListElement {
40+ value: 10
41+ label: "10"
42+ }
43+ ListElement {
44+ value: 30
45+ label: "30"
46+ }
47+ ListElement {
48+ value: 100
49+ label: "100"
50+ }
51+ ListElement {
52+ value: 150
53+ label: "150"
54+ }
55+ }
56+ }
57+
58+ FilterWidgetFactory {
59+ id: factory
60+ widgetId: "testRangeValueSlider"
61+ widgetType: Filters.ValueSliderFilter
62+ widgetData: widgetData1;
63+ y: 100
64+ anchors {
65+ left: parent.left
66+ right: parent.right
67+ }
68+ }
69+
70+ UT.UnityTestCase {
71+ name: "FilterValueSlider"
72+ when: windowShown
73+
74+ function test_valueChanges() {
75+ var slider = findChild(factory, "slider");
76+ compare(slider.value, 30);
77+
78+ slider.value = 50
79+ compare(widgetData1.value, 50);
80+ }
81+
82+ function test_labelPositions() {
83+ var slider = findChild(factory, "slider");
84+ var repeater = findChild(factory, "repeater");
85+
86+ // It's very hard to check the position of the
87+ // labels is right, but at least we can check
88+ // they all have different X positions and the
89+ // same Y one
90+ compare(repeater.count, 4);
91+ for (var i = 0; i < repeater.count; ++i) {
92+ var itemI = repeater.itemAt(i);
93+ for (var j = i + 1; j < repeater.count; ++j) {
94+ var itemJ = repeater.itemAt(j);
95+ verify(itemI.x != itemJ.x);
96+ verify(itemI.y == itemJ.y);
97+ }
98+ }
99+ }
100+ }
101+}
tests/qmltests/Dash/Filters/tst_FilterWidgetFactory.qmlnew file+58
@@ -0,0 +1,58 @@
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 General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import QtTest 1.0
19+import "../../../../qml/Dash/Filters"
20+import Unity.Test 0.1 as UT
21+import Unity 0.2
22+import Ubuntu.Components 1.3
23+
24+Rectangle {
25+ id: root
26+ width: units.gu(60)
27+ height: units.gu(80)
28+ color: theme.palette.selected.background
29+
30+ FilterWidgetFactory {
31+ id: factory
32+ anchors {
33+ left: parent.left
34+ right: parent.right
35+ }
36+ }
37+
38+ UT.UnityTestCase {
39+ name: "FilterWidgetFactory"
40+ when: windowShown
41+
42+ function test_mapping_data() {
43+ return [
44+ { tag: "OptionSelector", type: Filters.OptionSelectorFilter, source: "FilterOptionSelector.qml" },
45+ { tag: "RangeInputFilter", type: Filters.RangeInputFilter, source: "FilterRangeInput.qml" },
46+ { tag: "ValueSliderFilter", type: Filters.ValueSliderFilter, source: "FilterValueSlider.qml" }
47+ ];
48+ }
49+
50+ function test_mapping(data) {
51+ factory.widgetData = {};
52+ factory.widgetType = data.type;
53+
54+ var loader = findChild(factory, "loader");
55+ verify((String(loader.source)).indexOf(data.source) != -1);
56+ }
57+ }
58+}
tests/qmltests/Dash/Previews/tst_Preview.qml+1
@@ -109,6 +109,7 @@ Rectangle {
109109 }
110110
111111 function test_comboEnsureVisible() {
112+ skip("Flaky in adt but can't reproduce locally. Given the deadline I won't risk this getting stuck in proposed.");
112113 waitForRendering(preview);
113114
114115 // Scroll down
tests/qmltests/Dash/tst_DashContent.qml+165−152
@@ -284,14 +284,15 @@ Item {
284284 var dashContentList = findChild(dashContent, "dashContentList");
285285 tryCompareFunction(function() { return findChild(dashContentList.currentItem, "dashNavigation") != null; }, true);
286286 var dashNavigation = findChild(dashContentList.currentItem, "dashNavigation");
287− tryCompare(dashNavigation, "visible", true);
288− var dashNavigationButton = findChild(dashContentList.currentItem, "navigationButton");
289− compare(dashNavigationButton.showList, false);
290− waitForRendering(dashNavigationButton);
291− mouseClick(dashNavigationButton);
292− compare(dashNavigationButton.showList, true);
293−
294− var navigationListView = findChild(dashNavigationButton, "navigationListView");
287+ var peExtraPanel = findChild(dashContentList.currentItem, "peExtraPanel");
288+ var searchButton = findChild(dashContentList.currentItem, "search_button");
289+ var searchTextField = findChild(dashContentList.currentItem, "searchTextField");
290+ compare(peExtraPanel.visible, false);
291+ mouseClick(searchButton);
292+ tryCompare(peExtraPanel, "visible", true);
293+ peExtraPanel.searchHistory.clear();
294+
295+ var navigationListView = findChild(dashNavigation, "navigationListView");
295296 tryCompareFunction(function() {
296297 return navigationListView.currentItem &&
297298 navigationListView.currentItem.navigation &&
@@ -300,235 +301,247 @@ Item {
300301 waitForRendering(navigationListView);
301302 waitForRendering(navigationListView.currentItem);
302303
303− var allButton = findChild(dashNavigationButton, "allButton");
304− compare(allButton.visible, false);
305−
306− var navigation = findChild(dashNavigationButton, "navigation0child3");
304+ var navigation = findChild(dashNavigation, "navigation0child3");
307305 mouseClick(navigation);
308− compare(dashNavigationButton.showList, false);
309− tryCompare(dashNavigationButton.currentNavigation, "navigationId", "middle3");
306+ compare(peExtraPanel.visible, false);
307+ tryCompare(dashNavigation.currentNavigation, "navigationId", "middle3");
310308 tryCompare(navigationListView.currentItem.navigation, "navigationId", "root");
311309
312− mouseClick(dashNavigationButton);
313− compare(dashNavigationButton.showList, true);
310+ mouseClick(searchTextField);
311+ tryCompare(peExtraPanel, "visible", true);
314312 waitForRendering(navigationListView);
315313 waitForRendering(navigationListView.currentItem);
316− compare(allButton.visible, true);
317314
318− mouseClick(allButton);
319− compare(dashNavigationButton.showList, false);
320− tryCompare(dashNavigationButton.currentNavigation, "navigationId", "root");
315+ var header0 = findChild(dashNavigation, "dashNavigationHeader0");
316+ compare(header0.backVisible, false);
317+ mouseClick(header0);
318+ compare(peExtraPanel.visible, false);
319+ tryCompare(dashNavigation.currentNavigation, "navigationId", "root");
321320 tryCompare(navigationListView.currentItem.navigation, "navigationId", "root");
322321
323− mouseClick(dashNavigationButton);
324− compare(dashNavigationButton.showList, true);
322+ var headerContainer = findChild(dashContentList.currentItem, "headerContainer");
323+ tryCompare(headerContainer, "contentY", headerContainer.height);
324+ mouseClick(searchButton);
325+ tryCompare(peExtraPanel, "visible", true);
325326 waitForRendering(navigationListView);
326327 waitForRendering(navigationListView.currentItem);
327− compare(allButton.visible, false);
328328
329− navigation = findChild(dashNavigationButton, "navigation0child2");
329+ navigation = findChild(dashNavigation, "navigation0child2");
330330 mouseClick(navigation);
331− compare(dashNavigationButton.showList, true);
332− tryCompare(dashNavigationButton.currentNavigation, "navigationId", "middle2");
331+ compare(peExtraPanel.visible, true);
332+ tryCompare(dashNavigation.currentNavigation, "navigationId", "middle2");
333333 tryCompare(navigationListView.currentItem.navigation, "navigationId", "middle2");
334334
335− var navigationList1 = findChild(dashNavigationButton, "navigation1");
336− allButton = findChild(navigationList1, "allButton");
337− var backButton = findChild(findChild(navigationList1, "navigation1"), "backButton");
338− compare(allButton.visible, true);
339− compare(backButton.visible, true);
335+ var navigationList1 = findChild(dashNavigation, "navigation1");
336+ var header1 = findChild(dashNavigation, "dashNavigationHeader1");
337+ compare(header0.height, 0);
338+ compare(header1.backVisible, true);
340339
341340 tryCompare(navigationListView, "contentX", navigationList1.x);
342341 waitForRendering(navigationListView);
343− mouseClick(allButton);
344− compare(dashNavigationButton.showList, false);
345− tryCompare(dashNavigationButton.currentNavigation, "navigationId", "middle2");
342+ mouseClick(header1);
343+ compare(peExtraPanel.visible, false);
344+ tryCompare(dashNavigation.currentNavigation, "navigationId", "middle2");
346345 tryCompare(navigationListView.currentItem.navigation, "navigationId", "middle2");
347346
348− mouseClick(dashNavigationButton);
349− compare(dashNavigationButton.showList, true);
347+ mouseClick(searchTextField);
348+ tryCompare(peExtraPanel, "visible", true);
350349 waitForRendering(navigationListView);
351350 waitForRendering(navigationListView.currentItem);
352− compare(allButton.visible, true);
353− compare(backButton.visible, true);
351+ compare(header0.height, 0);
352+ compare(header1.backVisible, true);
354353
355354 tryCompare(navigationList1.navigation, "loaded", true);
356− tryCompare(navigationList1, "implicitHeight", navigationList1.itemHeight * 10);
355+ tryCompare(navigationList1, "implicitHeight", navigationList1.itemHeight * 8);
357356 tryCompare(navigationList1, "height", navigationList1.implicitHeight);
358− navigation = findChild(dashNavigationButton, "navigation1child2");
357+ navigation = findChild(dashNavigation, "navigation1child2");
359358 mouseClick(navigation);
360− compare(dashNavigationButton.showList, false);
361− tryCompare(dashNavigationButton.currentNavigation, "navigationId", "childmiddle22");
359+ compare(peExtraPanel.visible, false);
360+ tryCompare(dashNavigation.currentNavigation, "navigationId", "childmiddle22");
362361 tryCompare(navigationListView.currentItem.navigation, "navigationId", "middle2");
363362
364− mouseClick(dashNavigationButton);
365− compare(dashNavigationButton.showList, true);
363+ mouseClick(searchTextField);
364+ tryCompare(peExtraPanel, "visible", true);
366365 waitForRendering(navigationListView);
367366 waitForRendering(navigationListView.currentItem);
368367
369368 tryCompare(navigationList1.navigation, "loaded", true);
370− navigation = findChild(dashNavigationButton, "navigation1child3");
369+ navigation = findChild(dashNavigation, "navigation1child3");
371370 mouseClick(navigation);
372− compare(dashNavigationButton.showList, false);
373− tryCompare(dashNavigationButton.currentNavigation, "navigationId", "childmiddle23");
371+ compare(peExtraPanel.visible, false);
372+ tryCompare(dashNavigation.currentNavigation, "navigationId", "childmiddle23");
374373 tryCompare(navigationListView.currentItem.navigation, "navigationId", "middle2");
375374
376− mouseClick(dashNavigationButton);
377− compare(dashNavigationButton.showList, true);
375+ mouseClick(searchTextField);
376+ tryCompare(peExtraPanel, "visible", true);
378377 waitForRendering(navigationListView);
379378 waitForRendering(navigationListView.currentItem);
380− mouseClick(backButton);
379+ mouseClick(findChild(header1, "backButton"));
381380
382− tryCompare(dashNavigationButton.currentNavigation, "navigationId", "root");
381+ tryCompare(dashNavigation.currentNavigation, "navigationId", "root");
383382 tryCompare(navigationListView.currentItem.navigation, "navigationId", "root");
384− compare(dashNavigationButton.showList, true);
385− mouseClick(dashNavigationButton);
386− compare(dashNavigationButton.showList, false);
383+ compare(peExtraPanel.visible, true);
384+ mouseClick(header0);
385+ compare(peExtraPanel.visible, false);
387386
388− mouseClick(dashNavigationButton);
389− compare(dashNavigationButton.showList, true);
387+ tryCompare(headerContainer, "contentY", headerContainer.height);
388+ mouseClick(searchButton);
389+ tryCompare(peExtraPanel, "visible", true);
390390 tryCompare(navigationListView.currentItem.navigation, "loaded", true);
391− var navigationList0 = findChild(dashNavigationButton, "navigation0");
391+ var navigationList0 = findChild(dashNavigation, "navigation0");
392392 tryCompare(navigationList0, "implicitHeight", navigationList0.itemHeight * 8);
393393 tryCompare(navigationList0, "height", navigationList0.implicitHeight);
394− navigation = findChild(dashNavigationButton, "navigation0child2");
394+ navigation = findChild(dashNavigation, "navigation0child2");
395395 mouseClick(navigation);
396− compare(dashNavigationButton.showList, true);
397− navigationList1 = findChild(dashNavigationButton, "navigation1");
396+ compare(peExtraPanel.visible, true);
397+ navigationList1 = findChild(dashNavigation, "navigation1");
398398 compare(navigationList1.navigation.loaded, false);
399− tryCompare(dashNavigationButton.currentNavigation, "navigationId", "middle2");
400− allButton = findChild(navigationList1, "allButton");
401− tryCompare(dashNavigationButton.currentNavigation, "navigationId", "middle2");
402− mouseClick(allButton);
403− tryCompare(dashNavigationButton.currentNavigation, "navigationId", "middle2");
399+ tryCompare(dashNavigation.currentNavigation, "navigationId", "middle2");
400+ mouseClick(header0);
401+ tryCompare(dashNavigation.currentNavigation, "navigationId", "middle2");
404402 }
405403
406− function test_altNavigation() {
404+ function goToSecondLevel() {
407405 var dashContentList = findChild(dashContent, "dashContentList");
408406 tryCompareFunction(function() { return findChild(dashContentList.currentItem, "dashNavigation") != null; }, true);
409407 var dashNavigation = findChild(dashContentList.currentItem, "dashNavigation");
410− tryCompare(dashNavigation, "visible", true);
411− var dashNavigationButton = findChild(dashContentList.currentItem, "altNavigationButton");
412− verify(dashNavigationButton, "Can't find navigation button");
413−
414− compare(dashNavigationButton.showList, false);
415− waitForRendering(dashNavigationButton);
416− mouseClick(dashNavigationButton);
417− compare(dashNavigationButton.showList, true);
418−
419− var navigationListView = findChild(dashNavigationButton, "navigationListView");
408+ var peExtraPanel = findChild(dashContentList.currentItem, "peExtraPanel");
409+ var searchButton = findChild(dashContentList.currentItem, "search_button");
410+ var searchTextField = findChild(dashContentList.currentItem, "searchTextField");
411+ compare(peExtraPanel.visible, false);
412+ mouseClick(searchButton);
413+ tryCompare(peExtraPanel, "visible", true);
414+
415+ var navigationListView = findChild(dashNavigation, "navigationListView");
420416 tryCompareFunction(function() {
421417 return navigationListView.currentItem &&
422418 navigationListView.currentItem.navigation &&
423419 navigationListView.currentItem.navigation.loaded; }, true);
424−
425420 waitForRendering(navigationListView);
426421 waitForRendering(navigationListView.currentItem);
427422
428− tryCompare(dashNavigationButton.currentNavigation, "navigationId", "altrootChild1");
429− tryCompare(navigationListView.currentItem.navigation, "navigationId", "altroot");
430−
431− var allButton = findChild(dashNavigationButton, "allButton");
432− compare(allButton.visible, false);
433− var backButton = findChild(navigationListView, "backButton");
434− compare(backButton.visible, false);
423+ var navigation4 = findChild(dashNavigation, "navigation0child4");
424+ mouseClick(navigation4);
425+ tryCompare(dashNavigation.currentNavigation, "navigationId", "middle4");
426+ tryCompare(navigationListView.currentItem.navigation, "navigationId", "middle4");
427+ tryCompare(dashNavigation.currentNavigation, "loaded", true);
435428
436− var navigation = findChild(dashNavigationButton, "navigation0child3");
437− mouseClick(navigation);
438− compare(dashNavigationButton.showList, false);
439− tryCompare(dashNavigationButton.currentNavigation, "navigationId", "altrootChild3");
440− tryCompare(navigationListView.currentItem.navigation, "navigationId", "altroot");
441−
442− mouseClick(dashNavigationButton);
443− compare(dashNavigationButton.showList, true);
444429 waitForRendering(navigationListView);
445430 waitForRendering(navigationListView.currentItem);
446− compare(allButton.visible, false);
447431
448− navigation = findChild(dashNavigationButton, "navigation0child2");
449− mouseClick(navigation);
450− compare(dashNavigationButton.showList, false);
451− tryCompare(dashNavigationButton.currentNavigation, "navigationId", "altrootChild2");
452− tryCompare(navigationListView.currentItem.navigation, "navigationId", "altroot");
432+ var navigation44 = findChild(dashNavigation, "navigation1child4");
433+ mouseClick(navigation44);
434+ tryCompare(dashNavigation.currentNavigation, "navigationId", "childmiddle44");
435+ tryCompare(navigationListView.currentItem.navigation, "navigationId", "childmiddle44");
436+ tryCompare(dashNavigation.currentNavigation, "loaded", true);
453437
454− mouseClick(dashNavigationButton);
455− compare(dashNavigationButton.showList, true);
456438 waitForRendering(navigationListView);
457439 waitForRendering(navigationListView.currentItem);
458− verify(!backButton.visible, "Back button should not be visible");
459440
460− compare(navigationListView.count, 1, "There should be no second-level navigation");
441+ // header 0 is hidden
442+ var header0 = findChild(dashNavigation, "dashNavigationHeader0");
443+ compare(header0.height, 0);
444+ }
445+
446+ function test_navigationSecondLevelToRoot() {
447+ goToSecondLevel();
448+
449+ var dashContentList = findChild(dashContent, "dashContentList");
450+ var dashNavigation = findChild(dashContentList.currentItem, "dashNavigation");
451+ var navigationListView = findChild(dashNavigation, "navigationListView");
452+ var peExtraPanel = findChild(dashContentList.currentItem, "peExtraPanel");
453+
454+ // Go directly to the root pressing the back button of header1
455+ var header1 = findChild(dashNavigation, "dashNavigationHeader1");
456+ compare(header1.backVisible, true);
457+ mouseClick(findChild(header1, "backButton"));
461458
462− mouseClick(dashNavigationButton);
463− compare(dashNavigationButton.showList, false);
459+ tryCompare(dashNavigation.currentNavigation, "navigationId", "root");
460+ tryCompare(navigationListView.currentItem.navigation, "navigationId", "root");
461+ compare(peExtraPanel.visible, true);
464462 }
465463
466− function test_navigationsClosing() {
464+ function test_navigationSecondLevelToFirstName() {
465+ goToSecondLevel();
466+
467467 var dashContentList = findChild(dashContent, "dashContentList");
468− tryCompareFunction(function() { return findChild(dashContentList.currentItem, "dashNavigation") != null; }, true);
469468 var dashNavigation = findChild(dashContentList.currentItem, "dashNavigation");
470− tryCompare(dashNavigation, "visible", true);
471− var dashNavigationButton = findChild(dashContentList.currentItem, "navigationButton");
472− verify(dashNavigationButton, "Can't find navigation button");
473− var dashAltNavigationButton = findChild(dashContentList.currentItem, "altNavigationButton");
474− verify(dashAltNavigationButton, "Can't find alternative navigation button");
469+ var navigationListView = findChild(dashNavigation, "navigationListView");
470+ var peExtraPanel = findChild(dashContentList.currentItem, "peExtraPanel");
475471
476− waitForRendering(dashNavigationButton);
477− waitForRendering(dashAltNavigationButton);
472+ // Go directly to the first pressing the header1
473+ var header1 = findChild(dashNavigation, "dashNavigationHeader1");
474+ mouseClick(header1);
478475
479− compare(dashNavigationButton.showList, false);
480− compare(dashAltNavigationButton.showList, false);
476+ tryCompare(dashNavigation.currentNavigation, "navigationId", "middle4");
477+ tryCompare(navigationListView.currentItem.navigation, "navigationId", "middle4");
478+ compare(peExtraPanel.visible, false);
479+ }
481480
482− mouseClick(dashAltNavigationButton);
483− compare(dashNavigationButton.showList, false);
484− compare(dashAltNavigationButton.showList, true);
481+ function test_navigationSecondLevelToFirstBack() {
482+ goToSecondLevel();
485483
486− var navigationListView = findChild(dashAltNavigationButton, "navigationListView");
487− tryCompareFunction(function() {
488− return navigationListView.currentItem &&
489− navigationListView.currentItem.navigation &&
490− navigationListView.currentItem.navigation.loaded; }, true);
484+ var dashContentList = findChild(dashContent, "dashContentList");
485+ var dashNavigation = findChild(dashContentList.currentItem, "dashNavigation");
486+ var navigationListView = findChild(dashNavigation, "navigationListView");
487+ var peExtraPanel = findChild(dashContentList.currentItem, "peExtraPanel");
491488
492− var blackRect = findChild(dashNavigation, "blackRect");
493− tryCompare(blackRect, "opacity", 0.5);
489+ // Go back to the first level pressing the back button of header2
490+ var header2 = findChild(dashNavigation, "dashNavigationHeader2");
491+ compare(header2.backVisible, true);
492+ mouseClick(findChild(header2, "backButton"));
494493
495− mouseClick(dashNavigationButton);
496− compare(dashNavigationButton.showList, false);
497− compare(dashAltNavigationButton.showList, false);
494+ tryCompare(dashNavigation.currentNavigation, "navigationId", "middle4");
495+ tryCompare(navigationListView.currentItem.navigation, "navigationId", "middle4");
496+ compare(peExtraPanel.visible, true);
497+ }
498498
499− tryCompare(navigationListView, "visible", false);
499+ function test_clearSearchWithNavigationClosed() {
500+ var dashContentList = findChild(dashContent, "dashContentList");
501+ var searchButton = findChild(dashContentList.currentItem, "search_button");
502+ var peExtraPanel = findChild(dashContentList.currentItem, "peExtraPanel");
503+ var searchTextField = findChild(dashContentList.currentItem, "searchTextField");
500504
501− mouseClick(dashNavigationButton);
502− compare(dashNavigationButton.showList, true);
503− compare(dashAltNavigationButton.showList, false);
505+ compare(peExtraPanel.visible, false);
506+ mouseClick(searchButton);
507+ tryCompare(peExtraPanel, "visible", true);
504508
505− navigationListView = findChild(dashNavigationButton, "navigationListView");
506− tryCompare(navigationListView.currentItem.navigation, "loaded", true);
509+ typeString("A");
510+ compare(peExtraPanel.visible, false);
507511
508− mouseClick(dashAltNavigationButton);
509− compare(dashNavigationButton.showList, false);
510− compare(dashAltNavigationButton.showList, false);
512+ var clearIcon = findChild(searchTextField, "clearIcon");
513+ mouseClick(clearIcon);
514+ tryCompare(peExtraPanel, "visible", true);
511515 }
512516
513− function test_navigationsSwipeToNextScope() {
517+ function test_navigationShowFilterPopup() {
518+ goToSecondLevel();
519+
514520 var dashContentList = findChild(dashContent, "dashContentList");
515− tryCompareFunction(function() { return findChild(dashContentList.currentItem, "dashNavigation") != null; }, true);
516− var dashNavigation = findChild(dashContentList.currentItem, "dashNavigation");
517− tryCompare(dashNavigation, "visible", true);
518− var dashNavigationButton = findChild(dashContentList.currentItem, "altNavigationButton");
519− verify(dashNavigationButton, "Can't find navigation button");
521+ var settingsButton = findChild(dashContentList.currentItem, "settingsButton");
520522
521− compare(dashNavigationButton.showList, false);
522− waitForRendering(dashNavigationButton);
523− mouseClick(dashNavigationButton, 0, 0);
524− compare(dashNavigationButton.showList, true);
523+ var filtersPopover = findChild(shell, "filtersPopover")
524+ verify(!filtersPopover);
525525
526− mouseFlick(dashNavigationButton, dashNavigationButton.width - units.gu(1), units.gu(1), units.gu(1), units.gu(1));
527− compare(dashNavigationButton.showList, false);
526+ mouseClick(settingsButton);
528527
528+ filtersPopover = findChild(shell, "filtersPopover")
529+
530+ verify(filtersPopover);
531+ }
532+
533+ function test_primaryFilter() {
529534 var dashContentList = findChild(dashContent, "dashContentList");
530− expectFail("", "should not change the parent list.");
531− tryCompare(dashContentList, "currentIndex", 1);
535+ tryCompareFunction(function() { return findChild(dashContentList.currentItem, "dashNavigation") != null; }, true);
536+ dashContentList.currentItem.item.scope.setHasNavigation(false);
537+ var peExtraPanel = findChild(dashContentList.currentItem, "peExtraPanel");
538+ var searchButton = findChild(dashContentList.currentItem, "search_button");
539+
540+ compare(peExtraPanel.visible, false);
541+ mouseClick(searchButton);
542+ tryCompare(peExtraPanel, "visible", true);
543+
544+ tryCompareFunction(function() { return findChild(peExtraPanel, "OSF3") != null; }, true);
532545 }
533546
534547 function test_searchHint() {
@@ -540,7 +553,7 @@ Item {
540553 var categoryListView = findChild(scope, "categoryListView");
541554 waitForRendering(categoryListView);
542555
543− compare(categoryListView.pageHeader.item.searchHint, "Search People");
556+ compare(categoryListView.pageHeader.searchHint, "Search People");
544557 }
545558
546559 function compareArrays(a, b) {
tests/qmltests/Dash/tst_DashPageHeader.qml+24−15
@@ -18,6 +18,7 @@ import QtQuick 2.4
1818import QtTest 1.0
1919import ".."
2020import "../../../qml/Dash"
21+import "../../../qml/Components/SearchHistoryModel"
2122import Ubuntu.Components 1.3
2223import Unity 0.2
2324import Unity.Test 0.1 as UT
@@ -25,7 +26,7 @@ import Unity.Test 0.1 as UT
2526Item {
2627 id: root
2728 width: units.gu(110)
28− height: units.gu(30)
29+ height: units.gu(50)
2930
3031 UT.UnityTestCase {
3132 name: "PageHeaderLabelTest"
@@ -54,7 +55,7 @@ Item {
5455 pageHeader.searchHistory.clear();
5556
5657 // Check initial state
57− tryCompareFunction(function() { return headerContainer.popover === null; }, true);
58+ tryCompare(pageHeader.extraPanel, "visible", false);
5859 compare(pageHeader.searchHistory.count, 0);
5960 }
6061
@@ -146,7 +147,7 @@ Item {
146147 doResetSearch();
147148 }
148149
149− function test_popover() {
150+ function test_extraPanel() {
150151 searchEnabled = true;
151152 pageHeader.searchHistory.clear();
152153
@@ -155,22 +156,18 @@ Item {
155156
156157 pageHeader.triggerSearch();
157158
158− var headerContainer = findChild(pageHeader, "headerContainer");
159− verify(headerContainer !== null, "headerContainer != null");
160− tryCompareFunction(function() { return headerContainer.popover !== null; }, true);
161−
162− tryCompare(headerContainer.popover, "visible", true);
159+ tryCompare(pageHeader.extraPanel, "visible", true);
163160
164161 var searchTextField = findChild(pageHeader, "searchTextField");
165162 compare(searchTextField.focus, true);
166163
167− var recentSearches = findChild(headerContainer.popover, "recentSearches");
168− verify(recentSearches, "Could not find recent searches in the popover");
164+ var recentSearches = findChild(pageHeader.extraPanel, "recentSearchesRepeater");
165+ verify(recentSearches, "Could not find recent searches");
169166 waitForRendering(recentSearches);
170167 mouseClick(recentSearches.itemAt(0));
171168
172169 compare(pageHeader.searchQuery, "Search2");
173− tryCompareFunction(function() { return headerContainer.popover === null; }, true);
170+ tryCompare(pageHeader.extraPanel, "visible", false);
174171 compare(searchTextField.focus, false);
175172 }
176173
@@ -212,9 +209,7 @@ Item {
212209 pageHeader.triggerSearch();
213210
214211 var headerContainer = findChild(pageHeader, "headerContainer");
215− verify(headerContainer !== null, "headerContainer != null");
216− tryCompareFunction(function() { return headerContainer.popover !== null; }, true);
217− compare(headerContainer.popover.visible, true);
212+ tryCompare(pageHeader.extraPanel, "visible", true);
218213
219214 pageHeader.searchQuery = data.searchText;
220215
@@ -225,7 +220,7 @@ Item {
225220 }
226221
227222 tryCompare(headerContainer, "showSearch", !data.hideSearch);
228− tryCompareFunction(function() { return headerContainer.popover === null; }, true);
223+ tryCompare(pageHeader.extraPanel, "visible", false);
229224
230225 doResetSearch();
231226 }
@@ -251,8 +246,10 @@ Item {
251246 right: parent.right
252247 }
253248
249+ searchHistory: SearchHistoryModel
254250 searchEntryEnabled: true
255251 title: "%^$%^%^&%^&%^$%GHR%"
252+ extraPanel: peExtraPanel
256253 scopeStyle: QtObject {
257254 readonly property color foreground: theme.palette.normal.baseText
258255 readonly property url headerLogo: showImageCheckBox.checked ? pageHeader.titleImageSource : ""
@@ -264,6 +261,18 @@ Item {
264261 onBackClicked: lastBackClicked = new Date()
265262 }
266263
264+ PageHeaderExtraPanel {
265+ id: peExtraPanel
266+ width: parent.width
267+ z: 1
268+ visible: false
269+ searchHistory: SearchHistoryModel
270+ onHistoryItemClicked: {
271+ SearchHistoryModel.addQuery(text);
272+ pageHeader.searchQuery = text;
273+ }
274+ }
275+
267276 Row {
268277 spacing: units.gu(1)
269278 anchors {
tests/qmltests/Dash/tst_GenericScopeView.qml+3−3
@@ -428,8 +428,8 @@ Item {
428428
429429 function test_header_style_data() {
430430 return [
431− { tag: "Default", index: 0, foreground: UbuntuColors.darkGrey, background: "color:///#f5f5f5", logo: "" },
432− { tag: "Foreground", index: 1, foreground: "yellow", background: "color:///#f5f5f5", logo: "" },
431+ { tag: "Default", index: 0, foreground: UbuntuColors.darkGrey, background: "color:///#ffffff", logo: "" },
432+ { tag: "Foreground", index: 1, foreground: "yellow", background: "color:///#ffffff", logo: "" },
433433 { tag: "Logo+Background", index: 2, foreground: UbuntuColors.darkGrey, background: "gradient:///lightgrey/grey",
434434 logo: Qt.resolvedUrl("../Dash/tst_PageHeader/logo-ubuntu-orange.svg") },
435435 ];
@@ -478,7 +478,7 @@ Item {
478478 mouseClick(seeAll0);
479479 verify(category0.expanded);
480480 tryCompare(category0, "height", category0.item.expandedHeight + seeAll0.height);
481− tryCompare(genericScopeView.categoryView, "contentY", units.gu(13));
481+ tryCompare(genericScopeView.categoryView, "contentY", units.gu(8));
482482
483483 scrollToEnd();
484484
tests/qmltests/Greeter/tst_Greeter.qml+7−1
@@ -328,9 +328,15 @@ Item {
328328 LightDM.Greeter.active = true;
329329
330330 greeter.forcedUnlock = true;
331− compare(view.locked, false);
331+ compare(greeter.required, false);
332332 greeter.forcedUnlock = false;
333333
334+ // Now recover from tearing down the view above
335+ LightDM.Greeter.showGreeter();
336+ tryCompare(greeter, "required", true);
337+ tryCompare(greeter, "waiting", false);
338+ view = findChild(greeter, "testView");
339+
334340 selectUser("no-password");
335341 tryCompare(view, "locked", false);
336342 selectUser("has-password");
tests/qmltests/Stages/tst_DesktopStage.qml−2
@@ -508,8 +508,6 @@ Item {
508508 }
509509
510510 function test_dropShadow() {
511− killAllRunningApps();
512−
513511 // verify the drop shadow is not visible initially
514512 verify(PanelState.dropShadow == false);
515513
tests/qmltests/Stages/tst_TabletStage.qml+143
@@ -20,6 +20,7 @@ import Ubuntu.Components 1.3
2020import Ubuntu.Components.ListItems 1.3 as ListItem
2121import Unity.Application 0.1
2222import Unity.Test 0.1
23+import Utils 0.1
2324
2425import ".."
2526import "../../../qml/Stages"
@@ -78,6 +79,7 @@ Rectangle {
7879 Column {
7980 anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
8081 spacing: units.gu(1)
82+
8183 EdgeBarrierControls {
8284 id: edgeBarrierControls
8385 text: "Drag here to pull out spread"
@@ -87,6 +89,16 @@ Rectangle {
8789 edgeBarrierControls.target = testCase.findChild(tabletStageLoader, "edgeBarrierController");
8890 }
8991 }
92+
93+ Button {
94+ text: testCase.sideStage ? testCase.sideStage.shown ? "Hide Side-stage" : "Show Side-stage" : ""
95+ enabled: testCase.sideStage
96+ onClicked: {
97+ if (testCase.sideStage.shown) testCase.sideStage.hide();
98+ else testCase.sideStage.show();
99+ }
100+ }
101+
90102 ApplicationCheckBox {
91103 id: webbrowserCheckBox
92104 appId: "webbrowser-app"
@@ -112,6 +124,7 @@ Rectangle {
112124 when: windowShown
113125
114126 property Item tabletStage: tabletStageLoader.status === Loader.Ready ? tabletStageLoader.item : null
127+ property Item sideStage: tabletStage ? findChild(tabletStage, "sideStage") : null
115128
116129 function init() {
117130 tabletStageLoader.active = true;
@@ -129,6 +142,7 @@ Rectangle {
129142 }
130143
131144 waitUntilAppSurfaceShowsUp("unity8-dash");
145+ sideStage.hideNow()
132146 }
133147
134148 function cleanup() {
@@ -148,6 +162,7 @@ Rectangle {
148162 galleryCheckBox.checked = false;
149163 dialerCheckBox.checked = false;
150164 facebookCheckBox.checked = false;
165+ WindowStateStorage.clear();
151166 }
152167
153168 function waitUntilAppSurfaceShowsUp(appId) {
@@ -187,6 +202,8 @@ Rectangle {
187202 }
188203
189204 function test_tappingSwitchesFocusBetweenStages() {
205+ WindowStateStorage.saveStage(dialerCheckBox.appId, ApplicationInfoInterface.SideStage)
206+
190207 webbrowserCheckBox.checked = true;
191208 waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId);
192209 var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
@@ -222,6 +239,8 @@ Rectangle {
222239 }
223240
224241 function test_closeAppInSideStage() {
242+ WindowStateStorage.saveStage(dialerCheckBox.appId, ApplicationInfoInterface.SideStage)
243+
225244 dialerCheckBox.checked = true;
226245 waitUntilAppSurfaceShowsUp(dialerCheckBox.appId);
227246
@@ -293,6 +312,8 @@ Rectangle {
293312
294313
295314 function test_foregroundMainAndSideStageAppsAreKeptRunning() {
315+ WindowStateStorage.saveStage(facebookCheckBox.appId, ApplicationInfoInterface.SideStage)
316+ WindowStateStorage.saveStage(dialerCheckBox.appId, ApplicationInfoInterface.SideStage)
296317
297318 var stagesPriv = findInvisibleChild(tabletStage, "stagesPriv");
298319 verify(stagesPriv);
@@ -351,6 +372,8 @@ Rectangle {
351372 }
352373
353374 function test_foregroundAppsAreSuspendedWhenStageIsSuspended() {
375+ WindowStateStorage.saveStage(dialerCheckBox.appId, ApplicationInfoInterface.SideStage)
376+
354377 webbrowserCheckBox.checked = true;
355378 waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId)
356379 var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
@@ -384,5 +407,125 @@ Rectangle {
384407 }
385408 tryCompare(spreadView, "phase", 2);
386409 }
410+
411+ function test_threeFingerTapOpensSideStage_data() {
412+ return [
413+ { tag: "1 finger", touchIds: [0], result: false },
414+ { tag: "2 finger", touchIds: [0, 1], result: false },
415+ { tag: "3 finger", touchIds: [0, 1, 2], result: true },
416+ { tag: "4 finger", touchIds: [0, 1, 2, 3], result: false },
417+ ];
418+ }
419+
420+ function test_threeFingerTapOpensSideStage(data) {
421+ multiTouchTap(data.touchIds, tabletStage, tabletStage.width / 2, tabletStage.height / 2);
422+ wait(200);
423+ tryCompare(sideStage, "shown", data.result);
424+ }
425+
426+
427+ function test_threeFingerTapClosesSideStage_data() {
428+ return [
429+ { tag: "1 finger", touchIds: [0], result: true },
430+ { tag: "2 finger", touchIds: [0, 1], result: true },
431+ { tag: "3 finger", touchIds: [0, 1, 2], result: false },
432+ { tag: "4 finger", touchIds: [0, 1, 2, 3], result: true },
433+ ];
434+ }
435+
436+ function test_threeFingerTapClosesSideStage(data) {
437+ sideStage.showNow();
438+
439+ multiTouchTap(data.touchIds, tabletStage, tabletStage.width / 2, tabletStage.height / 2);
440+ wait(200);
441+ tryCompare(sideStage, "shown", data.result);
442+ }
443+
444+ function test_threeFingerDragOpensSidestage() {
445+ multiTouchDragUntil([0,1,2],
446+ tabletStage,
447+ tabletStage.width / 4,
448+ tabletStage.height / 4,
449+ units.gu(1),
450+ 0,
451+ function() { return sideStage.shown; });
452+ }
453+
454+ function test_applicationLoadsInCorrectStage_data() {
455+ return [
456+ { tag: "MainStage", stage: ApplicationInfoInterface.MainStage, mainStageAppId: "webbrowser-app", sideStageAppId: ""},
457+ { tag: "SideStage", stage: ApplicationInfoInterface.SideStage, mainStageAppId: "unity8-dash", sideStageAppId: "webbrowser-app" },
458+ ];
459+ }
460+
461+ function test_applicationLoadsInCorrectStage(data) {
462+ WindowStateStorage.saveStage(webbrowserCheckBox.appId, data.stage)
463+
464+ var stagesPriv = findInvisibleChild(tabletStage, "stagesPriv");
465+ verify(stagesPriv);
466+
467+ tryCompare(stagesPriv, "mainStageAppId", "unity8-dash");
468+ tryCompare(stagesPriv, "sideStageAppId", "");
469+
470+ webbrowserCheckBox.checked = true;
471+ waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId)
472+
473+ tryCompare(stagesPriv, "mainStageAppId", data.mainStageAppId);
474+ tryCompare(stagesPriv, "sideStageAppId", data.sideStageAppId);
475+ }
476+
477+ function test_loadSideStageByDragginFromMainStage() {
478+ sideStage.showNow();
479+ webbrowserCheckBox.checked = true;
480+ waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId);
481+
482+ var appDelegate = findChild(tabletStage, "tabletSpreadDelegate_" + webbrowserCheckBox.appId);
483+ verify(appDelegate);
484+ compare(appDelegate.stage, ApplicationInfoInterface.MainStage);
485+
486+ var pos = tabletStage.width - sideStage.width - (tabletStage.width - sideStage.width) / 2;
487+ var end_pos = tabletStage.width - sideStage.width / 2;
488+
489+ multiTouchDragUntil([0,1,2],
490+ tabletStage,
491+ pos,
492+ tabletStage.height / 2,
493+ units.gu(3),
494+ 0,
495+ function() {
496+ pos += units.gu(3);
497+ return sideStage.shown && !sideStage.showAnimation.running &&
498+ pos >= end_pos;
499+ });
500+
501+ tryCompare(appDelegate, "stage", ApplicationInfoInterface.SideStage);
502+ }
503+
504+ function test_unloadSideStageByDragginFromStageStage() {
505+ sideStage.showNow();
506+ WindowStateStorage.saveStage(webbrowserCheckBox.appId, ApplicationInfoInterface.SideStage)
507+ webbrowserCheckBox.checked = true;
508+ waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId);
509+
510+ var appDelegate = findChild(tabletStage, "tabletSpreadDelegate_" + webbrowserCheckBox.appId);
511+ verify(appDelegate);
512+ compare(appDelegate.stage, ApplicationInfoInterface.SideStage);
513+
514+ var pos = tabletStage.width - sideStage.width / 2;
515+ var end_pos = tabletStage.width - sideStage.width - (tabletStage.width - sideStage.width) / 2;
516+
517+ multiTouchDragUntil([0,1,2],
518+ tabletStage,
519+ pos,
520+ tabletStage.height / 2,
521+ -units.gu(3),
522+ 0,
523+ function() {
524+ pos -= units.gu(3);
525+ return pos <= end_pos;
526+ });
527+
528+ tryCompare(appDelegate, "stage", ApplicationInfoInterface.MainStage);
529+ }
387530 }
388531}
tests/qmltests/Stages/tst_WindowResizeArea.qml+7
@@ -72,6 +72,13 @@ Item {
7272 windowId: "test-window-id"
7373 screenWidth: root.width
7474 screenHeight: root.height
75+
76+ Component.onCompleted: {
77+ loadWindowState();
78+ }
79+ Component.onDestruction: {
80+ saveWindowState();
81+ }
7582 }
7683
7784 Rectangle {
tests/qmltests/Tutorial/tst_Tutorial.qml+611−193
@@ -19,15 +19,20 @@ import QtTest 1.0
1919import AccountsService 0.1
2020import IntegratedLightDM 0.1 as LightDM
2121import Ubuntu.Components 1.3
22+import Ubuntu.Components.ListItems 1.3
23+import Ubuntu.Telephony 0.1 as Telephony
2224import Unity.Application 0.1
2325import Unity.Test 0.1 as UT
26+import Utils 0.1
2427
2528import "../../../qml"
29+import "../../../qml/Components"
2630
27−Item {
31+Rectangle {
2832 id: root
29− width: shellLoader.width + buttons.width
30− height: shellLoader.height
33+ color: UbuntuColors.lightGrey
34+ width: units.gu(100) + buttons.width
35+ height: units.gu(71)
3136
3237 QtObject {
3338 id: applicationArguments
@@ -45,6 +50,11 @@ Item {
4550 }
4651 }
4752
53+ Telephony.CallEntry {
54+ id: phoneCall
55+ phoneNumber: "+447812221111"
56+ }
57+
4858 Component.onCompleted: {
4959 // must set the mock mode before loading the Shell
5060 LightDM.Greeter.mockMode = "single-pin";
@@ -52,21 +62,63 @@ Item {
5262 shellLoader.active = true;
5363 }
5464
55− Row {
56− spacing: 0
57− anchors.fill: parent
65+ Item {
66+ id: shellContainer
67+ anchors.left: root.left
68+ anchors.right: buttons.left
69+ anchors.top: root.top
70+ anchors.bottom: root.bottom
5871
5972 Loader {
6073 id: shellLoader
6174
6275 active: false
63− width: units.gu(40)
64− height: units.gu(71)
76+ anchors.horizontalCenter: parent.horizontalCenter
77+ anchors.top: parent.top
78+ anchors.bottom: parent.bottom
79+
80+ property int shellOrientation: Qt.PortraitOrientation
81+ property int nativeOrientation: Qt.PortraitOrientation
82+ property int primaryOrientation: Qt.PortraitOrientation
83+
84+ state: modeSelector.model[modeSelector.selectedIndex]
85+ states: [
86+ State {
87+ name: "phone"
88+ PropertyChanges {
89+ target: shellLoader
90+ width: units.gu(40)
91+ }
92+ },
93+ State {
94+ name: "tablet"
95+ PropertyChanges {
96+ target: shellLoader
97+ width: units.gu(100)
98+ shellOrientation: Qt.LandscapeOrientation
99+ nativeOrientation: Qt.LandscapeOrientation
100+ primaryOrientation: Qt.LandscapeOrientation
101+ }
102+ },
103+ State {
104+ name: "desktop"
105+ PropertyChanges {
106+ target: shellLoader
107+ width: units.gu(100)
108+ }
109+ }
110+ ]
65111
66112 property bool itemDestroyed: false
67113 sourceComponent: Component {
68114 Shell {
115+ usageScenario: shellLoader.state
69116 property string indicatorProfile: "phone"
117+ orientation: shellLoader.shellOrientation
118+ orientations: Orientations {
119+ native_: shellLoader.nativeOrientation
120+ primary: shellLoader.primaryOrientation
121+ }
70122
71123 Component.onDestruction: {
72124 shellLoader.itemDestroyed = true;
@@ -74,29 +126,92 @@ Item {
74126 }
75127 }
76128 }
129+ }
130+
131+ Rectangle {
132+ id: buttons
133+ color: UbuntuColors.darkGrey
134+ width: units.gu(30)
135+ anchors.top: root.top
136+ anchors.bottom: root.bottom
137+ anchors.right: root.right
138+
139+ Column {
140+ anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
141+ spacing: units.gu(1)
142+ Row {
143+ anchors { left: parent.left; right: parent.right }
144+ Button {
145+ text: "Hide Greeter"
146+ onClicked: {
147+ if (shellLoader.status !== Loader.Ready)
148+ return;
149+
150+ var greeter = testCase.findChild(shellLoader.item, "greeter");
151+ if (greeter.shown) {
152+ greeter.hide();
153+ }
154+ }
155+ }
156+ }
157+
158+ Row {
159+ anchors { left: parent.left; right: parent.right }
160+ Button {
161+ text: "Restart Tutorial"
162+ onClicked: {
163+ if (shellLoader.status !== Loader.Ready)
164+ return;
165+
166+ AccountsService.demoEdges = false;
167+ AccountsService.demoEdgesCompleted = [];
168+ AccountsService.demoEdges = true;
169+ }
170+ }
171+ }
77172
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;
173+ Row {
174+ anchors { left: parent.left; right: parent.right }
175+ CheckBox {
176+ onCheckedChanged: {
177+ if (checked) {
178+ callManager.foregroundCall = phoneCall;
179+ } else {
180+ callManager.foregroundCall = null;
97181 }
98182 }
99183 }
184+ Label {
185+ text: "Active Call"
186+ anchors.verticalCenter: parent.verticalCenter
187+ }
188+ }
189+
190+ Row {
191+ anchors { left: parent.left; right: parent.right }
192+ CheckBox {
193+ activeFocusOnPress: false
194+ onCheckedChanged: {
195+ var surface = SurfaceManager.inputMethodSurface;
196+ if (checked) {
197+ surface.setState(Mir.RestoredState);
198+ } else {
199+ surface.setState(Mir.MinimizedState);
200+ }
201+ }
202+ }
203+ Label {
204+ text: "Input Method"
205+ anchors.verticalCenter: parent.verticalCenter
206+ }
207+ }
208+
209+ ItemSelector {
210+ id: modeSelector
211+ anchors { left: parent.left; right: parent.right }
212+ activeFocusOnPress: false
213+ text: "Mode"
214+ model: ["phone", "tablet", "desktop"]
100215 }
101216 }
102217 }
@@ -111,17 +226,21 @@ Item {
111226 property real halfHeight: shell ? shell.height / 2 : 0
112227
113228 function init() {
114− tryCompare(shell, "enabled", true); // enabled by greeter when ready
229+ prepareShell();
115230
116− AccountsService.demoEdges = false;
117− AccountsService.demoEdges = true;
118− swipeAwayGreeter();
231+ var tutorialLeft = findChild(shell, "tutorialLeft");
232+ tryCompare(tutorialLeft, "opacity", 1);
119233 }
120234
121235 function cleanup() {
236+ resetLoader("phone");
237+ }
238+
239+ function resetLoader(state) {
122240 shellLoader.itemDestroyed = false;
123241
124242 shellLoader.active = false;
243+ shellLoader.state = state;
125244
126245 tryCompare(shellLoader, "status", Loader.Null);
127246 tryCompare(shellLoader, "item", null);
@@ -143,6 +262,24 @@ Item {
143262 removeTimeConstraintsFromDirectionalDragAreas(shellLoader.item);
144263 }
145264
265+ function prepareShell() {
266+ tryCompare(shell, "enabled", true); // enabled by greeter when ready
267+
268+ WindowStateStorage.clear();
269+ SurfaceManager.inputMethodSurface.setState(Mir.MinimizedState);
270+ callManager.foregroundCall = null;
271+ AccountsService.demoEdges = false;
272+ AccountsService.demoEdgesCompleted = [];
273+ AccountsService.demoEdges = true;
274+
275+ LightDM.Greeter.hideGreeter();
276+ }
277+
278+ function loadShell(state) {
279+ resetLoader(state);
280+ prepareShell();
281+ }
282+
146283 function killApps() {
147284 while (ApplicationManager.count > 1) {
148285 var appIndex = ApplicationManager.get(0).appId == "unity8-dash" ? 1 : 0
@@ -163,234 +300,515 @@ Item {
163300 waitForRendering(greeter);
164301 }
165302
166− function waitForPage(name) {
167− waitForRendering(findChild(shell, name));
168− var page = findChild(shell, name);
169− tryCompare(page, "shown", true);
170− tryCompare(page.showAnimation, "running", false);
171− return page;
303+ function openTutorialTop() {
304+ var tutorialLeftLoader = findChild(shell, "tutorialLeftLoader");
305+ var tutorialTop = findChild(shell, "tutorialTop");
306+ var tutorialTopTimer = findInvisibleChild(tutorialTop, "tutorialTopTimer");
307+
308+ tutorialTopTimer.interval = 1;
309+ AccountsService.demoEdgesCompleted = ["left", "left-long"];
310+
311+ tryCompare(tutorialLeftLoader, "active", false);
312+ tryCompare(tutorialTop, "shown", true);
313+ tryCompare(tutorialTop, "opacity", 1);
172314 }
173315
174− function checkTopEdge() {
175− touchFlick(shell, halfWidth, 0, halfWidth, halfHeight);
316+ function openTutorialRight() {
317+ var tutorialLeftLoader = findChild(shell, "tutorialLeftLoader");
318+ var tutorialRight = findChild(shell, "tutorialRight");
176319
177− var panel = findChild(shell, "panel");
178− tryCompare(panel.indicators, "fullyClosed", true);
320+ AccountsService.demoEdgesCompleted = ["left", "left-long", "top"];
321+ ApplicationManager.startApplication("gallery-app");
322+ ApplicationManager.startApplication("facebook-webapp");
323+
324+ tryCompare(tutorialLeftLoader, "active", false);
325+ tryCompare(tutorialRight, "shown", true);
326+ tryCompare(tutorialRight, "opacity", 1);
179327 }
180328
181− function checkLeftEdge() {
182− touchFlick(shell, 0, halfHeight, halfWidth, halfHeight);
329+ function openTutorialBottom() {
330+ var tutorialLeftLoader = findChild(shell, "tutorialLeftLoader");
331+ var tutorialBottom = findChild(shell, "tutorialBottom");
332+
333+ AccountsService.demoEdgesCompleted = ["left", "left-long", "top", "right"];
334+ tryCompare(tutorialLeftLoader, "active", false);
183335
336+ ApplicationManager.startApplication("dialer-app");
337+ ApplicationManager.requestFocusApplication("dialer-app");
338+
339+ tryCompare(tutorialBottom, "shown", true);
340+ tryCompare(tutorialBottom, "opacity", 1);
341+ }
342+
343+ function test_tutorialLeftEdges() {
344+ var tutorial = findChild(shell, "tutorial");
345+ var tutorialLeft = findChild(tutorial, "tutorialLeft");
184346 var launcher = findChild(shell, "launcher");
185− tryCompare(launcher, "state", "");
347+ var stage = findChild(shell, "stage");
348+ var panel = findChild(shell, "panel");
349+
350+ verify(tutorial.running);
351+ verify(tutorial.launcherEnabled);
352+ verify(!tutorial.spreadEnabled);
353+ verify(!tutorial.panelEnabled);
354+ verify(tutorialLeft.shown);
355+ verify(launcher.available);
356+ verify(!stage.spreadEnabled);
357+ verify(!panel.indicators.available);
186358 }
187359
188− function checkRightEdge() {
189− if (shell.usageScenario === "phone") {
190− touchFlick(shell, shell.width, halfHeight, halfWidth, halfHeight);
360+ function test_tutorialLeftFinish() {
361+ var tutorial = findChild(shell, "tutorial");
362+ var tutorialLeft = findChild(tutorial, "tutorialLeft");
363+ var launcher = findChild(shell, "launcher");
191364
192− var stage = findChild(shell, "stage");
193− var spreadView = findChild(stage, "spreadView");
194− tryCompare(spreadView, "phase", 0);
195− }
365+ touchFlick(shell, 0, halfHeight, halfWidth, halfHeight);
366+
367+ tryCompare(tutorialLeft, "shown", false);
368+ tryCompare(AccountsService, "demoEdgesCompleted", ["left"]);
369+ tryCompare(launcher, "state", "visible");
196370 }
197371
198− function checkBottomEdge() {
199− // Can't actually check effect of swipe, since dash isn't really loaded
200− var applicationsDisplayLoader = findChild(shell, "applicationsDisplayLoader");
201− tryCompare(applicationsDisplayLoader, "interactive", false);
372+ function test_tutorialLeftShortDrag() {
373+ // Here we want to test just barely pulling the launcher out and
374+ // letting go (i.e. not triggering the "progress" property of
375+ // Launcher).
376+ var tutorialLeft = findChild(shell, "tutorialLeft");
377+ var launcher = findChild(shell, "launcher");
378+
379+ // Confirm fade during drag
380+ touchFlick(shell, 0, halfHeight, launcher.panelWidth * 0.4, halfHeight, true, false);
381+ // compare opacity with a bound rather than hard 0.6 because progress doesn't
382+ // always match the drag perfectly (takes a moment for drag to kick in)
383+ tryCompareFunction(function() {
384+ return tutorialLeft.opacity >= 0.6 && tutorialLeft.opacity < 0.8;
385+ }, true);
386+ touchFlick(shell, 0, halfHeight, launcher.panelWidth * 0.4, halfHeight, false, true);
387+
388+
389+ // Make sure we don't do anything if we don't pull the launcher
390+ // out much.
391+ touchFlick(shell, 0, halfHeight, launcher.panelWidth * 0.4, halfHeight);
392+ tryCompare(launcher, "state", ""); // should remain hidden
393+ tryCompare(tutorialLeft, "shown", true); // and we should still be on left
394+
395+
396+ // Now drag out but not past launcher itself
397+ touchFlick(shell, 0, halfHeight, launcher.panelWidth * 0.9, halfHeight);
398+
399+ tryCompare(tutorialLeft, "shown", false);
400+ tryCompare(AccountsService, "demoEdgesCompleted", ["left"]);
401+ tryCompare(launcher, "state", "visible");
202402 }
203403
204− function checkFinished() {
205− tryCompare(AccountsService, "demoEdges", false);
404+ function test_tutorialLeftLongDrag() {
405+ // Just confirm that a long drag ("dash" drag) doesn't confuse us
206406
207− var tutorial = findChild(shell, "tutorial");
208− tryCompare(tutorial, "running", false);
407+ // So that we actually switch to dash and launcher hides
408+ ApplicationManager.startApplication("gallery-app");
209409
210410 var launcher = findChild(shell, "launcher");
211− tryCompare(launcher, "shown", false);
411+ var tutorialLeft = findChild(shell, "tutorialLeft");
412+ touchFlick(shell, 0, halfHeight, shell.width, halfHeight);
413+
414+ tryCompare(tutorialLeft, "shown", false);
415+ tryCompare(AccountsService, "demoEdgesCompleted", ["left", "left-long"]);
212416 }
213417
214− function goToPage(name) {
215− var page = waitForPage("tutorialLeft");
216− checkTopEdge();
217− checkRightEdge();
218− checkBottomEdge();
219− if (name === "tutorialLeft") return page;
418+ function test_tutorialLeftAutoSkipped() {
419+ // Test that we skip the tutorial if user uses left edge themselves
420+
421+ var tutorialLeft = findChild(shell, "tutorialLeft");
422+ LightDM.Greeter.showGreeter();
423+ tryCompare(tutorialLeft, "visible", false);
424+ compare(AccountsService.demoEdgesCompleted, []);
425+
220426 touchFlick(shell, 0, halfHeight, halfWidth, halfHeight);
427+ tryCompare(AccountsService, "demoEdgesCompleted", ["left"]);
428+ }
221429
222− page = waitForPage("tutorialLeftFinish");
223− if (name === "tutorialLeftFinish") return page;
224− var tick = findChild(page, "tick");
225− tap(tick);
226−
227− page = waitForPage("tutorialRight");
228− checkTopEdge();
229− checkLeftEdge();
230− checkBottomEdge();
231− if (name === "tutorialRight") return page;
232− touchFlick(shell,
233− shell.width, halfHeight,
234− halfWidth, halfHeight,
235− true /* beginTouch */, true /* endTouch */,
236− 20 /* speed */, 50 /* iterations */);
237− var overlay = findChild(page, "overlay");
238− tryCompare(overlay, "shown", true);
239− var tick = findChild(page, "tick");
240− tap(tick);
241−
242− var page = waitForPage("tutorialBottom");
243− checkTopEdge();
244− checkLeftEdge();
245− checkRightEdge();
246− if (name === "tutorialBottom") return page;
247− touchFlick(shell, halfWidth, shell.height, halfWidth, halfHeight);
430+ function test_tutorialTopEdges() {
431+ var tutorial = findChild(shell, "tutorial");
432+ var tutorialTop = findChild(tutorial, "tutorialTop");
433+ var launcher = findChild(shell, "launcher");
434+ var stage = findChild(shell, "stage");
435+ var panel = findChild(shell, "panel");
248436
249− var page = waitForPage("tutorialBottomFinish");
250− checkTopEdge();
251− checkLeftEdge();
252− checkRightEdge();
253− checkBottomEdge();
254− if (name === "tutorialBottomFinish") return page;
255− var tick = findChild(page, "tick");
256− tap(tick);
437+ openTutorialTop();
257438
258− checkFinished();
259− return null;
439+ tryCompare(tutorial, "running", true);
440+ verify(!tutorial.launcherEnabled);
441+ verify(!tutorial.spreadEnabled);
442+ verify(tutorial.panelEnabled);
443+ verify(tutorialTop.shown);
444+ verify(!launcher.available);
445+ verify(!stage.spreadEnabled);
446+ verify(panel.indicators.available);
260447 }
261448
262− function test_walkthrough() {
263− goToPage(null);
449+ function test_tutorialTopFinish() {
450+ var tutorial = findChild(shell, "tutorial");
451+ var tutorialTop = findChild(tutorial, "tutorialTop");
452+ var panel = findChild(shell, "panel");
453+
454+ openTutorialTop();
455+ touchFlick(shell, halfWidth, 0, halfWidth, shell.height);
456+
457+ tryCompare(tutorialTop, "shown", false);
458+ tryCompare(AccountsService, "demoEdgesCompleted", ["left", "left-long", "top"]);
459+ tryCompare(panel.indicators, "fullyOpened", true);
264460 }
265461
266− function test_skipOnDesktop() {
462+ function test_tutorialTopShortDrag() {
267463 var tutorial = findChild(shell, "tutorial");
268− tryCompare(tutorial, "active", true);
269− tryCompare(tutorial, "running", true);
464+ var tutorialTop = findChild(tutorial, "tutorialTop");
465+ var panel = findChild(shell, "panel");
270466
271− shell.usageScenario = "desktop";
272− tryCompare(tutorial, "active", false);
273− tryCompare(tutorial, "running", false);
467+ openTutorialTop();
468+ touchFlick(shell, halfWidth, 0, halfWidth, shell.height * 0.4, true, false);
469+ // compare opacity with a bound rather than hard 0.6 because progress doesn't
470+ // always match the drag perfectly (takes a moment for drag to kick in)
471+ tryCompareFunction(function() {
472+ return tutorialTop.opacity >= 0.6 && tutorialTop.opacity < 0.7;
473+ }, true);
474+ touchFlick(shell, halfWidth, 0, halfWidth, shell.height * 0.4, false, true);
475+
476+ compare(tutorialTop.shown, true);
274477 }
275478
276− function test_launcherShortDrag() {
277− // goToPage does a normal launcher pull. But here we want to test
278− // just barely pulling the launcher out and letting go (i.e. not
279− // triggering the "progress" property of Launcher).
479+ function test_tutorialTopAutoSkipped() {
480+ // Test that we skip the tutorial if user uses top edge themselves
280481
281− var left = goToPage("tutorialLeft");
482+ var tutorialLeftLoader = findChild(shell, "tutorialLeftLoader");
483+ var tutorialTop = findChild(shell, "tutorialTop");
484+ AccountsService.demoEdgesCompleted = ["left", "left-long"];
485+ tryCompare(tutorialLeftLoader, "active", false);
486+ verify(!tutorialTop.shown);
282487
283− // Make sure we don't do anything if we don't pull the launcher
284− // out much.
488+ touchFlick(shell, halfWidth, 0, halfWidth, shell.height);
489+ tryCompare(AccountsService, "demoEdgesCompleted", ["left", "left-long", "top"]);
490+ }
491+
492+ function test_tutorialRightEdges() {
493+ var tutorial = findChild(shell, "tutorial");
494+ var tutorialRight = findChild(tutorial, "tutorialRight");
285495 var launcher = findChild(shell, "launcher");
286− touchFlick(shell, 0, halfHeight, launcher.panelWidth * 0.4, halfHeight);
287− tryCompare(launcher, "state", ""); // should remain hidden
288− tryCompare(left, "shown", true); // and we should still be on left
496+ var stage = findChild(shell, "stage");
497+ var panel = findChild(shell, "panel");
289498
290− // Now drag out but not past launcher itself
291− touchFlick(shell, 0, halfHeight, launcher.panelWidth * 0.9, halfHeight);
499+ openTutorialRight();
500+
501+ tryCompare(tutorial, "running", true);
502+ verify(!tutorial.launcherEnabled);
503+ verify(tutorial.spreadEnabled);
504+ verify(!tutorial.panelEnabled);
505+ verify(tutorialRight.shown);
506+ verify(!launcher.available);
507+ verify(stage.spreadEnabled);
508+ verify(!panel.indicators.available);
509+ }
510+
511+ function test_tutorialRightFinish() {
512+ var tutorial = findChild(shell, "tutorial");
513+ var tutorialRight = findChild(tutorial, "tutorialRight");
514+ var stage = findChild(shell, "stage");
515+
516+ openTutorialRight();
517+ touchFlick(shell, shell.width, halfHeight, 0, halfHeight);
518+
519+ tryCompare(tutorialRight, "shown", false);
520+ tryCompare(AccountsService, "demoEdgesCompleted", ["left", "left-long", "top", "right"]);
521+ }
522+
523+ function test_tutorialRightShortDrag() {
524+ var tutorial = findChild(shell, "tutorial");
525+ var tutorialRight = findChild(tutorial, "tutorialRight");
526+ var stage = findChild(shell, "stage");
527+
528+ openTutorialRight();
529+ touchFlick(shell, shell.width, halfHeight, shell.width * 0.8, halfHeight, true, false);
530+ // compare opacity with a bound rather than hard 0.6 because progress doesn't
531+ // always match the drag perfectly (takes a moment for drag to kick in)
532+ tryCompareFunction(function() {
533+ return tutorialRight.opacity >= 0.6 && tutorialRight.opacity < 0.8;
534+ }, true);
535+ touchFlick(shell, shell.width, halfHeight, shell.width * 0.8, halfHeight, false, true);
536+
537+ compare(tutorialRight.shown, true);
538+ }
292539
293− waitForPage("tutorialLeftFinish");
540+ function test_tutorialRightDelay() {
541+ // Test that if we exit the top tutorial, we don't immediately
542+ // jump into right tutorial.
543+ var tutorialRight = findChild(shell, "tutorialRight");
544+ var tutorialRightTimer = findInvisibleChild(tutorialRight, "tutorialRightInactivityTimer");
545+
546+ tutorialRightTimer.interval = 1;
547+ openTutorialTop();
548+ ApplicationManager.startApplication("gallery-app");
549+ ApplicationManager.startApplication("facebook-webapp");
550+ tryCompare(ApplicationManager, "count", 3);
551+
552+ AccountsService.demoEdgesCompleted = ["left", "left-long", "top"];
553+ verify(tutorialRightTimer.running, true);
554+ verify(!tutorialRight.shown);
555+ tryCompare(tutorialRight, "shown", true);
294556 }
295557
296− function test_launcherLongDrag() {
297− // goToPage does a normal launcher pull. But here we want to test
298− // a full pull across the page.
558+ function test_tutorialRightAutoSkipped() {
559+ // Test that we skip the tutorial if user uses right edge themselves
299560
300− var left = goToPage("tutorialLeft");
561+ var tutorialLeftLoader = findChild(shell, "tutorialLeftLoader");
562+ AccountsService.demoEdgesCompleted = ["left", "left-long"];
563+ tryCompare(tutorialLeftLoader, "active", false);
301564
565+ touchFlick(shell, shell.width, halfHeight, 0, halfHeight);
566+ tryCompare(AccountsService, "demoEdgesCompleted", ["left", "left-long", "right"]);
567+ }
568+
569+ function test_tutorialBottomEdges() {
570+ var tutorial = findChild(shell, "tutorial");
571+ var tutorialBottom = findChild(tutorial, "tutorialBottom");
572+ var tutorialLabel = findChild(tutorialBottom, "tutorialLabel");
302573 var launcher = findChild(shell, "launcher");
303− touchFlick(shell, 0, halfHeight, shell.width, halfHeight);
574+ var stage = findChild(shell, "stage");
575+ var panel = findChild(shell, "panel");
304576
305− var errorTextLabel = findChild(left, "errorTextLabel");
306− var errorTitleLabel = findChild(left, "errorTitleLabel");
307− tryCompare(launcher, "state", ""); // launcher goes away
308− tryCompare(left, "shown", true); // still on left page
309− tryCompare(errorTextLabel, "opacity", 1); // show error
310− tryCompare(errorTitleLabel, "opacity", 1); // show error
577+ openTutorialBottom();
578+
579+ tryCompare(tutorial, "running", true);
580+ verify(!tutorial.launcherEnabled);
581+ verify(!tutorial.spreadEnabled);
582+ verify(!tutorial.panelEnabled);
583+ verify(tutorialBottom.shown);
584+ verify(!launcher.available);
585+ verify(!stage.spreadEnabled);
586+ verify(!panel.indicators.available);
587+ compare(tutorialLabel.text, "Swipe up for recent calls");
311588 }
312589
313− function test_launcherDragBack() {
314− // goToPage does a full launcher pull. But here we test pulling
315− // all the way out, then dragging back into place.
590+ function test_tutorialBottomFinish() {
591+ var tutorial = findChild(shell, "tutorial");
592+ var tutorialBottom = findChild(tutorial, "tutorialBottom");
316593
317− var left = goToPage("tutorialLeft");
318− touchFlick(shell, 0, halfHeight, halfWidth, halfHeight, true, false);
319− touchFlick(shell, halfWidth, halfHeight, 0, halfHeight, false, true);
594+ openTutorialBottom();
595+ touchFlick(shell, halfWidth, shell.height, halfWidth, halfHeight);
320596
321− tryCompare(left, "shown", true); // and we should still be on left
597+ tryCompare(tutorialBottom, "shown", false);
598+ tryCompare(AccountsService, "demoEdgesCompleted", ["left", "left-long", "top", "right", "bottom-dialer-app"]);
599+
600+ // OK, we did one, just confirm that when all are done, we mark whole tutorial as done.
601+ verify(AccountsService.demoEdges);
602+ AccountsService.demoEdgesCompleted = ["left", "left-long", "top", "right",
603+ "bottom-address-book-app",
604+ "bottom-com.ubuntu.calculator_calculator",
605+ "bottom-dialer-app",
606+ "bottom-messaging-app"];
607+ verify(!AccountsService.demoEdges);
322608 }
323609
324− function test_launcherNoDragGap() {
325− // See bug 1454882, where if you dragged the launcher while it was
326− // visible, you could pull it further than the edge of the screen.
610+ function test_tutorialBottomAppearsBeforeRight() {
611+ // Confirm that if bottom edge and right edge would appear on the
612+ // the same app open, bottom edge appears first. (this is a lightly
613+ // edited version of openTutorialRight)
614+ var tutorialLeftLoader = findChild(shell, "tutorialLeftLoader");
615+ var tutorialRight = findChild(shell, "tutorialRight");
616+ var tutorialBottom = findChild(shell, "tutorialBottom");
327617
328− var left = goToPage("tutorialLeft");
329− var launcher = findChild(shell, "launcher");
330− var teaseAnimation = findInvisibleChild(left, "teaseAnimation");
618+ AccountsService.demoEdgesCompleted = ["left", "left-long", "top"];
619+ ApplicationManager.startApplication("gallery-app");
620+ ApplicationManager.startApplication("dialer-app");
621+
622+ tryCompare(tutorialLeftLoader, "active", false);
623+ tryCompare(tutorialBottom, "shown", true);
624+ tryCompare(tutorialBottom, "opacity", 1);
625+ tryCompare(tutorialRight, "visible", false);
626+ }
331627
332− // Wait for launcher to be really out there
333− tryCompareFunction(function() {return launcher.x > teaseAnimation.maxBounce/2}, true);
334− verify(teaseAnimation.running);
628+ function test_tutorialBottomOnlyCoversSideStageOnTablet() {
629+ loadShell("tablet");
335630
336− // Start a drag, make sure animation stops
337− touchFlick(shell, 0, halfHeight, units.gu(4), halfHeight, true, false);
338− verify(!teaseAnimation.running);
339− verify(launcher.visibleWidth > 0);
340− verify(launcher.x > 0);
341− compare(launcher.x, teaseAnimation.bounce);
631+ var tutorialBottom = findChild(shell, "tutorialBottom");
632+ var targetHeight = shell.height - units.gu(4);
633+ var mainStageX = units.gu(20);
634+ var sideStageX = shell.width - units.gu(20);
342635
343− // Continue drag, make sure we don't create a gap on the left hand side
344− touchFlick(shell, units.gu(4), halfHeight, shell.width, halfHeight, false, false);
345− verify(!teaseAnimation.running);
346− compare(launcher.visibleWidth, launcher.panelWidth);
347− compare(launcher.x, 0);
636+ WindowStateStorage.saveStage("dialer-app", ApplicationInfoInterface.SideStage);
637+ openTutorialBottom();
348638
349− // Finish and make sure we continue animation
350− touchFlick(shell, shell.width, halfHeight, shell.width, halfHeight, false, true);
351− tryCompare(teaseAnimation, "running", true);
639+ touchFlick(shell, mainStageX, shell.height, mainStageX, targetHeight, true, false);
640+ compare(tutorialBottom.opacity, 1);
641+ touchFlick(shell, mainStageX, shell.height, mainStageX, targetHeight, false, true);
642+
643+ touchFlick(shell, sideStageX, shell.height, sideStageX, targetHeight, true, false);
644+ verify(tutorialBottom.opacity < 1);
645+ touchFlick(shell, sideStageX, shell.height, sideStageX, targetHeight, false, true);
352646 }
353647
354− function test_spread() {
355− // Unfortunately, most of what we want to test of the spread is
356− // "did it render correctly?" but that's hard to test. So instead,
357− // just poke and prod it a little bit to see if some of the values
358− // we'd expect to be correct, are so.
648+ function test_tutorialBottomOnlyCoversMainStageOnTablet() {
649+ loadShell("tablet");
359650
360− var right = goToPage("tutorialRight");
361− var stage = findChild(right, "stage");
362− var delegate0 = findChild(right, "appDelegate0");
651+ var tutorialBottom = findChild(shell, "tutorialBottom");
652+ var targetHeight = shell.height - units.gu(4);
653+ var mainStageX = units.gu(20);
654+ var sideStageX = shell.width - units.gu(20);
363655
364− tryCompare(stage, "dragProgress", 0);
365− touchFlick(shell, shell.width, halfHeight, shell.width * 0.8, halfHeight, true, false);
366− verify(stage.dragProgress > 0);
367− compare(stage.dragProgress, -delegate0.xTranslate);
368− touchFlick(shell, shell.width * 0.8, halfHeight, shell.width, halfHeight, false, true);
369− tryCompare(stage, "dragProgress", 0);
656+ WindowStateStorage.saveStage("notes-app", ApplicationInfoInterface.SideStage);
657+ ApplicationManager.startApplication("notes-app");
370658
371− tryCompare(delegate0, "x", shell.width);
659+ openTutorialBottom();
372660
373− var screenshotImage = findChild(right, "screenshotImage");
374− tryCompare(screenshotImage, "source", Qt.resolvedUrl("../../../qml/Tutorial/graphics/facebook.png"));
375− tryCompare(screenshotImage, "visible", true);
661+ touchFlick(shell, sideStageX, shell.height, sideStageX, targetHeight, true, false);
662+ compare(tutorialBottom.opacity, 1);
663+ touchFlick(shell, sideStageX, shell.height, sideStageX, targetHeight, false, true);
664+
665+ touchFlick(shell, mainStageX, shell.height, mainStageX, targetHeight, true, false);
666+ verify(tutorialBottom.opacity < 1);
667+ touchFlick(shell, mainStageX, shell.height, mainStageX, targetHeight, false, true);
376668 }
377669
378− function test_bottomShortDrag() {
379− var bottom = goToPage("tutorialBottom");
670+ function test_activeCallInterruptsTutorial() {
671+ var tutorialLeft = findChild(shell, "tutorialLeft");
672+ verify(tutorialLeft.shown);
673+ verify(!tutorialLeft.paused);
380674
381− touchFlick(shell, halfWidth, shell.height, halfWidth, shell.height * 0.8);
675+ callManager.foregroundCall = phoneCall;
676+ verify(!tutorialLeft.shown);
677+ verify(tutorialLeft.paused);
678+ tryCompare(tutorialLeft, "visible", false);
382679
383− var errorTextLabel = findChild(bottom, "errorTextLabel");
384− var errorTitleLabel = findChild(bottom, "errorTitleLabel");
385− tryCompare(bottom, "shown", true); // still on bottom page
386− tryCompare(errorTextLabel, "opacity", 1); // show error
387− tryCompare(errorTitleLabel, "opacity", 1); // show error
680+ callManager.foregroundCall = null;
681+ tryCompare(tutorialLeft, "shown", true);
682+ verify(!tutorialLeft.paused);
388683 }
389684
390− function test_interrupted() {
391− goToPage("tutorialLeft");
685+ function test_dialerInterruptionWithNoOverlappingTutorials() {
686+ var tutorialLeft = findChild(shell, "tutorialLeft");
687+ var tutorialBottom = findChild(shell, "tutorialBottom");
688+ verify(tutorialLeft.shown);
689+
690+ // Start call, hiding left tutorial
691+ callManager.foregroundCall = phoneCall;
692+ tryCompare(tutorialLeft, "shown", false);
693+
694+ // Start dialer app
695+ ApplicationManager.startApplication("dialer-app");
696+ tryCompare(ApplicationManager, "focusedApplicationId", "dialer-app");
697+ verify(!tutorialBottom.shown);
698+
699+ // Dismiss call, bottom tutorial should appear
700+ callManager.foregroundCall = null;
701+ tryCompare(tutorialBottom, "shown", true);
702+ verify(!tutorialLeft.shown);
703+
704+ // Get rid of bottom tutorial, left should now be shown
705+ touchFlick(shell, halfWidth, shell.height, halfWidth, halfHeight);
706+ tryCompare(tutorialBottom, "shown", false);
707+ tryCompare(tutorialLeft, "shown", true);
708+ }
709+
710+ function test_greeterInterruptsTutorial() {
711+ var tutorialLeft = findChild(shell, "tutorialLeft");
712+ verify(tutorialLeft.shown);
713+ verify(!tutorialLeft.paused);
714+
715+ LightDM.Greeter.showGreeter();
716+ verify(!tutorialLeft.shown);
717+ verify(tutorialLeft.paused);
718+ tryCompare(tutorialLeft, "visible", false);
719+
720+ LightDM.Greeter.hideGreeter();
721+ tryCompare(tutorialLeft, "shown", true);
722+ verify(!tutorialLeft.paused);
723+ }
724+
725+ function test_interruptionChecksReadyStateWhenDone() {
726+ // If we're done with an interruption (like active call), make sure
727+ // that we don't blindly resume the tutorial -- our trigger
728+ // conditions still need to be met. For example, there need to be
729+ // enough apps open for the right edge tutorial.
730+
731+ openTutorialRight();
732+
733+ var tutorialRight = findChild(shell, "tutorialRight");
734+ verify(tutorialRight.isReady);
735+ verify(tutorialRight.shown);
736+ verify(!tutorialRight.paused);
737+
738+ callManager.foregroundCall = phoneCall;
739+ killApps();
740+ callManager.foregroundCall = null;
741+
742+ verify(!tutorialRight.isReady);
743+ verify(!tutorialRight.shown);
744+ verify(!tutorialRight.paused);
745+ compare(AccountsService.demoEdgesCompleted, ["left", "left-long", "top"]);
746+ }
747+
748+ function test_desktopOnlyShowsTutorialRight() {
749+ loadShell("desktop");
750+
751+ var tutorialLeftLoader = findChild(shell, "tutorialLeftLoader");
752+ var tutorialTopLoader = findChild(shell, "tutorialTopLoader");
753+ var tutorialRightLoader = findChild(shell, "tutorialRightLoader");
754+ var tutorialBottomLoader = findChild(shell, "tutorialBottomLoader");
755+ verify(!tutorialLeftLoader.active);
756+ verify(!tutorialTopLoader.active);
757+ verify(tutorialRightLoader.active);
758+ verify(!tutorialBottomLoader.active);
759+ compare(AccountsService.demoEdgesCompleted, []);
760+
392761 ApplicationManager.startApplication("dialer-app");
393− checkFinished();
762+ ApplicationManager.startApplication("camera-app");
763+ tryCompare(tutorialRightLoader.item, "isReady", true);
764+ tryCompare(tutorialRightLoader, "shown", true);
765+ }
766+
767+ function test_oskDoesNotHideTutorial() {
768+ var tutorialLeft = findChild(shell, "tutorialLeft");
769+ verify(tutorialLeft.shown);
770+
771+ var surface = SurfaceManager.inputMethodSurface;
772+ surface.setState(Mir.RestoredState);
773+
774+ var inputMethod = findInvisibleChild(shell, "inputMethod");
775+ tryCompare(inputMethod, "state", "shown");
776+
777+ verify(tutorialLeft.shown);
778+ }
779+
780+ function test_oskPreventsTutorial() {
781+ var surface = SurfaceManager.inputMethodSurface;
782+ var inputMethod = findInvisibleChild(shell, "inputMethod");
783+
784+ AccountsService.demoEdges = false;
785+ surface.setState(Mir.RestoredState);
786+ tryCompare(inputMethod, "state", "shown");
787+
788+ var tutorial = findChild(shell, "tutorial");
789+ tryCompare(tutorial, "keyboardVisible", true);
790+
791+ AccountsService.demoEdges = true;
792+ var tutorialLeft = findChild(shell, "tutorialLeft");
793+ verify(!tutorialLeft.shown);
794+
795+ surface.setState(Mir.MinimizedState);
796+ tryCompare(inputMethod, "state", "hidden");
797+ tryCompare(tutorialLeft, "shown", false);
798+ }
799+
800+ function test_accountsServiceSettings() {
801+ var tutorialLeft = findChild(shell, "tutorialLeft");
802+ verify(tutorialLeft != null);
803+ verify(tutorialLeft.shown);
804+
805+ AccountsService.demoEdges = false;
806+ verify(findChild(shell, "tutorialLeft") == null);
807+
808+ AccountsService.demoEdges = true;
809+ tutorialLeft = findChild(shell, "tutorialLeft");
810+ verify(tutorialLeft != null);
811+ tryCompare(tutorialLeft, "shown", true);
394812 }
395813 }
396814}
tests/qmltests/Wizard/tst_Wizard.qml+126−214

This diff is too big to show here.

tests/qmltests/tst_OrientedShell.qml+126−15

This diff is too big to show here.

tests/qmltests/tst_Shell.qml+125−8

This diff is too big to show here.

tests/qmltests/tst_ShellWithPin.qml−22

This diff is too big to show here.

tests/utils/modules/Unity/Test/UnityTestCase.qml+67−11

This diff is too big to show here.