on xenial help sign in
merge trunk
commitc74d874a625bfab46678327a86da1c271c482810 authorMichael Zanetti <michael.zanetti@canonical.com> dateFri 19 Sep 2014, 13:27 (12 years ago) parentaa536adc712c92
100 files changed+2156−1474
cmake/modules/QmlTest.cmake+15−3 data/unity8-dash.desktop.in+3−3 data/unity8-filewatcher.conf+8 data/unity8.desktop.in+1−1 debian/changelog+60 debian/control+3−3 debian/unity8.install+1 plugins/Dash/CardCreator.js+2−2 plugins/Dash/listviewwithpageheader.cpp+11−1 plugins/Dash/listviewwithpageheader.h+2 plugins/Lights/Lights.cpp+1−2 plugins/Lights/android-hardware.h+23 plugins/Ubuntu/Payments/Payments.cpp+21−1 plugins/Ubuntu/Payments/Payments.h+4 plugins/Unity/Launcher/CMakeLists.txt+5−3 plugins/Unity/Launcher/backend/launcherbackend.cpp−591 plugins/Unity/Launcher/backend/launcherbackend.h−196 plugins/Unity/Launcher/dbusinterface.cpp+232 plugins/Unity/Launcher/dbusinterface.h+50 plugins/Unity/Launcher/desktopfilehandler.cpp+151 plugins/Unity/Launcher/desktopfilehandler.h+56 plugins/Unity/Launcher/gsettings.cpp+66 plugins/Unity/Launcher/gsettings.h+37 plugins/Unity/Launcher/launcheritem.cpp+30 plugins/Unity/Launcher/launcheritem.h+19−20 plugins/Unity/Launcher/launchermodel.cpp+85−15 plugins/Unity/Launcher/launchermodel.h+15−8 plugins/Unity/Launcher/common/quicklistentry.cpp → plugins/Unity/Launcher/quicklistentry.cpp plugins/Unity/Launcher/common/quicklistentry.h → plugins/Unity/Launcher/quicklistentry.h plugins/Unity/Launcher/quicklistmodel.h+1−1 plugins/Unity/Session/CMakeLists.txt+7 plugins/Unity/Session/orientationlock.cpp+74 plugins/Unity/Session/orientationlock.h+62 plugins/Unity/Session/plugin.cpp+8−4 plugins/Utils/windowkeysfilter.cpp+1 po/ja.po+15−15 po/unity8.pot+33−29 qml/Components/Dialogs.qml+10 qml/Dash/CardCarousel.qml+1−1 qml/Dash/CardGrid.qml+1−1 qml/Dash/CardHorizontalList.qml+1−1 qml/Dash/CardVerticalJournal.qml+1−1 qml/Dash/DashApplication.qml+2−2 qml/Dash/DashNavigation.qml+2−3 qml/Dash/DashRenderer.qml+3−2 qml/Dash/GenericScopeView.qml+85−35 qml/Dash/PageHeader.qml+4−3 qml/Dash/Previews/PreviewPayments.qml+6 qml/Dash/ScopesOverview.qml+1−1 qml/Launcher/Launcher.qml+1−2 qml/Launcher/LauncherDelegate.qml+4−2 qml/Launcher/LauncherPanel.qml+1 qml/Notifications/Notification.qml+2−2 qml/Shell.qml+33−1 qml/Stages/ApplicationWindow.qml+2 qml/Stages/PhoneStage.qml+9 qml/Stages/SessionContainer.qml+7 qml/Stages/SpreadDelegate.qml+3 qml/Stages/SurfaceContainer.qml+5 qml/Stages/TabletStage.qml+8 src/Dash/CMakeLists.txt+1 src/Dash/main.cpp+16−2 tests/autopilot/unity8/shell/tests/__init__.py+4−21 tests/mocks/HudClient/fake_libhud_client.cpp+8−1 tests/mocks/QtMultimedia/audio.cpp+1−1 tests/mocks/Ubuntu/Payments/MockPayments.cpp+3−1 tests/mocks/Ubuntu/Payments/MockPayments.h+1 tests/mocks/Unity/Application/MirSurfaceItem.cpp+14 tests/mocks/Unity/Application/MirSurfaceItem.h+7 tests/mocks/Unity/Application/MirSurfaceItem.qml+12−2 tests/mocks/Unity/Launcher/CMakeLists.txt+1−1 tests/mocks/Unity/Launcher/MockLauncherItem.cpp+14 tests/mocks/Unity/Launcher/MockLauncherItem.h+18−17 tests/mocks/Unity/Launcher/MockLauncherModel.cpp+10 tests/mocks/Unity/fake_categories.cpp+95−136 tests/mocks/Unity/fake_categories.h+7−12 tests/mocks/Unity/fake_resultsmodel.cpp+9 tests/mocks/Unity/fake_resultsmodel.h+3 tests/mocks/Unity/fake_scope.cpp+26−4 tests/mocks/Unity/fake_scope.h+5−1 tests/mocks/Unity/fake_scopes.cpp+1 tests/plugins/Dash/cardcreator/1.res+2−2 tests/plugins/Dash/cardcreator/3.res+3−3 tests/plugins/Dash/cardcreator/5.res+3−3 tests/plugins/Unity/Launcher/CMakeLists.txt+9−24 tests/plugins/Unity/Launcher/gsettings.cpp+40 tests/plugins/Unity/Launcher/gsettings.h+38 tests/plugins/Unity/Launcher/launcherbackendtest.cpp−239 tests/plugins/Unity/Launcher/launchermodeltest.cpp+81−1 tests/plugins/Utils/CMakeLists.txt−1 tests/qmltests/Dash/Previews/tst_PreviewPayments.qml+39 tests/qmltests/Dash/tst_Card.qml+3−3 tests/qmltests/Dash/tst_GenericScopeView.qml+129−1 tests/qmltests/Launcher/tst_Launcher.qml+3−1 tests/qmltests/Notifications/tst_Notifications.qml+1−1 tests/qmltests/Stages/RecursingChildSessionControl.qml+1−1 tests/qmltests/Stages/tst_ApplicationWindow.qml+30−12 tests/qmltests/Stages/tst_PhoneStage.qml+58 tests/qmltests/Stages/tst_SessionContainer.qml+63 tests/qmltests/tst_Shell.qml+103−28
cmake/modules/QmlTest.cmake+15−3
@@ -27,11 +27,14 @@ endif()
2727
2828set(qmlscene_exe ${CMAKE_BINARY_DIR}/tests/uqmlscene/uqmlscene)
2929
30+set(test_env UNITY_TESTING=1)
31+
3032macro(add_manual_qml_test SUBPATH COMPONENT_NAME)
3133 set(options NO_ADD_TEST NO_TARGETS)
3234 set(multi_value_keywords IMPORT_PATHS TARGETS PROPERTIES ENVIRONMENT)
3335
3436 cmake_parse_arguments(qmltest "${options}" "" "${multi_value_keywords}" ${ARGN})
37+ cmake_parse_arguments(qmltest_default "${options}" "" "${multi_value_keywords}" ${qmltest_DEFAULT_PROPERTIES})
3538
3639 set(qmlscene_TARGET try${COMPONENT_NAME})
3740 set(qmltest_FILE ${SUBPATH}/tst_${COMPONENT_NAME})
@@ -49,8 +52,12 @@ macro(add_manual_qml_test SUBPATH COMPONENT_NAME)
4952 endforeach(IMPORT_PATH)
5053 endif()
5154
55+ if("${qmltest_ENVIRONMENT}" STREQUAL "")
56+ set(qmltest_ENVIRONMENT "${qmltest_default_ENVIRONMENT}")
57+ endif()
58+
5259 set(qmlscene_command
53− env ${qmltest_ENVIRONMENT}
60+ env ${test_env} ${qmltest_ENVIRONMENT}
5461 ${qmlscene_exe} -qmljsdebugger=port:3768 ${CMAKE_CURRENT_SOURCE_DIR}/${qmltest_FILE}.qml
5562 ${qmlscene_imports}
5663 )
@@ -71,6 +78,7 @@ macro(add_qml_test_internal SUBPATH COMPONENT_NAME ITERATIONS)
7178 set(multi_value_keywords IMPORT_PATHS TARGETS PROPERTIES ENVIRONMENT)
7279
7380 cmake_parse_arguments(qmltest "${options}" "" "${multi_value_keywords}" ${ARGN})
81+ cmake_parse_arguments(qmltest_default "${options}" "" "${multi_value_keywords}" ${qmltest_DEFAULT_PROPERTIES})
7482
7583 set(qmltest_TARGET test${COMPONENT_NAME})
7684 set(qmltest_xvfb_TARGET xvfbtest${COMPONENT_NAME})
@@ -102,8 +110,12 @@ macro(add_qml_test_internal SUBPATH COMPONENT_NAME ITERATIONS)
102110 set(ITERATIONS_STRING "")
103111 endif()
104112
113+ if("${qmltest_ENVIRONMENT}" STREQUAL "")
114+ set(qmltest_ENVIRONMENT "${qmltest_default_ENVIRONMENT}")
115+ endif()
116+
105117 set(qmltest_command
106− env ${qmltest_ENVIRONMENT} UNITY_TESTING=1
118+ env ${test_env} ${qmltest_ENVIRONMENT}
107119 ${qmltestrunner_exe} -input ${CMAKE_CURRENT_SOURCE_DIR}/${qmltest_FILE}.qml
108120 ${qmltestrunner_imports}
109121 ${ITERATIONS_STRING}
@@ -117,7 +129,7 @@ macro(add_qml_test_internal SUBPATH COMPONENT_NAME ITERATIONS)
117129 set(LD_PRELOAD_PATH "LD_PRELOAD=/usr/lib/${ARCH_TRIPLET}/mesa/libGL.so.1")
118130 endif()
119131 set(qmltest_xvfb_command
120− env ${qmltest_ENVIRONMENT} ${LD_PRELOAD_PATH} UNITY_TESTING=1
132+ env ${test_env} ${qmltest_ENVIRONMENT} ${LD_PRELOAD_PATH}
121133 xvfb-run --server-args "-screen 0 1024x768x24" --auto-servernum
122134 ${qmltestrunner_exe} -input ${CMAKE_CURRENT_SOURCE_DIR}/${qmltest_FILE}.qml
123135 ${qmltestrunner_imports}
data/unity8-dash.desktop.in+3−3
@@ -1,9 +1,9 @@
11[Desktop Entry]
22Type=Application
3−Name=Unity 8 Dash
4−Comment=The converged Unity shell dash
3+Name=Scopes
4+Comment=An interface for using the Scopes system
55Exec=@CMAKE_INSTALL_PREFIX@/@CMAKE_INSTALL_BINDIR@/unity8-dash
66Terminal=false
7−Icon=unity8
7+Icon=
88NoDisplay=true
99X-Ubuntu-Touch=true
data/unity8-filewatcher.confnew file+8
@@ -0,0 +1,8 @@
1+description "File system watcher for unity8"
2+author "Michael Zanetti <michael.zanetti@canonical.com>"
3+
4+# Workaround for bug 1360208. ~ and * in one expression doesn't work currently
5+start on (file FILE=/home/phablet/.local/share/applications/*.desktop) or (file FILE=/usr/share/applications/*.desktop)
6+
7+exec dbus-send --type=method_call --dest=com.canonical.Unity.Launcher /com/canonical/Unity/Launcher com.canonical.Unity.Launcher.Refresh
8+
data/unity8.desktop.in+1−1
@@ -4,6 +4,6 @@ Name=Unity 8
44Comment=The converged Unity shell
55Exec=@CMAKE_INSTALL_PREFIX@/@CMAKE_INSTALL_BINDIR@/unity8
66Terminal=false
7−Icon=unity8
7+Icon=
88NoDisplay=true
99X-Ubuntu-Touch=true
debian/changelog+60
@@ -1,3 +1,63 @@
1+unity8 (8.00+14.10.20140918.3-0ubuntu1) utopic; urgency=low
2+
3+ [ Michał Sawicz ]
4+ * Add -windowgeometry option to the Dash and drop any user-visible
5+ mentions of Dash
6+ * Dash: Fix issue when expanding a category and collapsing another one
7+ at the same time.
8+
9+ [ Albert Astals ]
10+ * Dash: Fix issue when expanding a category and collapsing another one
11+ at the same time.
12+ * Make the categoryView invisible when we are in the preview mode (LP:
13+ #1341205)
14+ * Pixel pushing in the dash header (LP: #1365929)
15+
16+ [ Ying-Chun Liu ]
17+ * Re-add restart button for power menu. (LP: 1358197) (LP: #1358197)
18+
19+ [ Marcus Tomlinson ]
20+ * Don't show a preview if a null response is returned from
21+ scope.preview(result)
22+
23+ [ Mirco Müller ]
24+ * Don't limit the number of text-lines for body-text. (LP: #1369438)
25+
26+ [ Michael Zanetti ]
27+ * Don't hide launcher when nothing happens on long left edge swipes
28+ (LP: #1357333)
29+ * Rework LauncherBackend
30+
31+ [ Michael Terry ]
32+ * When running qmluitests, make sure that they use LANGUAGE=C, fixing
33+ a test failure when running locally in the US.
34+
35+ [ Daniel d'Andrada ]
36+ * Some ApplicationWindow test improvements
37+
38+ [ Rodney Dawes ]
39+ * Check purchase state to determine if purchase was cancelled and hide
40+ progress. (LP: #1362622)
41+
42+ -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Thu, 18 Sep 2014 21:26:42 +0000
43+
44+unity8 (8.00+14.10.20140918-0ubuntu1) utopic; urgency=low
45+
46+ [ Michael Zanetti ]
47+ * Focus first app if there are already some running when we're
48+ starting up (LP: #1339883)
49+
50+ [ Michał Sawicz ]
51+ * Don't play empty urls in Notification.qml
52+
53+ [ Daniel d'Andrada ]
54+ * Improve tst_Shell
55+ * Build without any warnings
56+ * Make tst_Card work from outside the source tree (LP:1359201) (LP:
57+ #1359201)
58+
59+ -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Thu, 18 Sep 2014 09:44:06 +0000
60+
161unity8 (8.00+14.10.20140910.1-0ubuntu1) utopic; urgency=low
262
363 [ Pete Woods ]
debian/control+3−3
@@ -87,7 +87,7 @@ Depends: gsettings-desktop-schemas,
8787 qtdeclarative5-gsettings1.0,
8888 qtdeclarative5-qtmir-plugin (>= 0.4.3),
8989 qtdeclarative5-ubuntu-telephony0.1,
90− unity-launcher-impl-3,
90+ unity-launcher-impl-4,
9191 unity8-common (= ${source:Version}),
9292 unity8-private (= ${binary:Version}),
9393 unity8-private | unity-launcher-impl,
@@ -171,12 +171,12 @@ Pre-Depends: ${misc:Pre-Depends},
171171Depends: accountsservice-ubuntu-schemas (>= 0.0.3),
172172 gsettings-ubuntu-schemas (>= 0.0.2+14.10.20140815),
173173 libhardware2,
174− libunity-core-6.0-9,
174+ unity-schemas (>= 7.3.1+14.10.20140915),
175175 pay-service,
176176 ${misc:Depends},
177177 ${shlibs:Depends},
178178Provides: unity-launcher-impl,
179− unity-launcher-impl-3,
179+ unity-launcher-impl-4,
180180Description: Unity 8 private libs
181181 The Unity 8 shell is the primary user interface for Ubuntu devices.
182182 .
debian/unity8.install+1
@@ -1,5 +1,6 @@
11data/unity8.conf usr/share/upstart/sessions/
22data/unity8-dash.conf usr/share/upstart/sessions/
3+data/unity8-filewatcher.conf usr/share/upstart/sessions/
34usr/bin/unity8
45usr/bin/unity8-dash
56usr/share/applications/unity8.desktop
plugins/Dash/CardCreator.js+2−2
@@ -78,8 +78,8 @@ var kArtShapeHolderCode = 'Item { \n\
7878 width = root.fixedArtShapeSize.width; \n\
7979 height = root.fixedArtShapeSize.height; \n\
8080 } else { \n\
81− width = Qt.binding(function() { return !visible ? 0 : image.width }); \n\
82− height = Qt.binding(function() { return !visible ? 0 : image.height }); \n\
81+ width = Qt.binding(function() { return image.status !== Image.Ready ? 0 : image.width }); \n\
82+ height = Qt.binding(function() { return image.status !== Image.Ready ? 0 : image.height }); \n\
8383 } \n\
8484 } \n\
8585 image: Image { \n\
plugins/Dash/listviewwithpageheader.cpp+11−1
@@ -409,6 +409,16 @@ void ListViewWithPageHeader::showHeader()
409409 }
410410}
411411
412+int ListViewWithPageHeader::firstCreatedIndex() const
413+{
414+ return m_firstVisibleIndex;
415+}
416+
417+int ListViewWithPageHeader::createdItemCount() const
418+{
419+ return m_visibleItems.count();
420+}
421+
412422QQuickItem *ListViewWithPageHeader::item(int modelIndex) const
413423{
414424 ListItem *item = itemAtIndex(modelIndex);
@@ -520,7 +530,7 @@ void ListViewWithPageHeader::adjustHeader(qreal diff)
520530 // (but the header was not shown by it's own position)
521531 // or the header is partially shown and we are not doing a maximizeVisibleArea either
522532 const bool scrolledUp = m_previousContentY > contentY();
523− const bool notRebounding = contentY() + height() < contentHeight();
533+ const bool notRebounding = qRound(contentY() + height()) < qRound(contentHeight());
524534 const bool notShownByItsOwn = contentY() + diff >= m_headerItem->y() + m_headerItem->height();
525535 const bool maximizeVisibleAreaRunning = m_contentYAnimation->isRunning() && contentYAnimationType == ContentYAnimationMaximizeVisibleArea;
526536
plugins/Dash/listviewwithpageheader.h+2
@@ -86,6 +86,8 @@ public:
8686
8787 Q_INVOKABLE void positionAtBeginning();
8888 Q_INVOKABLE void showHeader();
89+ Q_INVOKABLE int firstCreatedIndex() const;
90+ Q_INVOKABLE int createdItemCount() const;
8991 Q_INVOKABLE QQuickItem *item(int modelIndex) const;
9092
9193 // The index has to be created for this to try to do something
plugins/Lights/Lights.cpp+1−2
@@ -20,8 +20,7 @@
2020#include <QtCore/QDebug>
2121
2222extern "C" {
23−#include <hardware/hardware.h>
24−#include <hardware/lights.h>
23+#include "android-hardware.h"
2524#include <string.h>
2625}
2726
plugins/Lights/android-hardware.hnew file+23
@@ -0,0 +1,23 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ *
16+ */
17+
18+// The purpose of this header wrapper is just to avoid having warnings being reported on
19+// code from external header files, as we can't do anything about them.
20+#pragma GCC system_header
21+
22+#include <hardware/hardware.h>
23+#include <hardware/lights.h>
plugins/Ubuntu/Payments/Payments.cpp+21−1
@@ -41,13 +41,23 @@ static void observer(PayPackage* /* package */, const char* itemid, PayPackageIt
4141 case PAY_PACKAGE_ITEM_STATUS_PURCHASED:
4242 Q_EMIT self->purchaseCompleted();
4343 break;
44+ case PAY_PACKAGE_ITEM_STATUS_PURCHASING:
45+ self->setPurchasing(true);
46+ break;
47+ case PAY_PACKAGE_ITEM_STATUS_NOT_PURCHASED:
48+ if (self->purchasing()) {
49+ self->setPurchasing(false);
50+ Q_EMIT self->purchaseCancelled();
51+ }
52+ break;
4453 default:
4554 break;
4655 }
4756}
4857
4958Payments::Payments(QObject *parent)
50− : QObject(parent)
59+ : QObject(parent),
60+ m_purchasing(false)
5161{
5262 m_package = pay_package_new("click-scope");
5363 pay_package_item_observer_install(m_package, observer, this);
@@ -80,6 +90,11 @@ QString Payments::formattedPrice() const
8090 return locale.toCurrencyString(m_price, m_currency);
8191}
8292
93+bool Payments::purchasing() const
94+{
95+ return m_purchasing;
96+}
97+
8398void Payments::setCurrency(const QString &currency)
8499{
85100 if(m_currency != currency) {
@@ -112,6 +127,11 @@ void Payments::setStoreItemId(const QString &store_item_id)
112127 pay_package_item_start_verification(m_package, m_store_item_id.toLocal8Bit().data());
113128}
114129
130+void Payments::setPurchasing(bool is_purchasing)
131+{
132+ m_purchasing = is_purchasing;
133+}
134+
115135void Payments::start()
116136{
117137 pay_package_item_start_purchase(m_package, m_store_item_id.toLocal8Bit().data());
plugins/Ubuntu/Payments/Payments.h+4
@@ -39,10 +39,12 @@ public:
3939 double price() const;
4040 QString storeItemId() const;
4141 QString formattedPrice() const;
42+ bool purchasing() const;
4243
4344 void setCurrency(const QString& currency);
4445 void setPrice(const double price);
4546 void setStoreItemId(const QString& store_item_id);
47+ void setPurchasing(bool is_purchasing);
4648 Q_INVOKABLE void start();
4749
4850Q_SIGNALS:
@@ -53,11 +55,13 @@ Q_SIGNALS:
5355
5456 void purchaseError(const QString &error);
5557 void purchaseCompleted();
58+ void purchaseCancelled();
5659
5760private:
5861 QString m_currency;
5962 double m_price;
6063 QString m_store_item_id;
64+ bool m_purchasing;
6165
6266 PayPackage *m_package;
6367};
plugins/Unity/Launcher/CMakeLists.txt+5−3
@@ -1,5 +1,5 @@
11include(FindPkgConfig)
2−pkg_check_modules(LAUNCHER_API REQUIRED unity-shell-launcher=3)
2+pkg_check_modules(LAUNCHER_API REQUIRED unity-shell-launcher=4)
33pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=4)
44pkg_check_modules(GSETTINGS_QT REQUIRED gsettings-qt)
55
@@ -16,8 +16,10 @@ set(QMLLAUNCHERPLUGIN_SRC
1616 launchermodel.cpp
1717 launcheritem.cpp
1818 quicklistmodel.cpp
19− common/quicklistentry.cpp
20− backend/launcherbackend.cpp
19+ quicklistentry.cpp
20+ dbusinterface.cpp
21+ gsettings.cpp
22+ desktopfilehandler.cpp
2123 ${CMAKE_SOURCE_DIR}/plugins/AccountsService/AccountsServiceDBusAdaptor.cpp
2224 ${LAUNCHER_API_INCLUDEDIR}/unity/shell/launcher/LauncherItemInterface.h
2325 ${LAUNCHER_API_INCLUDEDIR}/unity/shell/launcher/LauncherModelInterface.h
plugins/Unity/Launcher/backend/launcherbackend.cppdeleted−591
@@ -1,591 +0,0 @@
1−/*
2− * Copyright (C) 2013 Canonical, Ltd.
3− *
4− * Authors:
5− * Michael Terry <michael.terry@canonical.com>
6− * Michael Zanetti <michael.zanetti@canonical.com>
7− *
8− * This program is free software; you can redistribute it and/or modify
9− * it under the terms of the GNU General Public License as published by
10− * the Free Software Foundation; version 3.
11− *
12− * This program is distributed in the hope that it will be useful,
13− * but WITHOUT ANY WARRANTY; without even the implied warranty of
14− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15− * GNU General Public License for more details.
16− *
17− * You should have received a copy of the GNU General Public License
18− * along with this program. If not, see <http://www.gnu.org/licenses/>.
19− */
20−
21−#include "AccountsServiceDBusAdaptor.h"
22−#include "launcherbackend.h"
23−
24−#include <QDir>
25−#include <QDBusArgument>
26−#include <QFileInfo>
27−#include <QGSettings>
28−#include <QDebug>
29−#include <QStandardPaths>
30−
31−#include <libintl.h>
32−
33−class LauncherBackendItem
34−{
35−public:
36− QString displayName;
37− QString icon;
38− int count;
39− bool countVisible;
40−};
41−
42−LauncherBackend::LauncherBackend(QObject *parent):
43− QDBusVirtualObject(parent),
44− m_accounts(nullptr)
45−{
46−#ifndef LAUNCHER_TESTING
47− m_accounts = new AccountsServiceDBusAdaptor(this);
48−#endif
49− m_user = qgetenv("USER");
50− syncFromAccounts();
51−
52− /* Set up ourselves on DBus */
53− QDBusConnection con = QDBusConnection::sessionBus();
54− if (!con.registerService("com.canonical.Unity.Launcher")) {
55− qDebug() << "Unable to register launcher name";
56− }
57− if (!con.registerVirtualObject("/com/canonical/Unity/Launcher", this, QDBusConnection::VirtualObjectRegisterOption::SubPath)) {
58− qDebug() << "Unable to register launcher object";
59− }
60−}
61−
62−LauncherBackend::~LauncherBackend()
63−{
64− /* Remove oursevles from DBus */
65− QDBusConnection con = QDBusConnection::sessionBus();
66− con.unregisterService("com.canonical.Unity.Launcher");
67− con.unregisterObject("/com/canonical/Unity/Launcher");
68−
69− /* Clear data */
70− m_storedApps.clear();
71−
72− Q_FOREACH(LauncherBackendItem *item, m_itemCache) {
73− delete item;
74− }
75− m_itemCache.clear();
76−}
77−
78−QStringList LauncherBackend::storedApplications() const
79−{
80− return m_storedApps;
81−}
82−
83−void LauncherBackend::setStoredApplications(const QStringList &appIds)
84−{
85− if (appIds.count() < m_storedApps.count()) {
86− Q_FOREACH(const QString &appId, m_storedApps) {
87− if (!appIds.contains(appId)) {
88− delete m_itemCache.take(appId);
89− }
90− }
91− }
92− m_storedApps = appIds;
93− Q_FOREACH(const QString &appId, appIds) {
94− if (!m_itemCache.contains(appId)) {
95− QString df = findDesktopFile(appId);
96− if (!df.isEmpty()) {
97− LauncherBackendItem *item = parseDesktopFile(df);
98− m_itemCache.insert(appId, item);
99− } else {
100− // Cannot find any data for that app... ignoring it.
101− qWarning() << "cannot find desktop file for" << appId << ". discarding app.";
102− m_storedApps.removeAll(appId);
103− }
104− }
105− }
106− syncToAccounts();
107−}
108−
109−QString LauncherBackend::desktopFile(const QString &appId) const
110−{
111− return findDesktopFile(appId);
112−}
113−
114−QString LauncherBackend::displayName(const QString &appId) const
115−{
116− LauncherBackendItem *item = m_itemCache.value(appId, nullptr);
117− if (item) {
118− return item->displayName;
119− }
120−
121− QString df = findDesktopFile(appId);
122− if (!df.isEmpty()) {
123− LauncherBackendItem *item = parseDesktopFile(df);
124− m_itemCache.insert(appId, item);
125− return item->displayName;
126− }
127−
128− return QString();
129−}
130−
131−QString LauncherBackend::icon(const QString &appId) const
132−{
133− QString iconName;
134− LauncherBackendItem *item = getItem(appId);
135− if (item) {
136− iconName = item->icon;
137− }
138−
139− return iconName;
140−}
141−
142−QList<QuickListEntry> LauncherBackend::quickList(const QString &appId) const
143−{
144− // TODO: Get static (from .desktop file) and dynamic (from the app itself)
145− // entries and return them here. Frontend related entries (like "Pin to launcher")
146− // don't matter here. This is just the backend part.
147− // TODO: emit quickListChanged() when the dynamic part changes
148− Q_UNUSED(appId)
149− return QList<QuickListEntry>();
150−}
151−
152−int LauncherBackend::progress(const QString &appId) const
153−{
154− // TODO: Return value for progress emblem.
155− // TODO: emit progressChanged() when this value changes.
156− Q_UNUSED(appId)
157− return -1;
158−}
159−
160−int LauncherBackend::count(const QString &appId) const
161−{
162− int count = -1;
163− LauncherBackendItem *item = getItem(appId);
164−
165− if (item) {
166− if (item->countVisible) {
167− count = item->count;
168− }
169− }
170−
171− return count;
172−}
173−
174−void LauncherBackend::setCount(const QString &appId, int count) const
175−{
176− LauncherBackendItem *item = getItem(appId);
177−
178− bool emitchange = false;
179− if (item) {
180− emitchange = (item->count != count);
181− item->count = count;
182− }
183−
184− if (emitchange) {
185− /* TODO: This needs to use the accessor to handle the visibility
186− correctly, but when we have the two properties we can just use
187− the local value */
188− Q_EMIT countChanged(appId, this->count(appId));
189− QVariant vcount(item->count);
190− emitPropChangedDbus(appId, "count", vcount);
191− }
192−}
193−
194−bool LauncherBackend::countVisible(const QString &appId) const
195−{
196− bool visible = false;
197− LauncherBackendItem *item = getItem(appId);
198−
199− if (item) {
200− visible = item->countVisible;
201− }
202−
203− return visible;
204−}
205−
206−void LauncherBackend::setCountVisible(const QString &appId, bool visible) const
207−{
208− LauncherBackendItem *item = getItem(appId);
209−
210− bool emitchange = false;
211− if (item) {
212− emitchange = (item->countVisible != visible);
213− item->countVisible = visible;
214− } else {
215− qDebug() << "Unable to find:" << appId;
216− }
217−
218− if (emitchange) {
219− /* TODO: Because we're using visible in determining the
220− count we need to emit a count changed as well */
221− Q_EMIT countChanged(appId, this->count(appId));
222− Q_EMIT countVisibleChanged(appId, item->countVisible);
223− QVariant vCountVisible(item->countVisible);
224− emitPropChangedDbus(appId, "countVisible", vCountVisible);
225− }
226−}
227−
228−void LauncherBackend::setUser(const QString &username)
229−{
230− if (qgetenv("USER") == "lightdm" && m_user != username) {
231− m_user = username;
232− syncFromAccounts();
233− }
234−}
235−
236−void LauncherBackend::triggerQuickListAction(const QString &appId, const QString &quickListId)
237−{
238− // TODO: execute the given quicklist action
239− Q_UNUSED(appId)
240− Q_UNUSED(quickListId)
241−}
242−
243−void LauncherBackend::syncFromAccounts()
244−{
245− QList<QVariantMap> apps;
246− bool defaults = true;
247−
248− m_storedApps.clear();
249−
250− if (m_accounts && !m_user.isEmpty()) {
251− QVariant variant = m_accounts->getUserProperty(m_user, "com.canonical.unity.AccountsService", "launcher-items");
252− if (variant.isValid() && variant.canConvert<QDBusArgument>()) {
253− apps = qdbus_cast<QList<QVariantMap>>(variant.value<QDBusArgument>());
254− defaults = isDefaultsItem(apps);
255− }
256− }
257−
258− if (m_accounts && defaults) { // Checking accounts as it'll be null when !useStorage
259− QGSettings gSettings("com.canonical.Unity.Launcher", "/com/canonical/unity/launcher/");
260− Q_FOREACH(const QString &entry, gSettings.get("favorites").toStringList()) {
261− if (entry.startsWith("application://")) {
262− QString appId = entry;
263− // Transform "application://foobar.desktop" to "application://foobar"
264− appId.remove("application://");
265− if (appId.endsWith(".desktop")) {
266− appId.chop(8);
267− }
268− QString df = findDesktopFile(appId);
269−
270− if (!df.isEmpty()) {
271− m_storedApps << appId;
272−
273− if (!m_itemCache.contains(appId)) {
274− m_itemCache.insert(appId, parseDesktopFile(df));
275− }
276− }
277− }
278− if (entry.startsWith("appid://")) {
279− QString appId = entry;
280− appId.remove("appid://");
281− // Strip hook name and current-user-version in case its there
282−
283− if (appId.split('/').count() != 3) {
284− qWarning() << "ignoring entry " + appId + ". Not a valid appId.";
285− continue;
286− }
287− appId = appId.split('/').first() + "_" + appId.split('/').at(1);
288− QString df = findDesktopFile(appId);
289−
290− if (!df.isEmpty()) {
291− m_storedApps << appId;
292−
293− if (!m_itemCache.contains(appId)) {
294− m_itemCache.insert(appId, parseDesktopFile(df));
295− }
296− }
297− }
298− }
299− } else {
300− for (const QVariant &app: apps) {
301− loadFromVariant(app.toMap());
302− }
303− }
304−}
305−
306−void LauncherBackend::syncToAccounts()
307−{
308− if (m_accounts && !m_user.isEmpty()) {
309− QList<QVariantMap> items;
310−
311− Q_FOREACH(const QString &appId, m_storedApps) {
312− items << itemToVariant(appId);
313− }
314−
315− m_accounts->setUserProperty(m_user, "com.canonical.unity.AccountsService", "launcher-items", QVariant::fromValue(items));
316− }
317−}
318−
319−QString LauncherBackend::findDesktopFile(const QString &appId) const
320−{
321− int dashPos = -1;
322− QString helper = appId;
323−
324− QStringList searchDirs = QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation);
325−#ifdef LAUNCHER_TESTING
326− searchDirs << "";
327−#endif
328−
329− do {
330− if (dashPos != -1) {
331− helper = helper.replace(dashPos, 1, '/');
332− }
333−
334− Q_FOREACH(const QString &searchDirName, searchDirs) {
335− QDir searchDir(searchDirName);
336− Q_FOREACH(const QString &desktopFile, searchDir.entryList(QStringList() << "*.desktop")) {
337− if (desktopFile.startsWith(helper)) {
338− QFileInfo fileInfo(searchDir, desktopFile);
339− return fileInfo.absoluteFilePath();
340− }
341− }
342− }
343−
344− dashPos = helper.indexOf("-");
345− } while (dashPos != -1);
346−
347− return QString();
348−}
349−
350−LauncherBackendItem* LauncherBackend::parseDesktopFile(const QString &desktopFile) const
351−{
352− QSettings settings(desktopFile, QSettings::IniFormat);
353−
354− LauncherBackendItem* item = new LauncherBackendItem();
355−
356− item->displayName = settings.value("Desktop Entry/Name").toString();
357− const QString domain = settings.value("Desktop Entry/X-Ubuntu-Gettext-Domain").toString();
358− if (!domain.isEmpty()) {
359− item->displayName = dgettext(domain.toUtf8().constData(), item->displayName.toUtf8().constData());
360− }
361−
362− QString iconString = settings.value("Desktop Entry/Icon").toString();
363− QString pathString = settings.value("Desktop Entry/Path").toString();
364− if (QFileInfo(iconString).exists()) {
365− item->icon = QFileInfo(iconString).absoluteFilePath();
366− } else if (QFileInfo(pathString + '/' + iconString).exists()) {
367− item->icon = pathString + '/' + iconString;
368− } else {
369− item->icon = "image://theme/" + iconString;
370− }
371−
372− /* TODO: These should be looked up in a cache somewhere */
373− item->count = 0;
374− item->countVisible = false;
375−
376− return item;
377−}
378−
379−/* Gets an item, and tries to create a new one if we need it to */
380−LauncherBackendItem* LauncherBackend::getItem(const QString &appId) const
381−{
382− LauncherBackendItem *item = m_itemCache.value(appId, nullptr);
383− if (!item) {
384− QString df = findDesktopFile(appId);
385− if (!df.isEmpty()) {
386− item = parseDesktopFile(df);
387− if (item) {
388− m_itemCache[appId] = item;
389− } else {
390− qWarning() << "Unable to parse desktop file for" << appId << "path" << df;
391− }
392− } else {
393− qWarning() << "Unable to find desktop file for:" << appId;
394− }
395− }
396−
397− if (!item)
398− qWarning() << "Unable to find item for: " << appId;
399−
400− return item;
401−}
402−
403−void LauncherBackend::loadFromVariant(const QVariantMap &details)
404−{
405− if (!details.contains("id")) {
406− return;
407− }
408− QString appId = details.value("id").toString();
409−
410− LauncherBackendItem *item = m_itemCache.value(appId, nullptr);
411− if (item) {
412− delete item;
413− }
414−
415− item = new LauncherBackendItem();
416−
417− item->displayName = details.value("name").toString();
418− item->icon = details.value("icon").toString();
419− item->count = details.value("count").toInt();
420− item->countVisible = details.value("countVisible").toBool();
421−
422− m_itemCache.insert(appId, item);
423− m_storedApps.append(appId);
424−}
425−
426−QVariantMap LauncherBackend::itemToVariant(const QString &appId) const
427−{
428− LauncherBackendItem *item = m_itemCache.value(appId);
429− QVariantMap details;
430− details.insert("id", appId);
431− details.insert("name", item->displayName);
432− details.insert("icon", item->icon);
433− details.insert("count", item->count);
434− details.insert("countVisible", item->countVisible);
435− return details;
436−}
437−
438−bool LauncherBackend::isDefaultsItem(const QList<QVariantMap> &apps) const
439−{
440− // To differentiate between an empty list and a list that hasn't been set
441− // yet (and should thus be populated with the defaults), we use a special
442− // list of one item with the 'defaults' field set to true.
443− return (apps.size() == 1 && apps[0].value("defaults").toBool());
444−}
445−
446−bool LauncherBackend::handleMessage(const QDBusMessage& message, const QDBusConnection& connection)
447−{
448− /* Check to make sure we're getting properties on our interface */
449− if (message.type() != QDBusMessage::MessageType::MethodCallMessage) {
450− return false;
451− }
452− if (message.interface() != "org.freedesktop.DBus.Properties") {
453− return false;
454− }
455− if (message.arguments()[0].toString() != "com.canonical.Unity.Launcher.Item") {
456− return false;
457− }
458−
459− /* Break down the path to just the app id */
460− QString pathtemp = message.path();
461− if (!pathtemp.startsWith("/com/canonical/Unity/Launcher/")) {
462− return false;
463− }
464− pathtemp.remove("/com/canonical/Unity/Launcher/");
465− if (pathtemp.indexOf('/') >= 0) {
466− return false;
467− }
468−
469− /* Find ourselves an appid */
470− QString appid = decodeAppId(pathtemp);
471− QVariantList retval;
472−
473− if (message.member() == "Get") {
474− if (message.arguments()[1].toString() == "count") {
475− retval.append(QVariant::fromValue(QDBusVariant(this->count(appid))));
476− } else if (message.arguments()[1].toString() == "countVisible") {
477− retval.append(QVariant::fromValue(QDBusVariant(this->countVisible(appid))));
478− }
479− } else if (message.member() == "Set") {
480− if (message.arguments()[1].toString() == "count") {
481− this->setCount(appid, message.arguments()[2].value<QDBusVariant>().variant().toInt());
482− } else if (message.arguments()[1].toString() == "countVisible") {
483− this->setCountVisible(appid, message.arguments()[2].value<QDBusVariant>().variant().toBool());
484− }
485− } else if (message.member() == "GetAll") {
486− retval.append(this->itemToVariant(appid));
487− } else {
488− return false;
489− }
490−
491− QDBusMessage reply = message.createReply(retval);
492− return connection.send(reply);
493−}
494−
495−QString LauncherBackend::introspect(const QString &path) const
496−{
497− /* This case we should just list the nodes */
498− if (path == "/com/canonical/Unity/Launcher/" || path == "/com/canonical/Unity/Launcher") {
499− QString nodes;
500−
501− Q_FOREACH(const QString &appId, m_itemCache.keys()) {
502− nodes.append("<node name=\"");
503− nodes.append(encodeAppId(appId));
504− nodes.append("\"/>\n");
505− }
506−
507− return nodes;
508− }
509−
510− /* Should not happen, but let's handle it */
511− if (!path.startsWith("/com/canonical/Unity/Launcher")) {
512− return "";
513− }
514−
515− /* Now we should be looking at a node */
516− QString nodeiface =
517− "<interface name=\"com.canonical.Unity.Launcher.Item\">"
518− "<property name=\"count\" type=\"i\" access=\"readwrite\" />"
519− "<property name=\"countVisible\" type=\"b\" access=\"readwrite\" />"
520− "</interface>";
521− return nodeiface;
522−}
523−
524−QString LauncherBackend::decodeAppId(const QString& path)
525−{
526− QByteArray bytes = path.toUtf8();
527− QByteArray decoded;
528−
529− for (int i = 0; i < bytes.size(); ++i) {
530− char chr = bytes.at(i);
531−
532− if (chr == '_') {
533− QString number;
534− number.append(bytes.at(i+1));
535− number.append(bytes.at(i+2));
536−
537− bool okay;
538− char newchar = number.toUInt(&okay, 16);
539− if (okay)
540− decoded.append(newchar);
541−
542− i += 2;
543− } else {
544− decoded.append(chr);
545− }
546− }
547−
548− return QString::fromUtf8(decoded);
549−}
550−
551−QString LauncherBackend::encodeAppId(const QString& appId)
552−{
553− QByteArray bytes = appId.toUtf8();
554− QString encoded;
555−
556− for (int i = 0; i < bytes.size(); ++i) {
557− uchar chr = bytes.at(i);
558−
559− if ((chr >= 'a' && chr <= 'z') ||
560− (chr >= 'A' && chr <= 'Z') ||
561− (chr >= '0' && chr <= '9'&& i != 0)) {
562− encoded.append(chr);
563− } else {
564− QString hexval = QString("_%1").arg(chr, 2, 16, QChar('0'));
565− encoded.append(hexval.toUpper());
566− }
567− }
568−
569− return encoded;
570−}
571−
572−void LauncherBackend::emitPropChangedDbus(const QString& appId, const QString& property, QVariant &value) const
573−{
574− QString path("/com/canonical/Unity/Launcher/");
575− path.append(encodeAppId(appId));
576−
577− QDBusMessage message = QDBusMessage::createSignal(path, "org.freedesktop.DBus.Properties", "PropertiesChanged");
578−
579− QList<QVariant> arguments;
580− QVariantHash changedprops;
581− changedprops[property] = QVariant::fromValue(QDBusVariant(value));
582− QVariantList deletedprops;
583−
584− arguments.append(changedprops);
585− arguments.append(deletedprops);
586−
587− message.setArguments(arguments);
588−
589− QDBusConnection con = QDBusConnection::sessionBus();
590− con.send(message);
591−}
plugins/Unity/Launcher/backend/launcherbackend.hdeleted−196
@@ -1,196 +0,0 @@
1−/* Copyright (C) 2013 Canonical, Ltd.
2− *
3− * Authors:
4− * Michael Zanetti <michael.zanetti@canonical.com>
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; version 3.
9− *
10− * This program is distributed in the hope that it will be useful,
11− * but WITHOUT ANY WARRANTY; without even the implied warranty of
12− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13− * GNU General Public License for more details.
14− *
15− * You should have received a copy of the GNU General Public License
16− * along with this program. If not, see <http://www.gnu.org/licenses/>.
17− */
18−
19−#ifndef LAUNCHERBACKEND_H
20−#define LAUNCHERBACKEND_H
21−
22−#include "common/quicklistentry.h"
23−
24−#include <QObject>
25−#include <QSettings>
26−#include <QStringList>
27−#include <QDBusVirtualObject>
28−
29−class AccountsServiceDBusAdaptor;
30−
31−/**
32− * @brief An interface that provides all the data needed by the launcher.
33− */
34−
35−class LauncherBackendItem;
36−class LauncherBackendTest;
37−
38−class LauncherBackend : public QDBusVirtualObject
39−{
40− Q_OBJECT
41−
42− friend LauncherBackendTest;
43−
44−public:
45− LauncherBackend(QObject *parent = 0);
46− virtual ~LauncherBackend();
47−
48− /**
49− * @brief Returns a list of stored applications.
50− * @returns A list of application IDs.
51− */
52− QStringList storedApplications() const;
53−
54− /**
55− * @brief Set the list of stored applications.
56− *
57− * Any previously stored information for apps not contained
58− * in the list any more, e.g. the pinned state, will be
59− * discarded.
60− *
61− * @param appIds The new list of stored applications.
62− */
63− void setStoredApplications(const QStringList &appIds);
64−
65− /**
66− * @brief Get the full path to the .desktop file.
67− *
68− * The application does not need to be in the list of stored applications.
69− *
70− * @returns The full path to the .dekstop file.
71− */
72− QString desktopFile(const QString &appId) const;
73−
74− /**
75− * @brief Get the user friendly name of an application.
76− *
77− * The application does not need to be in the list of stored applications.
78− *
79− * @param appId The ID of the application.
80− * @returns The user friendly name of the application.
81− */
82− QString displayName(const QString &appId) const;
83−
84− /**
85− * @brief Get the icon of an application.
86− *
87− * The application does not need to be in the list of stored applications.
88− *
89− * @param appId The ID of the application.
90− * @returns The full path to the icon for the application.
91− */
92− QString icon(const QString &appId) const;
93−
94− /**
95− * @brief Get the QuickList for an application.
96− * @param appId The ID of the application.
97− * @returns A QuickListModelInterface containing the QuickList.
98− */
99− QList<QuickListEntry> quickList(const QString &appId) const;
100−
101− /**
102− * @brief Execute an action from the quickList
103− * @param appId The app ID for which the action was triggered
104− * @param the quicklist ID of the action that was triggered
105− */
106− void triggerQuickListAction(const QString &appId, const QString &entryId);
107−
108− /**
109− * @brief Get the progress for the progress overlay of an application.
110− * @param appId The ID of the application.
111− * @returns The percentage of the overlay progress bar. -1 if no progress bar available.
112− */
113− int progress(const QString &appId) const;
114−
115− /**
116− * @brief Get the count of the count overlay of an application.
117− * @param appId The ID of the application.
118− * @returns The number to be displayed in the overlay. -1 if no count overlay is available.
119− */
120− int count(const QString &appId) const;
121−
122− /**
123− * @brief Set the count on an item
124− * @param appId The ID of the application
125− * @param count Count to show on the application
126− */
127− void setCount(const QString &appId, int count) const;
128−
129− /**
130− * @brief Get whether the count should be visible
131− * @param appId The ID of the application.
132− * @returns Whether to show a count on the launcher
133− */
134− bool countVisible(const QString &appId) const;
135−
136− /**
137− * @brief Set the visibility of the count item
138− * @param appId The ID of the application
139− * @param visible Whether the count should be visible
140− */
141− void setCountVisible(const QString &appId, bool visible) const;
142−
143− /**
144− * @brief Sets the username for which to look up launcher items.
145− * @param username The username to use.
146− */
147− void setUser(const QString &username);
148−
149− /**
150− * @brief Handle a message to an application node
151− * @param message DBus message to handle
152− * @param connection DBus connection that we're using
153− * @returns whether the message was handled
154− */
155− virtual bool handleMessage(const QDBusMessage& message, const QDBusConnection& connection);
156−
157− /**
158− * @brief Get introspection information on the objects we're exporting
159− * @param path the dbus path containing the appid
160− * @returns Introspection information for that node in the tree
161− */
162− virtual QString introspect (const QString &path) const;
163−
164−Q_SIGNALS:
165− void quickListChanged(const QString &appId, const QList<QuickListEntry> &quickList) const;
166− void progressChanged(const QString &appId, int progress) const;
167− void countChanged(const QString &appId, int count) const;
168− void countVisibleChanged(const QString &appId, bool visible) const;
169−
170−private:
171− QString findDesktopFile(const QString &appId) const;
172− LauncherBackendItem* parseDesktopFile(const QString &desktopFile) const;
173−
174− QVariantMap itemToVariant(const QString &appId) const;
175− void loadFromVariant(const QVariantMap &details);
176−
177− bool isDefaultsItem(const QList<QVariantMap> &apps) const;
178− void syncFromAccounts();
179− void syncToAccounts();
180−
181− QList<QString> m_storedApps;
182− mutable QHash<QString, LauncherBackendItem*> m_itemCache;
183−
184− AccountsServiceDBusAdaptor *m_accounts;
185− QString m_user;
186−
187− void emitPropChangedDbus(const QString& appId, const QString& property, QVariant &value) const;
188−
189−protected: /* Protected to allow testing */
190− LauncherBackendItem* getItem(const QString& appId) const;
191−
192− static QString decodeAppId(const QString& path);
193− static QString encodeAppId(const QString& appId);
194−};
195−
196−#endif // LAUNCHERBACKEND_H
plugins/Unity/Launcher/dbusinterface.cppnew file+232
@@ -0,0 +1,232 @@
1+/*
2+ * Copyright 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 Lesser General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ *
16+ * Authors:
17+ * Michael Zanetti <michael.zanetti@canonical.com>
18+ */
19+
20+#include "dbusinterface.h"
21+#include "launchermodel.h"
22+#include "launcheritem.h"
23+
24+#include <QDBusArgument>
25+#include <QDBusConnection>
26+#include <QDBusMessage>
27+#include <QDebug>
28+
29+DBusInterface::DBusInterface(LauncherModel *parent):
30+ QDBusVirtualObject(parent),
31+ m_launcherModel(parent)
32+{
33+ /* Set up ourselves on DBus */
34+ QDBusConnection con = QDBusConnection::sessionBus();
35+ if (!con.registerService("com.canonical.Unity.Launcher")) {
36+ qWarning() << "Unable to register launcher name";
37+ }
38+ if (!con.registerVirtualObject("/com/canonical/Unity/Launcher", this, QDBusConnection::VirtualObjectRegisterOption::SubPath)) {
39+ qWarning() << "Unable to register launcher object";
40+ }
41+}
42+
43+DBusInterface::~DBusInterface()
44+{
45+ /* Remove oursevles from DBus */
46+ QDBusConnection con = QDBusConnection::sessionBus();
47+ con.unregisterService("com.canonical.Unity.Launcher");
48+ con.unregisterObject("/com/canonical/Unity/Launcher");
49+}
50+
51+QString DBusInterface::introspect(const QString &path) const
52+{
53+ /* This case we should just list the nodes */
54+ if (path == "/com/canonical/Unity/Launcher/" || path == "/com/canonical/Unity/Launcher") {
55+ QString nodes;
56+
57+ // Add Refresh to introspect
58+ nodes = "<interface name=\"com.canonical.Unity.Launcher\">"
59+ "<method name=\"Refresh\"/>"
60+ "</interface>";
61+
62+ // Add dynamic properties for launcher emblems
63+ for (int i = 0; i < m_launcherModel->rowCount(); i++) {
64+ nodes.append("<node name=\"");
65+ nodes.append(encodeAppId(m_launcherModel->get(i)->appId()));
66+ nodes.append("\"/>\n");
67+ }
68+ return nodes;
69+ }
70+
71+ /* Should not happen, but let's handle it */
72+ if (!path.startsWith("/com/canonical/Unity/Launcher")) {
73+ return "";
74+ }
75+
76+ /* Now we should be looking at a node */
77+ QString nodeiface =
78+ "<interface name=\"com.canonical.Unity.Launcher.Item\">"
79+ "<property name=\"count\" type=\"i\" access=\"readwrite\" />"
80+ "<property name=\"countVisible\" type=\"b\" access=\"readwrite\" />"
81+ "</interface>";
82+ return nodeiface;
83+}
84+
85+
86+QString DBusInterface::decodeAppId(const QString& path)
87+{
88+ QByteArray bytes = path.toUtf8();
89+ QByteArray decoded;
90+
91+ for (int i = 0; i < bytes.size(); ++i) {
92+ char chr = bytes.at(i);
93+
94+ if (chr == '_') {
95+ QString number;
96+ number.append(bytes.at(i+1));
97+ number.append(bytes.at(i+2));
98+
99+ bool okay;
100+ char newchar = number.toUInt(&okay, 16);
101+ if (okay)
102+ decoded.append(newchar);
103+
104+ i += 2;
105+ } else {
106+ decoded.append(chr);
107+ }
108+ }
109+
110+ return QString::fromUtf8(decoded);
111+}
112+
113+QString DBusInterface::encodeAppId(const QString& appId)
114+{
115+ QByteArray bytes = appId.toUtf8();
116+ QString encoded;
117+
118+ for (int i = 0; i < bytes.size(); ++i) {
119+ uchar chr = bytes.at(i);
120+
121+ if ((chr >= 'a' && chr <= 'z') ||
122+ (chr >= 'A' && chr <= 'Z') ||
123+ (chr >= '0' && chr <= '9'&& i != 0)) {
124+ encoded.append(chr);
125+ } else {
126+ QString hexval = QString("_%1").arg(chr, 2, 16, QChar('0'));
127+ encoded.append(hexval.toUpper());
128+ }
129+ }
130+
131+ return encoded;
132+}
133+
134+bool DBusInterface::handleMessage(const QDBusMessage& message, const QDBusConnection& connection)
135+{
136+ /* Check to make sure we're getting properties on our interface */
137+ if (message.type() != QDBusMessage::MessageType::MethodCallMessage) {
138+ return false;
139+ }
140+
141+ // First handle methods of the Launcher interface
142+ if (message.interface() == "com.canonical.Unity.Launcher") {
143+ if (message.member() == "Refresh") {
144+ QDBusMessage reply = message.createReply();
145+ Q_EMIT refreshCalled();
146+ return connection.send(reply);
147+ }
148+ }
149+
150+ // Now handle dynamic properties (for launcher emblems)
151+ if (message.interface() != "org.freedesktop.DBus.Properties") {
152+ return false;
153+ }
154+
155+ if (message.member() != "GetAll" && message.arguments()[0].toString() != "com.canonical.Unity.Launcher.Item") {
156+ return false;
157+ }
158+
159+ /* Break down the path to just the app id */
160+ QString pathtemp = message.path();
161+ if (!pathtemp.startsWith("/com/canonical/Unity/Launcher/")) {
162+ return false;
163+ }
164+ pathtemp.remove("/com/canonical/Unity/Launcher/");
165+ if (pathtemp.indexOf('/') >= 0) {
166+ return false;
167+ }
168+
169+ /* Find ourselves an appid */
170+ QString appid = decodeAppId(pathtemp);
171+ int index = m_launcherModel->findApplication(appid);
172+ LauncherItem *item = static_cast<LauncherItem*>(m_launcherModel->get(index));
173+
174+ QVariantList retval;
175+ if (message.member() == "Get") {
176+ if (!item) {
177+ return false;
178+ }
179+ if (message.arguments()[1].toString() == "count") {
180+ retval.append(QVariant::fromValue(QDBusVariant(item->count())));
181+ } else if (message.arguments()[1].toString() == "countVisible") {
182+ retval.append(QVariant::fromValue(QDBusVariant(item->countVisible())));
183+ }
184+ } else if (message.member() == "Set") {
185+ if (message.arguments()[1].toString() == "count") {
186+ int newCount = message.arguments()[2].toInt();
187+ if (!item || newCount != item->count()) {
188+ Q_EMIT countChanged(appid, newCount);
189+ emitPropChangedDbus(appid, "count", QVariant(newCount));
190+ }
191+ } else if (message.arguments()[1].toString() == "countVisible") {
192+ bool newVisible = message.arguments()[2].toBool();
193+ if (!item || newVisible != item->countVisible()) {
194+ Q_EMIT countVisibleChanged(appid, newVisible);
195+ emitPropChangedDbus(appid, "countVisible", newVisible);
196+ }
197+ }
198+ } else if (message.member() == "GetAll") {
199+ if (item) {
200+ QVariantMap all;
201+ all.insert("count", item->count());
202+ all.insert("countVisible", item->countVisible());
203+ retval.append(all);
204+ }
205+ } else {
206+ return false;
207+ }
208+
209+ QDBusMessage reply = message.createReply(retval);
210+ return connection.send(reply);
211+}
212+
213+void DBusInterface::emitPropChangedDbus(const QString& appId, const QString& property, const QVariant &value)
214+{
215+ QString path("/com/canonical/Unity/Launcher/");
216+ path.append(encodeAppId(appId));
217+
218+ QDBusMessage message = QDBusMessage::createSignal(path, "org.freedesktop.DBus.Properties", "PropertiesChanged");
219+
220+ QList<QVariant> arguments;
221+ QVariantHash changedprops;
222+ changedprops[property] = QVariant::fromValue(QDBusVariant(value));
223+ QVariantList deletedprops;
224+
225+ arguments.append(changedprops);
226+ arguments.append(deletedprops);
227+
228+ message.setArguments(arguments);
229+
230+ QDBusConnection con = QDBusConnection::sessionBus();
231+ con.send(message);
232+}
plugins/Unity/Launcher/dbusinterface.hnew file+50
@@ -0,0 +1,50 @@
1+/*
2+ * Copyright 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 Lesser General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ *
16+ * Authors:
17+ * Michael Zanetti <michael.zanetti@canonical.com>
18+ */
19+
20+#include "launcheritem.h"
21+
22+#include <QDBusVirtualObject>
23+
24+class LauncherModel;
25+
26+class DBusInterface: public QDBusVirtualObject
27+{
28+ Q_OBJECT
29+public:
30+ DBusInterface(LauncherModel *parent);
31+ ~DBusInterface();
32+
33+ // QDBusVirtualObject implementaition
34+ QString introspect (const QString &path) const override;
35+ bool handleMessage(const QDBusMessage& message, const QDBusConnection& connection) override;
36+
37+Q_SIGNALS:
38+ void countChanged(const QString &appId, int count);
39+ void countVisibleChanged(const QString &appId, bool countVisible);
40+ void refreshCalled();
41+
42+private:
43+ static QString decodeAppId(const QString& path);
44+ static QString encodeAppId(const QString& appId);
45+
46+ void emitPropChangedDbus(const QString& appId, const QString& property, const QVariant &value);
47+
48+ LauncherModel *m_launcherModel;
49+
50+};
plugins/Unity/Launcher/desktopfilehandler.cppnew file+151
@@ -0,0 +1,151 @@
1+/*
2+ * Copyright 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 Lesser General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ *
16+ * Authors:
17+ * Michael Zanetti <michael.zanetti@canonical.com>
18+ */
19+
20+#include "desktopfilehandler.h"
21+
22+#include <QStringList>
23+#include <QStandardPaths>
24+#include <QDir>
25+#include <QSettings>
26+#include <QLocale>
27+
28+#include <libintl.h>
29+
30+DesktopFileHandler::DesktopFileHandler(const QString &appId, QObject *parent):
31+ QObject(parent),
32+ m_appId(appId)
33+{
34+ load();
35+}
36+
37+QString DesktopFileHandler::appId() const
38+{
39+ return m_appId;
40+}
41+
42+void DesktopFileHandler::setAppId(const QString &appId)
43+{
44+ if (m_appId != appId) {
45+ m_appId = appId;
46+ load();
47+ }
48+}
49+
50+QString DesktopFileHandler::filename() const
51+{
52+ return m_filename;
53+}
54+
55+bool DesktopFileHandler::isValid() const
56+{
57+ return !m_filename.isEmpty();
58+}
59+
60+void DesktopFileHandler::load()
61+{
62+ m_filename.clear();
63+
64+ if (m_appId.isEmpty()) {
65+ return;
66+ }
67+
68+ int dashPos = -1;
69+ QString helper = m_appId;
70+
71+ QStringList searchDirs = QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation);
72+#ifdef LAUNCHER_TESTING
73+ searchDirs << "";
74+#endif
75+
76+ QString path;
77+ do {
78+ if (dashPos != -1) {
79+ helper.replace(dashPos, 1, '/');
80+ }
81+
82+ if (helper.contains("/")) {
83+ path += helper.split('/').first() + '/';
84+ helper.remove(QRegExp("^" + path));
85+ }
86+
87+ Q_FOREACH(const QString &searchDirName, searchDirs) {
88+ QDir searchDir(searchDirName + "/" + path);
89+ Q_FOREACH(const QString &desktopFile, searchDir.entryList(QStringList() << "*.desktop")) {
90+ if (desktopFile.startsWith(helper)) {
91+ QFileInfo fileInfo(searchDir, desktopFile);
92+ m_filename = fileInfo.absoluteFilePath();
93+ return;
94+ }
95+ }
96+ }
97+
98+ dashPos = helper.indexOf("-");
99+ } while (dashPos != -1);
100+}
101+
102+QString DesktopFileHandler::displayName() const
103+{
104+ if (!isValid()) {
105+ return QString();
106+ }
107+
108+ QSettings settings(m_filename, QSettings::IniFormat);
109+ settings.beginGroup("Desktop Entry");
110+
111+ // First try to find Name[xx_YY] and Name[xx] in .desktop file
112+ QString locale = QLocale::system().name();
113+ QString shortLocale = locale.split('_').first();
114+
115+ if (locale != shortLocale && settings.contains(QString("Name[%1]").arg(locale))) {
116+ return settings.value(QString("Name[%1]").arg(locale)).toString();
117+ }
118+
119+ if (settings.contains(QString("Name[%1]").arg(shortLocale))) {
120+ return settings.value(QString("Name[%1]").arg(shortLocale)).toString();
121+ }
122+
123+ // No translation found in desktop file. Get the untranslated one and have a go with gettext.
124+ QString displayName = settings.value("Name").toString();
125+
126+ if (settings.contains("X-Ubuntu-Gettext-Domain")) {
127+ const QString domain = settings.value("X-Ubuntu-Gettext-Domain").toString();
128+ return dgettext(domain.toUtf8().constData(), displayName.toUtf8().constData());
129+ }
130+
131+ return displayName;
132+}
133+
134+QString DesktopFileHandler::icon() const
135+{
136+ if (!isValid()) {
137+ return QString();
138+ }
139+
140+ QSettings settings(m_filename, QSettings::IniFormat);
141+ settings.beginGroup("Desktop Entry");
142+ QString iconString = settings.value("Icon").toString();
143+ QString pathString = settings.value("Path").toString();
144+
145+ if (QFileInfo(iconString).exists()) {
146+ return QFileInfo(iconString).absoluteFilePath();
147+ } else if (QFileInfo(pathString + '/' + iconString).exists()) {
148+ return pathString + '/' + iconString;
149+ }
150+ return "image://theme/" + iconString;
151+}
plugins/Unity/Launcher/desktopfilehandler.hnew file+56
@@ -0,0 +1,56 @@
1+/*
2+ * Copyright 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 Lesser General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ *
16+ * Authors:
17+ * Michael Zanetti <michael.zanetti@canonical.com>
18+ */
19+
20+#ifndef DESKTOPFILEHANDLER_H
21+#define DESKTOPFILEHANDLER_H
22+
23+#include <QObject>
24+
25+/**
26+ * When an object of this class is created or whenever setAppId(appId) is called,
27+ * this will search for a .desktop file matching the give appId. If a file is
28+ * found, isValid() will return true and the other methods return the contents
29+ * of the .desktop file.
30+ *
31+ * Note that this class will consider the user's locale and do a best effort
32+ * to return localized values.
33+ */
34+
35+class DesktopFileHandler: public QObject
36+{
37+ Q_OBJECT
38+public:
39+ DesktopFileHandler(const QString &appId = QString(), QObject *parent = nullptr);
40+
41+ QString appId() const;
42+ void setAppId(const QString &appId);
43+
44+ bool isValid() const;
45+ QString filename() const;
46+ QString displayName() const;
47+ QString icon() const;
48+
49+private:
50+ void load();
51+
52+ QString m_appId;
53+ QString m_filename;
54+};
55+
56+#endif
plugins/Unity/Launcher/gsettings.cppnew file+66
@@ -0,0 +1,66 @@
1+/*
2+ * Copyright 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 Lesser General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ *
16+ * Authors:
17+ * Michael Zanetti <michael.zanetti@canonical.com>
18+ */
19+
20+#include "gsettings.h"
21+
22+#include <QGSettings>
23+#include <QVariant>
24+
25+GSettings::GSettings(QObject *parent):
26+ QObject(parent)
27+{
28+
29+}
30+
31+QStringList GSettings::storedApplications() const
32+{
33+ QStringList storedApps;
34+
35+ QGSettings gSettings("com.canonical.Unity.Launcher", "/com/canonical/unity/launcher/");
36+
37+ Q_FOREACH(const QString &entry, gSettings.get("items").toStringList()) {
38+ if (entry.startsWith("application:///")) {
39+ // convert legacy entries to new world appids
40+ QString appId = entry;
41+ // Transform "application://foobar.desktop" to "foobar"
42+ appId.remove(QRegExp("^application:///"));
43+ appId.remove(QRegExp(".desktop$"));
44+ storedApps << appId;
45+ } else if (entry.startsWith("appid://")) {
46+ QString appId = entry;
47+ appId.remove("appid://");
48+ if (appId.split('/').count() == 3) {
49+ // Strip current-user-version in case its there
50+ appId = appId.split('/').first() + "_" + appId.split('/').at(1);
51+ }
52+ storedApps << appId;
53+ }
54+ }
55+ return storedApps;
56+}
57+
58+void GSettings::setStoredApplications(const QStringList &storedApplications)
59+{
60+ QGSettings gSettings("com.canonical.Unity.Launcher", "/com/canonical/unity/launcher/");
61+ QStringList gSettingsList;
62+ Q_FOREACH(const QString &entry, storedApplications) {
63+ gSettingsList << QString("appid://%1").arg(entry);
64+ }
65+ gSettings.set("items", gSettingsList);
66+}
plugins/Unity/Launcher/gsettings.hnew file+37
@@ -0,0 +1,37 @@
1+/*
2+ * Copyright 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 Lesser General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ *
16+ * Authors:
17+ * Michael Zanetti <michael.zanetti@canonical.com>
18+ */
19+
20+#ifndef GSETTINGS_H
21+#define GSETTINGS_H
22+
23+#include <QObject>
24+#include <QStringList>
25+
26+
27+class GSettings: public QObject
28+{
29+ Q_OBJECT
30+public:
31+ GSettings(QObject *parent = nullptr);
32+
33+ QStringList storedApplications() const;
34+ void setStoredApplications(const QStringList &storedApplications);
35+};
36+
37+#endif
plugins/Unity/Launcher/launcheritem.cpp+30
@@ -32,6 +32,7 @@ LauncherItem::LauncherItem(const QString &appId, const QString &name, const QStr
3232 m_recent(false),
3333 m_progress(-1),
3434 m_count(0),
35+ m_countVisible(false),
3536 m_focused(false),
3637 m_quickList(new QuickListModel(this))
3738{
@@ -55,11 +56,27 @@ QString LauncherItem::name() const
5556 return m_name;
5657}
5758
59+void LauncherItem::setName(const QString &name)
60+{
61+ if (m_name != name) {
62+ m_name = name;
63+ Q_EMIT nameChanged(name);
64+ }
65+}
66+
5867QString LauncherItem::icon() const
5968{
6069 return m_icon;
6170}
6271
72+void LauncherItem::setIcon(const QString &icon)
73+{
74+ if (m_icon != icon) {
75+ m_icon = icon;
76+ Q_EMIT iconChanged(icon);
77+ }
78+}
79+
6380bool LauncherItem::pinned() const
6481{
6582 return m_pinned;
@@ -129,6 +146,19 @@ void LauncherItem::setCount(int count)
129146 }
130147}
131148
149+bool LauncherItem::countVisible() const
150+{
151+ return m_countVisible;
152+}
153+
154+void LauncherItem::setCountVisible(bool countVisible)
155+{
156+ if (m_countVisible != countVisible) {
157+ m_countVisible = countVisible;
158+ Q_EMIT countVisibleChanged(countVisible);
159+ }
160+}
161+
132162bool LauncherItem::focused() const
133163{
134164 return m_focused;
plugins/Unity/Launcher/launcheritem.h+19−20
@@ -34,34 +34,30 @@ class LauncherItem: public LauncherItemInterface
3434public:
3535 LauncherItem(const QString &appId, const QString &name, const QString &icon, QObject *parent = 0);
3636
37− QString appId() const;
38− QString name() const;
39− QString icon() const;
37+ QString appId() const override;
38+ QString name() const override;
39+ QString icon() const override;
40+ bool pinned() const override;
41+ bool running() const override;
42+ bool recent() const override;
43+ int progress() const override;
44+ int count() const override;
45+ bool countVisible() const override;
46+ bool focused() const override;
47+
48+ unity::shell::launcher::QuickListModelInterface *quickList() const override;
4049
41− bool pinned() const;
50+private:
51+ void setName(const QString &name);
52+ void setIcon(const QString &icon);
4253 void setPinned(bool pinned);
43−
44− bool running() const;
4554 void setRunning(bool running);
46−
47− bool recent() const;
4855 void setRecent(bool recent);
49−
50− int progress() const;
5156 void setProgress(int progress);
52−
53− int count() const;
5457 void setCount(int count);
55−
56− bool focused() const;
58+ void setCountVisible(bool countVisible);
5759 void setFocused(bool focused);
5860
59− unity::shell::launcher::QuickListModelInterface *quickList() const;
60−
61−Q_SIGNALS:
62− void favoriteChanged(bool favorite);
63− void runningChanged(bool running);
64−
6561private:
6662 QString m_appId;
6763 QString m_name;
@@ -71,8 +67,11 @@ private:
7167 bool m_recent;
7268 int m_progress;
7369 int m_count;
70+ bool m_countVisible;
7471 bool m_focused;
7572 QuickListModel *m_quickList;
73+
74+ friend class LauncherModel;
7675};
7776
7877#endif // LAUNCHERITEM_H
plugins/Unity/Launcher/launchermodel.cpp+85−15
@@ -19,7 +19,9 @@
1919
2020#include "launchermodel.h"
2121#include "launcheritem.h"
22−#include "backend/launcherbackend.h"
22+#include "gsettings.h"
23+#include "desktopfilehandler.h"
24+#include "dbusinterface.h"
2325
2426#include <unity/shell/application/ApplicationInfoInterface.h>
2527
@@ -30,16 +32,24 @@ using namespace unity::shell::application;
3032
3133LauncherModel::LauncherModel(QObject *parent):
3234 LauncherModelInterface(parent),
33− m_backend(new LauncherBackend(this)),
35+ m_settings(new GSettings(this)),
36+ m_dbusIface(new DBusInterface(this)),
3437 m_appManager(0)
3538{
36− connect(m_backend, SIGNAL(countChanged(QString,int)), SLOT(countChanged(QString,int)));
37− connect(m_backend, SIGNAL(progressChanged(QString,int)), SLOT(progressChanged(QString,int)));
39+ connect(m_dbusIface, &DBusInterface::countChanged, this, &LauncherModel::countChanged);
40+ connect(m_dbusIface, &DBusInterface::countVisibleChanged, this, &LauncherModel::countVisibleChanged);
41+ connect(m_dbusIface, &DBusInterface::refreshCalled, this, &LauncherModel::refresh);
42+
43+ Q_FOREACH (const QString &entry, m_settings->storedApplications()) {
44+ DesktopFileHandler desktopFile(entry);
45+ if (!desktopFile.isValid()) {
46+ qWarning() << "Couldn't find a .desktop file for" << entry << ". Skipping...";
47+ continue;
48+ }
3849
39− Q_FOREACH (const QString &entry, m_backend->storedApplications()) {
4050 LauncherItem *item = new LauncherItem(entry,
41− m_backend->displayName(entry),
42− m_backend->icon(entry),
51+ desktopFile.displayName(),
52+ desktopFile.icon(),
4353 this);
4454 item->setPinned(true);
4555 m_list.append(item);
@@ -73,6 +83,8 @@ QVariant LauncherModel::data(const QModelIndex &index, int role) const
7383 return item->pinned();
7484 case RoleCount:
7585 return item->count();
86+ case RoleCountVisible:
87+ return item->countVisible();
7688 case RoleProgress:
7789 return item->progress();
7890 case RoleFocused:
@@ -139,10 +151,17 @@ void LauncherModel::pin(const QString &appId, int index)
139151 if (index == -1) {
140152 index = m_list.count();
141153 }
154+
155+ DesktopFileHandler desktopFile(appId);
156+ if (!desktopFile.isValid()) {
157+ qWarning() << "Can't pin this application, there is no .destkop file available.";
158+ return;
159+ }
160+
142161 beginInsertRows(QModelIndex(), index, index);
143162 LauncherItem *item = new LauncherItem(appId,
144− m_backend->displayName(appId),
145− m_backend->icon(appId),
163+ desktopFile.displayName(),
164+ desktopFile.icon(),
146165 this);
147166 item->setPinned(true);
148167 m_list.insert(index, item);
@@ -197,25 +216,27 @@ void LauncherModel::quickListActionInvoked(const QString &appId, int actionIndex
197216
198217 // Nope, we don't know this action, let the backend forward it to the application
199218 } else {
200− m_backend->triggerQuickListAction(appId, actionId);
219+ // TODO: forward quicklist action to app, possibly via m_dbusIface
201220 }
202221 }
203222}
204223
205224void LauncherModel::setUser(const QString &username)
206225{
207− m_backend->setUser(username);
226+ Q_UNUSED(username)
227+ qWarning() << "This backend doesn't support multiple users";
208228}
209229
210230QString LauncherModel::getUrlForAppId(const QString &appId) const
211231{
212232 // appId is either an appId or a legacy app name. Let's find out which
213− if (appId.isEmpty())
233+ if (appId.isEmpty()) {
214234 return QString();
235+ }
215236
216− QString df = m_backend->desktopFile(appId + ".desktop");
217− if (!df.isEmpty())
237+ if (!appId.contains("_")) {
218238 return "application:///" + appId + ".desktop";
239+ }
219240
220241 QStringList parts = appId.split('_');
221242 QString package = parts.value(0);
@@ -275,7 +296,7 @@ void LauncherModel::storeAppList()
275296 appIds << item->appId();
276297 }
277298 }
278− m_backend->setStoredApplications(appIds);
299+ m_settings->setStoredApplications(appIds);
279300}
280301
281302int LauncherModel::findApplication(const QString &appId)
@@ -310,6 +331,55 @@ void LauncherModel::countChanged(const QString &appId, int count)
310331 }
311332}
312333
334+void LauncherModel::countVisibleChanged(const QString &appId, int countVisible)
335+{
336+ int idx = findApplication(appId);
337+ if (idx >= 0) {
338+ LauncherItem *item = m_list.at(idx);
339+ item->setCountVisible(countVisible);
340+ Q_EMIT dataChanged(index(idx), index(idx), QVector<int>() << RoleCountVisible);
341+
342+ // If countVisible goes to false, and the item is neither pinned nor recent we can drop it
343+ if (!countVisible && !item->pinned() && !item->recent()) {
344+ beginRemoveRows(QModelIndex(), idx, idx);
345+ m_list.takeAt(idx)->deleteLater();
346+ endRemoveRows();
347+ }
348+ } else {
349+ // Need to create a new LauncherItem and show the highlight
350+ DesktopFileHandler desktopFile(appId);
351+ if (countVisible && desktopFile.isValid()) {
352+ LauncherItem *item = new LauncherItem(appId,
353+ desktopFile.displayName(),
354+ desktopFile.icon());
355+ item->setCountVisible(true);
356+ beginInsertRows(QModelIndex(), m_list.count(), m_list.count());
357+ m_list.append(item);
358+ endInsertRows();
359+ }
360+ }
361+}
362+
363+void LauncherModel::refresh()
364+{
365+ QList<LauncherItem*> toBeRemoved;
366+ Q_FOREACH (LauncherItem* item, m_list) {
367+ DesktopFileHandler desktopFile(item->appId());
368+ if (!desktopFile.isValid()) {
369+ toBeRemoved << item;
370+ } else {
371+ int idx = m_list.indexOf(item);
372+ item->setName(desktopFile.displayName());
373+ item->setIcon(desktopFile.icon());
374+ Q_EMIT dataChanged(index(idx), index(idx), QVector<int>() << RoleName << RoleIcon);
375+ }
376+ }
377+
378+ Q_FOREACH (LauncherItem* item, toBeRemoved) {
379+ requestRemove(item->appId());
380+ }
381+}
382+
313383void LauncherModel::applicationAdded(const QModelIndex &parent, int row)
314384{
315385 Q_UNUSED(parent);
plugins/Unity/Launcher/launchermodel.h+15−8
@@ -20,15 +20,14 @@
2020#ifndef LAUNCHERMODEL_H
2121#define LAUNCHERMODEL_H
2222
23−// unity-api
2423#include <unity/shell/launcher/LauncherModelInterface.h>
2524#include <unity/shell/application/ApplicationManagerInterface.h>
2625
27−// Qt
2826#include <QAbstractListModel>
2927
3028class LauncherItem;
31−class LauncherBackend;
29+class GSettings;
30+class DBusInterface;
3231
3332using namespace unity::shell::launcher;
3433using namespace unity::shell::application;
@@ -41,14 +40,13 @@ public:
4140 LauncherModel(QObject *parent = 0);
4241 ~LauncherModel();
4342
44− int rowCount(const QModelIndex &parent) const;
43+ int rowCount(const QModelIndex &parent = QModelIndex()) const;
4544
4645 QVariant data(const QModelIndex &index, int role) const;
4746
4847 Q_INVOKABLE unity::shell::launcher::LauncherItemInterface* get(int index) const;
4948 Q_INVOKABLE void move(int oldIndex, int newIndex);
5049 Q_INVOKABLE void pin(const QString &appId, int index = -1);
51− Q_INVOKABLE void requestRemove(const QString &appId);
5250 Q_INVOKABLE void quickListActionInvoked(const QString &appId, int actionIndex);
5351 Q_INVOKABLE void setUser(const QString &username);
5452 Q_INVOKABLE QString getUrlForAppId(const QString &appId) const;
@@ -56,13 +54,19 @@ public:
5654 unity::shell::application::ApplicationManagerInterface* applicationManager() const;
5755 void setApplicationManager(unity::shell::application::ApplicationManagerInterface *appManager);
5856
57+ int findApplication(const QString &appId);
58+
59+public Q_SLOTS:
60+ void requestRemove(const QString &appId);
61+
5962private:
6063 void storeAppList();
61− int findApplication(const QString &appId);
6264
6365private Q_SLOTS:
64− void progressChanged(const QString &appId, int progress);
6566 void countChanged(const QString &appId, int count);
67+ void countVisibleChanged(const QString &appId, int count);
68+ void progressChanged(const QString &appId, int progress);
69+ void refresh();
6670
6771 void applicationAdded(const QModelIndex &parent, int row);
6872 void applicationRemoved(const QModelIndex &parent, int row);
@@ -70,7 +74,10 @@ private Q_SLOTS:
7074
7175private:
7276 QList<LauncherItem*> m_list;
73− LauncherBackend *m_backend;
77+
78+ GSettings *m_settings;
79+ DBusInterface *m_dbusIface;
80+
7481 ApplicationManagerInterface *m_appManager;
7582};
7683
plugins/Unity/Launcher/common/quicklistentry.cpp → plugins/Unity/Launcher/quicklistentry.cpprenamed

The content is the same.

plugins/Unity/Launcher/common/quicklistentry.h → plugins/Unity/Launcher/quicklistentry.hrenamed

The content is the same.

plugins/Unity/Launcher/quicklistmodel.h+1−1
@@ -20,7 +20,7 @@
2020#ifndef QUICKLISTMODEL_H
2121#define QUICKLISTMODEL_H
2222
23−#include "common/quicklistentry.h"
23+#include "quicklistentry.h"
2424
2525#include <unity/shell/launcher/QuickListModelInterface.h>
2626
plugins/Unity/Session/CMakeLists.txt+7
@@ -1,10 +1,16 @@
1+include(FindPkgConfig)
2+
3+pkg_search_module(GIO REQUIRED gio-2.0)
4+
15include_directories(
26 ${CMAKE_CURRENT_SOURCE_DIR}
7+ ${GIO_INCLUDE_DIRS}
38)
49
510set(QMLSESSIONPLUGIN_SRC
611 plugin.cpp
712 dbusunitysessionservice.cpp
13+ orientationlock.cpp
814 )
915
1016add_library(UnitySession-qml MODULE
@@ -12,6 +18,7 @@ add_library(UnitySession-qml MODULE
1218 )
1319
1420qt5_use_modules(UnitySession-qml DBus Qml)
21+target_link_libraries(UnitySession-qml ${GIO_LDFLAGS})
1522
1623# export the qmldir and qmltypes files
1724add_unity8_plugin(Unity.Session 0.1 Unity/Session TARGETS UnitySession-qml)
plugins/Unity/Session/orientationlock.cppnew file+74
@@ -0,0 +1,74 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software: you can redistribute it and/or modify it under
5+ * the terms of the GNU Lesser General Public License version 3, as published by
6+ * the Free Software Foundation.
7+ *
8+ * This program is distributed in the hope that it will be useful, but WITHOUT
9+ * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
10+ * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11+ * Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "orientationlock.h"
18+
19+#include <QDBusConnection>
20+#include <QDBusInterface>
21+
22+OrientationLock::OrientationLock(QObject *parent)
23+ : QObject(parent)
24+ , m_enabled(false)
25+ , m_savedOrientation(Qt::PortraitOrientation)
26+{
27+ systemSettings = g_settings_new("com.ubuntu.touch.system");
28+ g_signal_connect(systemSettings, "changed::rotation-lock",
29+ G_CALLBACK(OrientationLock::onEnabledChangedProxy), this);
30+ m_enabled = g_settings_get_boolean(systemSettings, "rotation-lock");
31+}
32+
33+OrientationLock::~OrientationLock()
34+{
35+ g_signal_handlers_disconnect_by_data(systemSettings, this);
36+ g_object_unref(systemSettings);
37+}
38+
39+bool OrientationLock::enabled() const
40+{
41+ return m_enabled;
42+}
43+
44+Qt::ScreenOrientation OrientationLock::savedOrientation() const
45+{
46+ return m_savedOrientation;
47+}
48+
49+void OrientationLock::onEnabledChangedProxy(GSettings */*settings*/, const gchar */*key*/, gpointer data)
50+{
51+ OrientationLock* _this = static_cast<OrientationLock*>(data);
52+ _this->onEnabledChanged();
53+}
54+
55+void OrientationLock::onEnabledChanged()
56+{
57+ const bool enabled = g_settings_get_boolean(systemSettings, "rotation-lock");
58+ if (m_enabled != enabled) {
59+ m_enabled = enabled;
60+ Q_EMIT enabledChanged();
61+ }
62+}
63+
64+void OrientationLock::setSavedOrientation(const Qt::ScreenOrientation orientation)
65+{
66+ if (orientation == m_savedOrientation) {
67+ return;
68+ }
69+
70+ m_savedOrientation = orientation;
71+
72+ //TODO - save value with dbus to persist over sessions
73+ Q_EMIT savedOrientationChanged();
74+}
plugins/Unity/Session/orientationlock.hnew file+62
@@ -0,0 +1,62 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software: you can redistribute it and/or modify it under
5+ * the terms of the GNU Lesser General Public License version 3, as published by
6+ * the Free Software Foundation.
7+ *
8+ * This program is distributed in the hope that it will be useful, but WITHOUT
9+ * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
10+ * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11+ * Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef ORIENTATIONLOCK_H
18+#define ORIENTATIONLOCK_H
19+
20+#include <gio/gio.h>
21+#include <QtCore/QObject>
22+#include <QtDBus/QDBusInterface>
23+
24+/**
25+ * @brief The OrientationLock class exports orientation lock related properties to QML
26+ * It has two properties:
27+ * - readonly boolean with the Orientation lock property state
28+ * - Qt::ScreenOrientation to save the locked orientation state across Sessions (only relevant if lock is true)
29+ */
30+class OrientationLock : public QObject
31+{
32+ Q_OBJECT
33+
34+ Q_PROPERTY(bool enabled READ enabled NOTIFY enabledChanged)
35+ Q_PROPERTY(Qt::ScreenOrientation savedOrientation READ savedOrientation WRITE setSavedOrientation
36+ NOTIFY savedOrientationChanged)
37+
38+public:
39+ explicit OrientationLock(QObject *parent = 0);
40+ ~OrientationLock();
41+
42+ bool enabled() const;
43+ Qt::ScreenOrientation savedOrientation() const;
44+ void setSavedOrientation(const Qt::ScreenOrientation orientation);
45+
46+Q_SIGNALS:
47+ void enabledChanged();
48+ void savedOrientationChanged();
49+
50+private Q_SLOTS:
51+ static void onEnabledChangedProxy(GSettings *settings, const gchar *key, gpointer data);
52+ void onEnabledChanged();
53+
54+private:
55+ QDBusInterface *dbusInterface;
56+ GSettings *systemSettings;
57+
58+ bool m_enabled;
59+ Qt::ScreenOrientation m_savedOrientation;
60+};
61+
62+#endif // ORIENTATIONLOCK_H
plugins/Unity/Session/plugin.cpp+8−4
@@ -18,23 +18,27 @@
1818
1919#include "plugin.h"
2020#include "dbusunitysessionservice.h"
21+#include "orientationlock.h"
2122
2223#include <QAbstractItemModel>
2324#include <QDBusConnection>
2425#include <QtQml/qqml.h>
2526
26−static QObject *dbusunitysessionservice_provider(QQmlEngine *engine, QJSEngine *scriptEngine)
27+static QObject *dbusunitysessionservice_provider(QQmlEngine */*engine*/, QJSEngine */*jsEngine*/)
2728{
28− Q_UNUSED(engine)
29− Q_UNUSED(scriptEngine)
30−
3129 return new DBusUnitySessionService();
3230}
3331
32+static QObject *orientationlock_provider(QQmlEngine */*engine*/, QJSEngine */*jsEngine*/)
33+{
34+ return new OrientationLock();
35+}
36+
3437void SessionPlugin::registerTypes(const char *uri)
3538{
3639 qmlRegisterType<QAbstractItemModel>();
3740
3841 Q_ASSERT(uri == QLatin1String("Unity.Session"));
3942 qmlRegisterSingletonType<DBusUnitySessionService>(uri, 0, 1, "DBusUnitySessionService", dbusunitysessionservice_provider);
43+ qmlRegisterSingletonType<OrientationLock>(uri, 0, 1, "OrientationLock", orientationlock_provider);
4044}
plugins/Utils/windowkeysfilter.cpp+1
@@ -31,6 +31,7 @@ bool WindowKeysFilter::eventFilter(QObject *watched, QEvent *event)
3131{
3232 Q_ASSERT(!m_filteredWindow.isNull());
3333 Q_ASSERT(watched == static_cast<QObject*>(m_filteredWindow.data()));
34+ Q_UNUSED(watched);
3435
3536 if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) {
3637 // Let QML see this event and decide if it does not want it
po/ja.po+15−15
@@ -8,14 +8,14 @@ msgstr ""
88"Project-Id-Version: unity\n"
99"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
1010"POT-Creation-Date: 2014-08-26 18:24+0200\n"
11−"PO-Revision-Date: 2014-09-01 06:20+0000\n"
11+"PO-Revision-Date: 2014-09-18 09:47+0000\n"
1212"Last-Translator: Mitsuya Shibata <mty.shibata@gmail.com>\n"
1313"Language-Team: Japanese <ja@li.org>\n"
1414"MIME-Version: 1.0\n"
1515"Content-Type: text/plain; charset=UTF-8\n"
1616"Content-Transfer-Encoding: 8bit\n"
17−"X-Launchpad-Export-Date: 2014-09-02 07:20+0000\n"
18−"X-Generator: Launchpad (build 17192)\n"
17+"X-Launchpad-Export-Date: 2014-09-19 08:11+0000\n"
18+"X-Generator: Launchpad (build 17196)\n"
1919
2020#: plugins/LightDM/Greeter.cpp:130
2121msgid "Password: "
@@ -36,7 +36,7 @@ msgstr "ログアウト"
3636
3737#: qml/Components/Dialogs.qml:71
3838msgid "Are you sure you want to log out?"
39−msgstr ""
39+msgstr "本当にログアウトしますか?"
4040
4141#: qml/Components/Dialogs.qml:73 qml/Components/Dialogs.qml:97
4242#: qml/Components/Dialogs.qml:122
@@ -54,7 +54,7 @@ msgstr "シャットダウン"
5454
5555#: qml/Components/Dialogs.qml:95
5656msgid "Are you sure you want to shut down?"
57−msgstr ""
57+msgstr "本当にシャットダウンしますか?"
5858
5959#: qml/Components/Dialogs.qml:119
6060msgid "Reboot"
@@ -62,17 +62,17 @@ msgstr "再起動"
6262
6363#: qml/Components/Dialogs.qml:120
6464msgid "Are you sure you want to reboot?"
65−msgstr ""
65+msgstr "本当に再起動しますか?"
6666
6767#: qml/Components/Dialogs.qml:144
6868msgid "Power"
69−msgstr ""
69+msgstr "電源"
7070
7171#: qml/Components/Dialogs.qml:145
7272msgid ""
7373"Are you sure you would like\n"
7474"to power off?"
75−msgstr ""
75+msgstr "本当に電源を切りますか?"
7676
7777#: qml/Components/Dialogs.qml:147
7878msgid "Power off"
@@ -249,11 +249,11 @@ msgstr "パスワードを表示"
249249
250250#: qml/Notifications/NotificationMenuItemFactory.qml:120
251251msgid "Enter SIM PIN"
252−msgstr ""
252+msgstr "SIM PINの入力"
253253
254254#: qml/Notifications/NotificationMenuItemFactory.qml:121
255255msgid "Sorry, incorrect PIN"
256−msgstr ""
256+msgstr "PINが違います"
257257
258258#: qml/Panel/ActiveCallHint.qml:77
259259msgid "Tap to return to call..."
@@ -297,19 +297,19 @@ msgstr "ローミング"
297297
298298#: qml/Shell.qml:279
299299msgid "Enter your passphrase"
300−msgstr ""
300+msgstr "パスフレーズの入力"
301301
302302#: qml/Shell.qml:280
303303msgid "Sorry, incorrect passphrase"
304−msgstr ""
304+msgstr "パスフレーズが違います"
305305
306306#: qml/Shell.qml:282
307307msgid "Enter your passcode"
308−msgstr ""
308+msgstr "パスコードの入力"
309309
310310#: qml/Shell.qml:283
311311msgid "Sorry, incorrect passcode"
312−msgstr "ごめんなさい、誤ったパスコードです"
312+msgstr "パスコードが違います"
313313
314314#: qml/Shell.qml:286
315315#, qt-format
@@ -319,7 +319,7 @@ msgstr "%1を入力してください"
319319#: qml/Shell.qml:287
320320#, qt-format
321321msgid "Sorry, incorrect %1"
322−msgstr ""
322+msgstr "%1が違います"
323323
324324#: qml/Shell.qml:323
325325msgid "Sorry, incorrect passphrase."
po/unity8.pot+33−29
@@ -8,7 +8,7 @@ msgid ""
88msgstr ""
99"Project-Id-Version: unity8\n"
1010"Report-Msgid-Bugs-To: \n"
11−"POT-Creation-Date: 2014-08-26 18:24+0200\n"
11+"POT-Creation-Date: 2014-09-18 20:12+0800\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"
@@ -21,12 +21,12 @@ msgstr ""
2121msgid "Password: "
2222msgstr ""
2323
24−#: plugins/Unity/Launcher/launcheritem.cpp:43
25−#: plugins/Unity/Launcher/launcheritem.cpp:73
24+#: plugins/Unity/Launcher/launcheritem.cpp:44
25+#: plugins/Unity/Launcher/launcheritem.cpp:74
2626msgid "Pin shortcut"
2727msgstr ""
2828
29−#: plugins/Unity/Launcher/launcheritem.cpp:73
29+#: plugins/Unity/Launcher/launcheritem.cpp:74
3030msgid "Unpin shortcut"
3131msgstr ""
3232
@@ -79,6 +79,10 @@ msgid "Power off"
7979msgstr ""
8080
8181#: qml/Components/Dialogs.qml:157
82+msgid "Restart"
83+msgstr ""
84+
85+#: qml/Components/Dialogs.qml:167
8286msgid "Cancel"
8387msgstr ""
8488
@@ -153,36 +157,36 @@ msgstr ""
153157msgid "Hello"
154158msgstr ""
155159
156−#: qml/Dash/GenericScopeView.qml:361
160+#: qml/Dash/GenericScopeView.qml:364 qml/Dash/GenericScopeView.qml:483
157161msgid "See less"
158162msgstr ""
159163
160−#: qml/Dash/GenericScopeView.qml:361
164+#: qml/Dash/GenericScopeView.qml:364
161165msgid "See all"
162166msgstr ""
163167
164−#: qml/Dash/GenericScopeView.qml:424 qml/Dash/PageHeader.qml:260
168+#: qml/Dash/GenericScopeView.qml:427 qml/Dash/PageHeader.qml:258
165169#: qml/Panel/SearchIndicator.qml:27
166170msgid "Search"
167171msgstr ""
168172
169−#: qml/Dash/PageHeader.qml:270
173+#: qml/Dash/PageHeader.qml:268
170174msgid "Settings"
171175msgstr ""
172176
173−#: qml/Dash/PageHeader.qml:279
177+#: qml/Dash/PageHeader.qml:275
174178msgid "Remove from Favorites"
175179msgstr ""
176180
177−#: qml/Dash/PageHeader.qml:279
181+#: qml/Dash/PageHeader.qml:275
178182msgid "Add to Favorites"
179183msgstr ""
180184
181−#: qml/Dash/Previews/PreviewActionCombo.qml:34
185+#: qml/Dash/Previews/PreviewActionCombo.qml:35
182186msgid "More..."
183187msgstr ""
184188
185−#: qml/Dash/Previews/PreviewActionCombo.qml:34
189+#: qml/Dash/Previews/PreviewActionCombo.qml:35
186190msgid "Less..."
187191msgstr ""
188192
@@ -198,15 +202,15 @@ msgstr ""
198202msgid "Send"
199203msgstr ""
200204
201−#: qml/Dash/ScopesOverview.qml:201
205+#: qml/Dash/ScopesOverview.qml:206
202206msgid "Manage Dash"
203207msgstr ""
204208
205−#: qml/Dash/ScopesOverview.qml:417
209+#: qml/Dash/ScopesOverview.qml:428
206210msgid "Done"
207211msgstr ""
208212
209−#: qml/Dash/ScopesOverview.qml:443
213+#: qml/Dash/ScopesOverview.qml:454
210214msgid "Store"
211215msgstr ""
212216
@@ -242,15 +246,15 @@ msgstr ""
242246msgid "Speaking..."
243247msgstr ""
244248
245−#: qml/Notifications/NotificationMenuItemFactory.qml:105
249+#: qml/Notifications/NotificationMenuItemFactory.qml:108
246250msgid "Show password"
247251msgstr ""
248252
249−#: qml/Notifications/NotificationMenuItemFactory.qml:120
253+#: qml/Notifications/NotificationMenuItemFactory.qml:124
250254msgid "Enter SIM PIN"
251255msgstr ""
252256
253−#: qml/Notifications/NotificationMenuItemFactory.qml:121
257+#: qml/Notifications/NotificationMenuItemFactory.qml:125
254258msgid "Sorry, incorrect PIN"
255259msgstr ""
256260
@@ -294,51 +298,51 @@ msgstr ""
294298msgid "Roaming"
295299msgstr ""
296300
297−#: qml/Shell.qml:279
301+#: qml/Shell.qml:292
298302msgid "Enter your passphrase"
299303msgstr ""
300304
301−#: qml/Shell.qml:280
305+#: qml/Shell.qml:293
302306msgid "Sorry, incorrect passphrase"
303307msgstr ""
304308
305−#: qml/Shell.qml:282
309+#: qml/Shell.qml:295
306310msgid "Enter your passcode"
307311msgstr ""
308312
309−#: qml/Shell.qml:283
313+#: qml/Shell.qml:296
310314msgid "Sorry, incorrect passcode"
311315msgstr ""
312316
313−#: qml/Shell.qml:286
317+#: qml/Shell.qml:299
314318#, qt-format
315319msgid "Enter your %1"
316320msgstr ""
317321
318−#: qml/Shell.qml:287
322+#: qml/Shell.qml:300
319323#, qt-format
320324msgid "Sorry, incorrect %1"
321325msgstr ""
322326
323−#: qml/Shell.qml:323
327+#: qml/Shell.qml:335
324328msgid "Sorry, incorrect passphrase."
325329msgstr ""
326330
327−#: qml/Shell.qml:324
331+#: qml/Shell.qml:336
328332msgid "Sorry, incorrect passcode."
329333msgstr ""
330334
331−#: qml/Shell.qml:325
335+#: qml/Shell.qml:337
332336msgid "This will be your last attempt."
333337msgstr ""
334338
335−#: qml/Shell.qml:327
339+#: qml/Shell.qml:339
336340msgid ""
337341"If passphrase is entered incorrectly, your phone will conduct a factory "
338342"reset and all personal data will be deleted."
339343msgstr ""
340344
341−#: qml/Shell.qml:328
345+#: qml/Shell.qml:340
342346msgid ""
343347"If passcode is entered incorrectly, your phone will conduct a factory reset "
344348"and all personal data will be deleted."
qml/Components/Dialogs.qml+10
@@ -153,6 +153,16 @@ Item {
153153 }
154154 color: UbuntuColors.red
155155 }
156+ Button {
157+ text: i18n.tr("Restart")
158+ onClicked: {
159+ dBusUnitySessionServiceConnection.closeAllApps();
160+ DBusUnitySessionService.Reboot();
161+ PopupUtils.close(dialoguePower);
162+ d.dialogShown = false;
163+ }
164+ color: UbuntuColors.green
165+ }
156166 Button {
157167 text: i18n.tr("Cancel")
158168 onClicked: {
qml/Dash/CardCarousel.qml+1−1
@@ -49,7 +49,7 @@ DashRenderer {
4949 objectName: "carouselDelegate" + index
5050
5151 function clicked() { cardCarousel.clicked(index, model.result, loader.item, model) }
52− function pressAndHold() { cardCarousel.pressAndHold(index, model) }
52+ function pressAndHold() { cardCarousel.pressAndHold(index, model.result, model) }
5353
5454 sourceComponent: cardTool.cardComponent
5555 onLoaded: {
qml/Dash/CardGrid.qml+1−1
@@ -74,7 +74,7 @@ DashRenderer {
7474 Connections {
7575 target: loader.item
7676 onClicked: root.clicked(index, result, loader.item, model)
77− onPressAndHold: root.pressAndHold(index, model)
77+ onPressAndHold: root.pressAndHold(index, result, model)
7878 }
7979 }
8080 }
qml/Dash/CardHorizontalList.qml+1−1
@@ -55,7 +55,7 @@ DashRenderer {
5555 Connections {
5656 target: loader.item
5757 onClicked: root.clicked(index, result, loader.item, model)
58− onPressAndHold: root.pressAndHold(index, model)
58+ onPressAndHold: root.pressAndHold(index, result, model)
5959 }
6060 }
6161 }
qml/Dash/CardVerticalJournal.qml+1−1
@@ -62,7 +62,7 @@ DashRenderer {
6262 Connections {
6363 target: loader.item
6464 onClicked: root.clicked(index, result, loader.item, model)
65− onPressAndHold: root.pressAndHold(index, model)
65+ onPressAndHold: root.pressAndHold(index, result, model)
6666 }
6767 }
6868 }
qml/Dash/DashApplication.qml+2−2
@@ -19,8 +19,8 @@ import Ubuntu.Components 1.0
1919import Ubuntu.Thumbnailer 0.1 // Register support for image://thumbnailer/ and image://albumart/
2020
2121MainView {
22− width: units.gu(40)
23− height: units.gu(71)
22+ width: applicationArguments.hasGeometry() ? applicationArguments.width() : units.gu(40)
23+ height: applicationArguments.hasGeometry() ? applicationArguments.height() : units.gu(68)
2424
2525 useDeprecatedToolbar: false
2626
qml/Dash/DashNavigation.qml+2−3
@@ -37,9 +37,8 @@ Item {
3737 // FIXME this is only here for highlight purposes (see Background.qml, too)
3838 readonly property var background: backgroundItem
3939
40− visible: navigationButton.currentNavigation || altNavigationButton.currentNavigation
41−
42− height: visible ? units.gu(5) : 0
40+ visible: height != 0
41+ height: navigationButton.currentNavigation || altNavigationButton.currentNavigation ? units.gu(5) : 0
4342
4443 QtObject {
4544 id: d
qml/Dash/DashRenderer.qml+3−2
@@ -40,13 +40,14 @@ Item {
4040
4141 /// Emitted when the user clicked on an item
4242 /// @param index is the index of the clicked item
43− /// @param result result model of the cliked item, used for activation
43+ /// @param result result model of the clicked item, used for activation
4444 /// @param item item that has been clicked
4545 /// @param itemModel model of the item
4646 signal clicked(int index, var result, var item, var itemModel)
4747
4848 /// Emitted when the user pressed and held on an item
4949 /// @param index is the index of the held item
50+ /// @param result result model of the clicked item, used for activation
5051 /// @param itemModel model of the item
51− signal pressAndHold(int index, var itemModel)
52+ signal pressAndHold(int index, var result, var itemModel)
5253}
qml/Dash/GenericScopeView.qml+85−35
@@ -47,6 +47,10 @@ FocusScope {
4747
4848 signal backClicked()
4949
50+ onScopeChanged: {
51+ floatingSeeLess.companionBase = null;
52+ }
53+
5054 function positionAtBeginning() {
5155 categoryView.positionAtBeginning()
5256 }
@@ -66,13 +70,17 @@ FocusScope {
6670 // so it's not implemented
6771 scope.activate(result)
6872 } else {
69− openPreview(index, resultsModel, limitedCategoryItemCount);
73+ if (scope.preview(result)) {
74+ openPreview(index, resultsModel, limitedCategoryItemCount);
75+ }
7076 }
7177 }
7278
73− function itemPressedAndHeld(index, itemModel, resultsModel, limitedCategoryItemCount) {
79+ function itemPressedAndHeld(index, result, itemModel, resultsModel, limitedCategoryItemCount) {
7480 if (itemModel.uri.indexOf("scope://") !== 0) {
75− openPreview(index, resultsModel, limitedCategoryItemCount);
81+ if (scope.preview(result)) {
82+ openPreview(index, resultsModel, limitedCategoryItemCount);
83+ }
7684 }
7785 }
7886
@@ -142,6 +150,7 @@ FocusScope {
142150 objectName: "categoryListView"
143151
144152 x: subPageLoader.open ? -width : 0
153+ visible: x != -width
145154 Behavior on x { UbuntuNumberAnimation { } }
146155 width: parent.width
147156 height: floatingSeeLess.visible ? parent.height - floatingSeeLess.height + floatingSeeLess.yOffset
@@ -152,11 +161,54 @@ FocusScope {
152161 forceNoClip: subPageLoader.open
153162 pixelAligned: true
154163
155− property Item expandedCategoryItem: null
164+ property string expandedCategoryId: ""
165+ property int runMaximizeAfterSizeChanges: 0
156166
157167 readonly property bool pageHeaderTotallyVisible: scopeView.showPageHeader &&
158168 ((headerItemShownHeight == 0 && categoryView.contentY <= categoryView.originY) || (headerItemShownHeight == pageHeaderLoader.item.height))
159169
170+ onExpandedCategoryIdChanged: {
171+ var firstCreated = firstCreatedIndex();
172+ var shrinkingAny = false;
173+ var shrinkHeightDifference = 0;
174+ for (var i = 0; i < createdItemCount(); ++i) {
175+ var baseItem = item(firstCreated + i);
176+ if (baseItem.expandable) {
177+ var shouldExpand = baseItem.category === expandedCategoryId;
178+ if (shouldExpand != baseItem.expanded) {
179+ var animate = false;
180+ if (!subPageLoader.open) {
181+ var animateShrinking = !shouldExpand && baseItem.y + baseItem.item.collapsedHeight + baseItem.seeAllButton.height < categoryView.height;
182+ var animateGrowing = shouldExpand && baseItem.y + baseItem.height < categoryView.height;
183+ animate = shrinkingAny || animateShrinking || animateGrowing;
184+ }
185+
186+ if (!shouldExpand) {
187+ shrinkingAny = true;
188+ shrinkHeightDifference = baseItem.item.expandedHeight - baseItem.item.collapsedHeight;
189+ }
190+
191+ if (shouldExpand && !subPageLoader.open) {
192+ if (!shrinkingAny) {
193+ categoryView.maximizeVisibleArea(firstCreated + i, baseItem.item.expandedHeight + baseItem.seeAllButton.height);
194+ } else {
195+ // If the space that shrinking is smaller than the one we need to grow we'll call maximizeVisibleArea
196+ // after the shrink/grow animation ends
197+ var growHeightDifference = baseItem.item.expandedHeight - baseItem.item.collapsedHeight;
198+ if (growHeightDifference > shrinkHeightDifference) {
199+ runMaximizeAfterSizeChanges = 2;
200+ } else {
201+ runMaximizeAfterSizeChanges = 0;
202+ }
203+ }
204+ }
205+
206+ baseItem.expand(shouldExpand, animate);
207+ }
208+ }
209+ }
210+ }
211+
160212 delegate: ListItems.Base {
161213 id: baseItem
162214 objectName: "dashCategory" + category
@@ -194,7 +246,7 @@ FocusScope {
194246 // This can happen with the VJ that doesn't know how height it will be on creation
195247 // so doesn't set expandable until a bit too late for onLoaded
196248 if (expandable) {
197− var shouldExpand = baseItem === categoryView.expandedCategoryItem;
249+ var shouldExpand = baseItem.category === categoryView.expandedCategoryId;
198250 baseItem.expand(shouldExpand, false /*animate*/);
199251 }
200252 }
@@ -215,9 +267,23 @@ FocusScope {
215267 id: heightBehaviour
216268 enabled: false
217269 animation: UbuntuNumberAnimation {
270+ duration: UbuntuAnimation.FastDuration
218271 onRunningChanged: {
219272 if (!running) {
220273 heightBehaviour.enabled = false
274+ if (categoryView.runMaximizeAfterSizeChanges > 0) {
275+ categoryView.runMaximizeAfterSizeChanges--;
276+ if (categoryView.runMaximizeAfterSizeChanges == 0) {
277+ var firstCreated = categoryView.firstCreatedIndex();
278+ for (var i = 0; i < categoryView.createdItemCount(); ++i) {
279+ var baseItem = categoryView.item(firstCreated + i);
280+ if (baseItem.category === categoryView.expandedCategoryId) {
281+ categoryView.maximizeVisibleArea(firstCreated + i, baseItem.item.expandedHeight + baseItem.seeAllButton.height);
282+ break;
283+ }
284+ }
285+ }
286+ }
221287 }
222288 }
223289 }
@@ -245,7 +311,7 @@ FocusScope {
245311 item.objectName = Qt.binding(function() { return categoryId })
246312 item.scopeStyle = scopeView.scopeStyle;
247313 if (baseItem.expandable) {
248− var shouldExpand = baseItem === categoryView.expandedCategoryItem;
314+ var shouldExpand = baseItem.category === categoryView.expandedCategoryId;
249315 baseItem.expand(shouldExpand, false /*animate*/);
250316 }
251317 updateDelegateCreationRange();
@@ -269,7 +335,7 @@ FocusScope {
269335 }
270336
271337 onPressAndHold: {
272− scopeView.itemPressedAndHeld(index, itemModel, target.model, categoryItemCount());
338+ scopeView.itemPressedAndHeld(index, result, itemModel, target.model, categoryItemCount());
273339 }
274340
275341 function categoryItemCount() {
@@ -282,27 +348,6 @@ FocusScope {
282348 }
283349 Connections {
284350 target: categoryView
285− onExpandedCategoryItemChanged: {
286− collapseAllButExpandedCategory();
287− }
288− function collapseAllButExpandedCategory() {
289− var item = rendererLoader.item;
290− if (baseItem.expandable) {
291− var shouldExpand = baseItem === categoryView.expandedCategoryItem;
292− if (shouldExpand != baseItem.expanded) {
293− // If the filter animation will be seen start it, otherwise, just flip the switch
294− var shrinkingVisible = !shouldExpand && y + item.collapsedHeight + seeAll.height < categoryView.height;
295− var growingVisible = shouldExpand && y + height < categoryView.height;
296− if (!subPageLoader.open || shouldExpand) {
297− var animate = shrinkingVisible || growingVisible;
298− baseItem.expand(shouldExpand, animate)
299− if (shouldExpand && !subPageLoader.open) {
300− categoryView.maximizeVisibleArea(index, item.expandedHeight + seeAll.height);
301− }
302− }
303− }
304− }
305− }
306351 onOriginYChanged: rendererLoader.updateDelegateCreationRange();
307352 onContentYChanged: rendererLoader.updateDelegateCreationRange();
308353 onHeightChanged: rendererLoader.updateDelegateCreationRange();
@@ -349,13 +394,15 @@ FocusScope {
349394 left: parent.left
350395 right: parent.right
351396 }
352− height: seeAllLabel.visible ? seeAllLabel.font.pixelSize + units.gu(4) : 0
397+ height: baseItem.expandable && !baseItem.headerLink ? seeAllLabel.font.pixelSize + units.gu(4) : 0
398+ visible: height != 0
353399
354400 onClicked: {
355− if (categoryView.expandedCategoryItem !== baseItem) {
356− categoryView.expandedCategoryItem = baseItem;
401+ if (categoryView.expandedCategoryId !== baseItem.category) {
402+ categoryView.expandedCategoryId = baseItem.category;
403+ floatingSeeLess.companionBase = baseItem;
357404 } else {
358− categoryView.expandedCategoryItem = null;
405+ categoryView.expandedCategoryId = "";
359406 }
360407 }
361408
@@ -369,7 +416,6 @@ FocusScope {
369416 fontSize: "small"
370417 font.weight: Font.Bold
371418 color: scopeStyle ? scopeStyle.foreground : Theme.palette.normal.baseText
372− visible: baseItem.expandable && !baseItem.headerLink
373419 }
374420 }
375421
@@ -455,7 +501,7 @@ FocusScope {
455501 objectName: "floatingSeeLess"
456502
457503 property Item companionTo: companionBase ? companionBase.seeAllButton : null
458− property Item companionBase: categoryView.expandedCategoryItem
504+ property Item companionBase: null
459505 property bool showBecausePosition: false
460506 property real yOffset: 0
461507
@@ -467,7 +513,7 @@ FocusScope {
467513 height: seeLessLabel.font.pixelSize + units.gu(4)
468514 visible: companionTo && showBecausePosition
469515
470− onClicked: categoryView.expandedCategoryItem = null;
516+ onClicked: categoryView.expandedCategoryId = "";
471517
472518 function updateVisibility() {
473519 var companionPos = companionTo.mapToItem(floatingSeeLess, 0, 0);
@@ -476,6 +522,10 @@ FocusScope {
476522 var posToBase = floatingSeeLess.mapToItem(companionBase, 0, -yOffset).y;
477523 yOffset = Math.max(0, companionBase.item.collapsedHeight - posToBase);
478524 yOffset = Math.min(yOffset, height);
525+
526+ if (!showBecausePosition && categoryView.expandedCategoryId === "") {
527+ companionBase = null;
528+ }
479529 }
480530
481531 Label {
qml/Dash/PageHeader.qml+4−3
@@ -37,7 +37,7 @@ Item {
3737 property ListModel searchHistory: SearchHistoryModel
3838 property alias searchQuery: searchTextField.text
3939 property alias searchHint: searchTextField.placeholderText
40− property alias showSignatureLine: bottomBorder.visible
40+ property bool showSignatureLine: true
4141
4242 property alias bottomItem: bottomContainer.children
4343 property int paginationCount: 0
@@ -132,7 +132,7 @@ Item {
132132 objectName: "headerContainer"
133133 clip: contentY < height
134134 anchors { left: parent.left; top: parent.top; right: parent.right }
135− height: units.gu(6.5)
135+ height: units.gu(7)
136136 contentHeight: headersColumn.height
137137 interactive: false
138138 contentY: showSearch ? 0 : height
@@ -287,7 +287,7 @@ Item {
287287 id: imageComponent
288288
289289 Item {
290− anchors { fill: parent; topMargin: units.gu(1); bottomMargin: units.gu(1) }
290+ anchors { fill: parent; topMargin: units.gu(1.5); bottomMargin: units.gu(1.5) }
291291 clip: true
292292 Image {
293293 objectName: "titleImage"
@@ -346,6 +346,7 @@ Item {
346346
347347 Rectangle {
348348 id: bottomBorder
349+ visible: showSignatureLine
349350 anchors {
350351 top: headerContainer.bottom
351352 left: parent.left
qml/Dash/Previews/PreviewPayments.qml+6
@@ -63,6 +63,12 @@ PreviewWidget {
6363 paymentButton.opacity = 1;
6464 root.triggered(widgetId, "purchaseError", source);
6565 }
66+ onPurchaseCancelled: {
67+ paymentButton.opacity = 1;
68+ // DO NOT emit the signal right now, as it causes a new
69+ // preview to be requested, when that is not what we want.
70+ //root.triggered(widgetId, "purchaseCancelled", source);
71+ }
6672 }
6773 }
6874
qml/Dash/ScopesOverview.qml+1−1
@@ -203,7 +203,7 @@ Item {
203203 }
204204 width: parent.width
205205 clip: true
206− title: i18n.tr("Manage Dash")
206+ title: i18n.tr("Manage Scopes")
207207 scopeStyle: overviewScopeStyle
208208 showSignatureLine: false
209209 searchEntryEnabled: true
qml/Launcher/Launcher.qml+1−2
@@ -243,10 +243,9 @@ Item {
243243 onDraggingChanged: {
244244 if (!dragging) {
245245 if (distance > panel.width / 2) {
246+ root.switchToNextState("visible")
246247 if (distance > minimizeDistance) {
247248 root.dash();
248− } else {
249− root.switchToNextState("visible")
250249 }
251250 } else {
252251 root.switchToNextState("")
qml/Launcher/LauncherDelegate.qml+4−2
@@ -21,7 +21,8 @@ Item {
2121 id: root
2222
2323 property string iconName
24− property int count: -1
24+ property int count: 0
25+ property bool countVisible: false
2526 property int progress: -1
2627 property bool itemFocused: false
2728 property real maxAngle: 0
@@ -80,11 +81,12 @@ Item {
8081 width: Math.min(root.itemWidth, Math.max(units.gu(2), countLabel.implicitWidth + units.gu(1)))
8182 height: units.gu(2)
8283 color: UbuntuColors.orange
83− visible: root.count > 0
84+ visible: root.countVisible
8485 borderSource: "none"
8586
8687 Label {
8788 id: countLabel
89+ objectName: "countLabel"
8890 text: root.count
8991 anchors.centerIn: parent
9092 // FIXME: verticalCenter seems to be off wee bit and QML doesn't have a centerLine
qml/Launcher/LauncherPanel.qml+1
@@ -156,6 +156,7 @@ Rectangle {
156156 height: itemHeight
157157 iconName: model.icon
158158 count: model.count
159+ countVisible: model.countVisible
159160 progress: model.progress
160161 clipCorner: model.pinned
161162 itemFocused: model.focused
qml/Notifications/Notification.qml+2−2
@@ -82,7 +82,7 @@ Item {
8282
8383 // FIXME: using onCompleted because of LP: #1354406 workaround, has to be onOpacityChanged really
8484 Component.onCompleted: {
85− if (opacity == 1.0 && hints["suppress-sound"] != "true" && sound.source) {
85+ if (opacity == 1.0 && hints["suppress-sound"] != "true" && sound.source != "") {
8686 sound.play();
8787 }
8888 }
@@ -262,7 +262,7 @@ Item {
262262 fontSize: "small"
263263 color: type == Notification.SnapDecision ? sdFontColor : Theme.palette.selected.backgroundText
264264 wrapMode: Text.WordWrap
265− maximumLineCount: 2
265+ maximumLineCount: type == Notification.SnapDecision ? 12 : 2
266266 elide: Text.ElideRight
267267 textFormat: Text.PlainText
268268 }
qml/Shell.qml+33−1
@@ -15,6 +15,7 @@
1515 */
1616
1717import QtQuick 2.0
18+import QtQuick.Window 2.0
1819import AccountsService 0.1
1920import GSettings 1.0
2021import Unity.Application 0.1
@@ -61,6 +62,22 @@ Item {
6162 property int failedLoginsDelayAttempts: 7 // number of failed logins
6263 property int failedLoginsDelaySeconds: 5 * 60 // seconds of forced waiting
6364
65+ property int orientation
66+ readonly property int deviceOrientationAngle: Screen.angleBetween(Screen.primaryOrientation, Screen.orientation)
67+ onDeviceOrientationAngleChanged: {
68+ if (!OrientationLock.enabled) {
69+ orientation = Screen.orientation;
70+ }
71+ }
72+ readonly property bool orientationLockEnabled: OrientationLock.enabled
73+ onOrientationLockEnabledChanged: {
74+ if (orientationLockEnabled) {
75+ OrientationLock.savedOrientation = Screen.orientation;
76+ } else {
77+ orientation = Screen.orientation;
78+ }
79+ }
80+
6481 function activateApplication(appId) {
6582 if (ApplicationManager.findApplication(appId)) {
6683 ApplicationManager.requestFocusApplication(appId);
@@ -85,6 +102,12 @@ Item {
85102
86103 Component.onCompleted: {
87104 Theme.name = "Ubuntu.Components.Themes.SuruGradient"
105+ if (ApplicationManager.count > 0) {
106+ ApplicationManager.focusApplication(ApplicationManager.get(0).appId);
107+ }
108+ if (orientationLockEnabled) {
109+ orientation = OrientationLock.savedOrientation;
110+ }
88111 }
89112
90113 GSettings {
@@ -215,6 +238,11 @@ Item {
215238 property: "inverseProgress"
216239 value: launcher.progress
217240 }
241+ Binding {
242+ target: applicationsDisplayLoader.item
243+ property: "orientation"
244+ value: shell.orientation
245+ }
218246 }
219247 }
220248
@@ -544,7 +572,11 @@ Item {
544572 available: edgeDemo.launcherEnabled && (!shell.locked || AccountsService.enableLauncherWhileLocked) && greeter.fakeActiveForApp === ""
545573
546574 onShowDashHome: showHome()
547− onDash: showDash()
575+ onDash: {
576+ if (ApplicationManager.focusedApplicationId != "unity8-dash") {
577+ showDash()
578+ }
579+ }
548580 onDashSwipeChanged: {
549581 if (dashSwipe) {
550582 dash.setCurrentScope("clickscope", false, true)
qml/Stages/ApplicationWindow.qml+2
@@ -27,6 +27,7 @@ Item {
2727
2828 // to be set from outside
2929 property QtObject application
30+ property int orientation
3031
3132 QtObject {
3233 id: d
@@ -111,6 +112,7 @@ Item {
111112 id: sessionContainer
112113 session: application ? application.session : null
113114 anchors.fill: parent
115+ orientation: root.orientation
114116
115117 onSurfaceChanged: {
116118 if (sessionContainer.surface) {
qml/Stages/PhoneStage.qml+9
@@ -18,6 +18,7 @@ import QtQuick 2.0
1818import Ubuntu.Components 0.1
1919import Ubuntu.Gestures 0.1
2020import Unity.Application 0.1
21+import Unity.Session 0.1
2122import Utils 0.1
2223import "../Components"
2324
@@ -30,6 +31,7 @@ Rectangle {
3031 property bool interactive
3132 property bool spreadEnabled: true // If false, animations and right edge will be disabled
3233 property real inverseProgress: 0 // This is the progress for left edge drags, in pixels.
34+ property int orientation: Qt.PortraitOrientation
3335
3436 color: "black"
3537
@@ -372,6 +374,13 @@ Rectangle {
372374 progress: appDelegate.progress - spreadView.positionMarker1
373375 }
374376
377+ Binding {
378+ target: appDelegate
379+ property: "orientation"
380+ when: appDelegate.interactive
381+ value: root.orientation
382+ }
383+
375384 onClicked: {
376385 if (spreadView.phase == 2) {
377386 if (ApplicationManager.focusedApplicationId == ApplicationManager.get(index).appId) {
qml/Stages/SessionContainer.qml+7
@@ -24,12 +24,14 @@ Item {
2424 readonly property var childSessions: session ? session.childSessions : null
2525 readonly property alias surface: _surfaceContainer.surface
2626 property bool interactive: true
27+ property int orientation
2728
2829 readonly property alias surfaceContainer: _surfaceContainer
2930 SurfaceContainer {
3031 id: _surfaceContainer
3132 anchors.fill: parent
3233 surface: session ? session.surface : null
34+ orientation: root.orientation
3335 }
3436
3537 Binding {
@@ -74,6 +76,11 @@ Item {
7476 target: item; when: item
7577 property: "height"; value: root.height
7678 }
79+
80+ Binding {
81+ target: item; when: item
82+ property: "orientation"; value: root.orientation
83+ }
7784 }
7885 }
7986
qml/Stages/SpreadDelegate.qml+3
@@ -36,6 +36,7 @@ Item {
3636 property alias swipeToCloseEnabled: dragArea.enabled
3737 property bool closeable
3838 property alias application: appWindow.application
39+ property int orientation
3940
4041 Item {
4142 objectName: "appWindowWithShadow"
@@ -56,12 +57,14 @@ Item {
5657
5758 ApplicationWindow {
5859 id: appWindow
60+ objectName: application ? "appWindow_" + application.appId : "appWindow_null"
5961 anchors {
6062 fill: parent
6163 topMargin: appWindow.fullscreen ? 0 : maximizedAppTopMargin
6264 }
6365
6466 interactive: root.interactive
67+ orientation: root.orientation
6568 }
6669 }
6770
qml/Stages/SurfaceContainer.qml+5
@@ -22,6 +22,7 @@ Item {
2222 objectName: "surfaceContainer"
2323 property Item surface: null
2424 property bool hadSurface: false
25+ property int orientation
2526
2627 onSurfaceChanged: {
2728 if (surface) {
@@ -34,6 +35,10 @@ Item {
3435 target: surface
3536 property: "anchors.fill"; value: root
3637 }
38+ Binding {
39+ target: surface
40+ property: "orientation"; value: root.orientation
41+ }
3742
3843 states: [
3944 State {
qml/Stages/TabletStage.qml+8
@@ -34,6 +34,7 @@ Rectangle {
3434 property real maximizedAppTopMargin
3535 property bool interactive
3636 property real inverseProgress: 0 // This is the progress for left edge drags, in pixels.
37+ property int orientation: Qt.PortraitOrientation
3738
3839 onInverseProgressChanged: {
3940 // This can't be a simple binding because that would be triggered after this handler
@@ -535,6 +536,13 @@ Rectangle {
535536 return progress;
536537 }
537538
539+ Binding {
540+ target: spreadTile
541+ property: "orientation"
542+ when: spreadTile.interactive
543+ value: root.orientation
544+ }
545+
538546 onClicked: {
539547 if (spreadView.phase == 2) {
540548 spreadView.snapTo(index);
src/Dash/CMakeLists.txt+1
@@ -1,5 +1,6 @@
11set(DASH_SRCS
22 main.cpp
3+ ../ApplicationArguments.h
34 ../MouseTouchAdaptor.cpp
45 ../CachingNetworkManagerFactory.cpp
56)
src/Dash/main.cpp+16−2
@@ -28,6 +28,7 @@
2828
2929#include <paths.h>
3030#include "../MouseTouchAdaptor.h"
31+#include "../ApplicationArguments.h"
3132#include "../CachingNetworkManagerFactory.h"
3233
3334int main(int argc, const char *argv[])
@@ -42,7 +43,11 @@ int main(int argc, const char *argv[])
4243 "Allow the mouse to provide touch input");
4344 parser.addOption(mousetouchOption);
4445
45− // FIXME Remove once we drop the need of the hint
46+ QCommandLineOption windowGeometryOption(QStringList() << "windowgeometry",
47+ "Specify the window geometry as [<width>x<height>]", "windowgeometry", "1");
48+ parser.addOption(windowGeometryOption);
49+
50+ // FIXME Remove once we drop the need of the hint
4651 QCommandLineOption desktopFileHintOption("desktop_file_hint",
4752 "The desktop_file_hint option for QtMir", "hint_file");
4853 parser.addOption(desktopFileHintOption);
@@ -52,6 +57,14 @@ int main(int argc, const char *argv[])
5257 parser.setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions);
5358 parser.process(*application);
5459
60+ ApplicationArguments qmlArgs;
61+ if (parser.isSet(windowGeometryOption) &&
62+ parser.value(windowGeometryOption).split('x').size() == 2)
63+ {
64+ QStringList geom = parser.value(windowGeometryOption).split('x');
65+ qmlArgs.setSize(geom.at(0).toInt(), geom.at(1).toInt());
66+ }
67+
5568 if (getenv("QT_LOAD_TESTABILITY")) {
5669 QLibrary testLib(QLatin1String("qttestability"));
5770 if (testLib.load()) {
@@ -72,7 +85,8 @@ int main(int argc, const char *argv[])
7285
7386 QQuickView* view = new QQuickView();
7487 view->setResizeMode(QQuickView::SizeRootObjectToView);
75− view->setTitle("Unity Dash");
88+ view->setTitle("Scopes");
89+ view->rootContext()->setContextProperty("applicationArguments", &qmlArgs);
7690
7791 // You will need this if you want to interact with touch-only components using a mouse
7892 // Needed only when manually testing on a desktop.
tests/autopilot/unity8/shell/tests/__init__.py+4−21
@@ -432,12 +432,6 @@ class DashBaseTestCase(AutopilotTestCase):
432432 binary_path = get_binary_path('unity8-dash')
433433 dash_proxy = self.launch_dash(binary_path, self.environment)
434434
435− if self.should_simulate_device():
436− # XXX Currently we have no way to launch the application with a
437− # specific size, so we must resize it after it's launched.
438− # --elopio - 2014-06-25
439− self.resize_window()
440−
441435 self.dash_app = dash_helpers.DashApp(dash_proxy)
442436 self.dash = self.dash_app.dash
443437 self.wait_for_dash()
@@ -470,18 +464,7 @@ class DashBaseTestCase(AutopilotTestCase):
470464 simulate_device_fixture = self.useFixture(
471465 toolkit_fixtures.SimulateDevice(
472466 self.app_width, self.app_height, self.grid_unit_px))
473− self.app_width = simulate_device_fixture.app_width
474− self.app_height = simulate_device_fixture.app_height
475−
476− def resize_window(self):
477− application = self.process_manager.get_running_applications()[0]
478− window = application.get_windows()[0]
479− window.resize(self.app_width, self.app_height)
480−
481− def get_window_size():
482− _, _, window_width, window_height = window.geometry
483− return window_width, window_height
484−
485− self.assertThat(
486− get_window_size,
487− Eventually(Equals((self.app_width, self.app_height))))
467+ self.environment['GRID_UNIT_PX'] = simulate_device_fixture.grid_unit_px
468+ self.environment['ARGS'] = '-windowgeometry {0}x{1}'\
469+ .format(simulate_device_fixture.app_width,
470+ simulate_device_fixture.app_height)
tests/mocks/HudClient/fake_libhud_client.cpp+8−1
@@ -63,6 +63,7 @@ HudClientQuery *hud_client_query_new(const gchar *query)
6363DeeModel *hud_client_query_get_results_model(HudClientQuery *cquery)
6464{
6565 Q_ASSERT(cquery == HudClientStub::m_query);
66+ Q_UNUSED(cquery);
6667 if (!resultsModel) {
6768 resultsModel = dee_sequence_model_new();
6869 dee_model_set_schema_full(resultsModel, results_model_schema, G_N_ELEMENTS(results_model_schema));
@@ -113,18 +114,21 @@ void hud_client_query_execute_toolbar_item(HudClientQuery *cquery, HudClientQuer
113114{
114115 Q_ASSERT(cquery == HudClientStub::m_query);
115116 Q_UNUSED(timestamp);
117+ Q_UNUSED(cquery);
116118 HudClientStub::m_lastExecutedToolbarItem = item;
117119}
118120
119121void hud_client_query_set_query(HudClientQuery *cquery, const char *query)
120122{
121123 Q_ASSERT(cquery == HudClientStub::m_query);
124+ Q_UNUSED(cquery);
122125 HudClientStub::m_lastSetQuery = QString::fromUtf8(query);
123126}
124127
125128void hud_client_query_execute_command(HudClientQuery *cquery, GVariant *command_key, guint timestamp)
126129{
127130 Q_ASSERT(cquery == HudClientStub::m_query);
131+ Q_UNUSED(cquery);
128132 Q_UNUSED(timestamp);
129133 for (uint i = 0; i < dee_model_get_n_rows(resultsModel); ++i) {
130134 DeeModelIter *iter = dee_model_get_iter_at_row(resultsModel, i);
@@ -144,6 +148,7 @@ void hud_client_query_execute_command(HudClientQuery *cquery, GVariant *command_
144148HudClientParam *hud_client_query_execute_param_command(HudClientQuery *cquery, GVariant *command_key, guint timestamp)
145149{
146150 Q_ASSERT(cquery == HudClientStub::m_query);
151+ Q_UNUSED(cquery);
147152 Q_UNUSED(timestamp);
148153 for (uint i = 0; i < dee_model_get_n_rows(resultsModel); ++i) {
149154 DeeModelIter *iter = dee_model_get_iter_at_row(resultsModel, i);
@@ -165,12 +170,14 @@ HudClientParam *hud_client_query_execute_param_command(HudClientQuery *cquery, G
165170void hud_client_query_voice_query(HudClientQuery *cquery)
166171{
167172 Q_ASSERT(cquery == HudClientStub::m_query);
173+ Q_UNUSED(cquery);
168174 // TODO We are not testing voice queries yet
169175}
170176
171177gboolean hud_client_query_toolbar_item_active(HudClientQuery *cquery, HudClientQueryToolbarItems item)
172178{
173179 Q_ASSERT(cquery == HudClientStub::m_query);
180+ Q_UNUSED(cquery);
174181 if (item == HUD_CLIENT_QUERY_TOOLBAR_HELP)
175182 return HudClientStub::m_helpToolbarItemEnabled;
176183
@@ -218,7 +225,7 @@ GActionGroup *hud_client_param_get_actions(HudClientParam *param)
218225 if (!parametrizedActionGroup) {
219226 GSimpleActionGroup *actionGroup = g_simple_action_group_new();
220227 GSimpleAction *action = g_simple_action_new("costAction", G_VARIANT_TYPE_DOUBLE);
221− g_simple_action_group_insert(actionGroup, G_ACTION(action));
228+ g_action_map_add_action(G_ACTION_MAP(actionGroup), G_ACTION(action));
222229
223230 g_signal_connect (action, "activate", G_CALLBACK (on_signal_activated), nullptr);
224231
tests/mocks/QtMultimedia/audio.cpp+1−1
@@ -77,7 +77,7 @@ void Audio::pause()
7777
7878void Audio::play()
7979{
80− if (m_playbackState != PlayingState && m_source.isValid()) {
80+ if (m_playbackState != PlayingState) {
8181 m_playbackState = PlayingState;
8282 Q_EMIT playbackStateChanged(m_playbackState);
8383
tests/mocks/Ubuntu/Payments/MockPayments.cpp+3−1
@@ -84,7 +84,9 @@ void MockPayments::start()
8484
8585void MockPayments::process()
8686{
87− if (m_store_item_id == "com.example.invalid") {
87+ if (m_store_item_id == "com.example.cancel") {
88+ Q_EMIT purchaseCancelled();
89+ } else if (m_store_item_id == "com.example.invalid") {
8890 Q_EMIT purchaseError("Purchase failed.");
8991 } else {
9092 Q_EMIT purchaseCompleted();
tests/mocks/Ubuntu/Payments/MockPayments.h+1
@@ -51,6 +51,7 @@ Q_SIGNALS:
5151
5252 void purchaseError(const QString &error);
5353 void purchaseCompleted();
54+ void purchaseCancelled();
5455
5556private:
5657 QString m_currency;
tests/mocks/Unity/Application/MirSurfaceItem.cpp+14
@@ -39,6 +39,7 @@ MirSurfaceItem::MirSurfaceItem(const QString& name,
3939 , m_type(type)
4040 , m_state(state)
4141 , m_live(true)
42+ , m_orientation(Qt::PortraitOrientation)
4243 , m_qmlItem(nullptr)
4344 , m_screenshotUrl(screenshot)
4445{
@@ -108,6 +109,19 @@ void MirSurfaceItem::release()
108109 }
109110}
110111
112+void MirSurfaceItem::setOrientation(const Qt::ScreenOrientation orientation)
113+{
114+ if (m_orientation == orientation)
115+ return;
116+
117+ m_orientation = orientation;
118+
119+ QQmlProperty orientationProp(m_qmlItem, "orientation");
120+ orientationProp.write(QVariant::fromValue(orientation));
121+
122+ Q_EMIT orientationChanged();
123+}
124+
111125void MirSurfaceItem::setSession(Session* session)
112126{
113127 m_session = session;
tests/mocks/Unity/Application/MirSurfaceItem.h+7
@@ -34,6 +34,7 @@ class MirSurfaceItem : public QQuickItem
3434 Q_PROPERTY(State state READ state NOTIFY stateChanged)
3535 Q_PROPERTY(QString name READ name CONSTANT)
3636 Q_PROPERTY(bool live READ live NOTIFY liveChanged)
37+ Q_PROPERTY(Qt::ScreenOrientation orientation READ orientation WRITE setOrientation NOTIFY orientationChanged DESIGNABLE false)
3738
3839public:
3940 enum Type {
@@ -64,6 +65,9 @@ public:
6465 State state() const { return m_state; }
6566 QString name() const { return m_name; }
6667 bool live() const { return m_live; }
68+ Qt::ScreenOrientation orientation() const { return m_orientation; }
69+
70+ void setOrientation(const Qt::ScreenOrientation orientation);
6771
6872 void setSession(Session* item);
6973 void setScreenshot(const QUrl& screenshot);
@@ -76,6 +80,7 @@ Q_SIGNALS:
7680 void typeChanged(Type);
7781 void stateChanged(State);
7882 void liveChanged(bool live);
83+ void orientationChanged();
7984
8085 void inputMethodRequested();
8186 void inputMethodDismissed();
@@ -104,6 +109,7 @@ private:
104109 const Type m_type;
105110 State m_state;
106111 bool m_live;
112+ Qt::ScreenOrientation m_orientation;
107113
108114 QQmlComponent *m_qmlContentComponent;
109115 QQuickItem *m_qmlItem;
@@ -114,5 +120,6 @@ private:
114120
115121Q_DECLARE_METATYPE(MirSurfaceItem*)
116122Q_DECLARE_METATYPE(QList<MirSurfaceItem*>)
123+Q_DECLARE_METATYPE(Qt::ScreenOrientation)
117124
118125#endif // MIRSURFACEITEM_H
tests/mocks/Unity/Application/MirSurfaceItem.qml+12−2
@@ -21,12 +21,22 @@ Rectangle {
2121 id: root
2222 color: "pink"
2323
24− anchors.fill: parent
25−
2624 implicitWidth: units.gu(40)
2725 implicitHeight: units.gu(70)
2826
27+ rotation: {
28+ if (orientation == Qt.PortraitOrientation) return 0;
29+ else if (orientation == Qt.LandscapeOrientation) return 90;
30+ else if (orientation == Qt.InvertedPortraitOrientation) return 180;
31+ else return 270;
32+ }
33+ x: parent ? (parent.width - width) / 2 : 0
34+ y: parent ? (parent.height - height) / 2 : 0
35+ width: parent ? (rotation == 0 || rotation == 180 ? parent.width : parent.height) : implicitWidth
36+ height: parent ? (rotation == 0 || rotation == 180 ? parent.height : parent.width) : implicitHeight
37+
2938 property alias screenshotSource: screenshotImage.source
39+ property int orientation: Qt.PortraitOrientation
3040
3141 property bool wantInputMethod: false
3242
tests/mocks/Unity/Launcher/CMakeLists.txt+1−1
@@ -1,4 +1,4 @@
1−pkg_check_modules(LAUNCHER_API REQUIRED unity-shell-launcher=3)
1+pkg_check_modules(LAUNCHER_API REQUIRED unity-shell-launcher=4)
22
33include_directories(
44 ${CMAKE_CURRENT_SOURCE_DIR}
tests/mocks/Unity/Launcher/MockLauncherItem.cpp+14
@@ -35,6 +35,7 @@ MockLauncherItem::MockLauncherItem(const QString &appId, const QString& desktopF
3535 m_recent(false),
3636 m_progress(-1),
3737 m_count(0),
38+ m_countVisible(false),
3839 m_focused(false),
3940 m_quickList(new MockQuickListModel(this))
4041{
@@ -131,6 +132,19 @@ void MockLauncherItem::setCount(int count)
131132 }
132133}
133134
135+bool MockLauncherItem::countVisible() const
136+{
137+ return m_countVisible;
138+}
139+
140+void MockLauncherItem::setCountVisible(bool countVisible)
141+{
142+ if (m_countVisible != countVisible) {
143+ m_countVisible = countVisible;
144+ Q_EMIT countVisibleChanged(countVisible);
145+ }
146+}
147+
134148bool MockLauncherItem::focused() const
135149{
136150 return m_focused;
tests/mocks/Unity/Launcher/MockLauncherItem.h+18−17
@@ -32,32 +32,30 @@ class MockLauncherItem: public LauncherItemInterface
3232public:
3333 MockLauncherItem(const QString &appId, const QString& desktopFile, const QString& name, const QString& icon, QObject* parent = 0);
3434
35− QString appId() const;
35+ QString appId() const override;
3636 QString desktopFile() const;
37− QString name() const;
38− QString icon() const;
37+ QString name() const override;
38+ QString icon() const override;
3939
40− bool pinned() const;
41− void setPinned(bool pinned);
40+ bool pinned() const override;
41+ bool running() const override;
42+ bool recent() const override;
43+ int progress() const override;
44+ int count() const override;
45+ bool countVisible() const override;
46+ bool focused() const;
4247
43− bool running() const;
44− void setRunning(bool running);
48+ unity::shell::launcher::QuickListModelInterface *quickList() const;
4549
46− bool recent() const;
50+private:
51+ void setPinned(bool pinned);
52+ void setRunning(bool running);
4753 void setRecent(bool recent);
48−
49− int progress() const;
5054 void setProgress(int progress);
51−
52− int count() const;
5355 void setCount(int count);
54−
55− bool focused() const;
56+ void setCountVisible(bool countVisible);
5657 void setFocused(bool focused);
5758
58− unity::shell::launcher::QuickListModelInterface *quickList() const;
59−
60−private:
6159 QString m_appId;
6260 QString m_desktopFile;
6361 QString m_name;
@@ -67,8 +65,11 @@ private:
6765 bool m_recent;
6866 int m_progress;
6967 int m_count;
68+ bool m_countVisible;
7069 bool m_focused;
7170 MockQuickListModel *m_quickList;
71+
72+ friend class MockLauncherModel;
7273};
7374
7475#endif // MOCKLAUNCHERITEM_H
tests/mocks/Unity/Launcher/MockLauncherModel.cpp+10
@@ -33,26 +33,32 @@ MockLauncherModel::MockLauncherModel(QObject* parent): LauncherModelInterface(pa
3333 m_list.append(item);
3434 item = new MockLauncherItem("gallery-app", "/usr/share/applications/gallery-app.desktop", "Gallery", "gallery", this);
3535 item->setProgress(50);
36+ item->setCountVisible(true);
3637 m_list.append(item);
3738 item = new MockLauncherItem("facebook-webapp", "/usr/share/applications/facebook-webapp.desktop", "Facebook", "facebook", this);
3839 item->setProgress(150);
3940 m_list.append(item);
4041 item = new MockLauncherItem("webbrowser-app", "/usr/share/applications/webbrowser-app.desktop", "Browser", "browser", this);
4142 item->setCount(1);
43+ item->setCountVisible(true);
4244 m_list.append(item);
4345 item = new MockLauncherItem("twitter-webapp", "/usr/share/applications/twitter-webapp.desktop", "Twitter", "twitter", this);
4446 item->setCount(12);
47+ item->setCountVisible(true);
4548 item->setPinned(true);
4649 m_list.append(item);
4750 item = new MockLauncherItem("gmail-webapp", "/usr/share/applications/gmail-webapp.desktop", "GMail", "gmail", this);
4851 item->setCount(123);
52+ item->setCountVisible(true);
4953 m_list.append(item);
5054 item = new MockLauncherItem("ubuntu-weather-app", "/usr/share/applications/ubuntu-weather-app.desktop", "Weather", "weather", this);
5155 item->setCount(1234567890);
56+ item->setCountVisible(true);
5257 m_list.append(item);
5358 item = new MockLauncherItem("notes-app", "/usr/share/applications/notes-app.desktop", "Notepad", "notepad", this);
5459 item->setProgress(50);
5560 item->setCount(5);
61+ item->setCountVisible(true);
5662 item->setFocused(true);
5763 item->setPinned(true);
5864 m_list.append(item);
@@ -76,6 +82,8 @@ QVariant MockLauncherModel::data(const QModelIndex& index, int role) const
7682 LauncherItemInterface *item = m_list.at(index.row());
7783 switch(role)
7884 {
85+ case RoleAppId:
86+ return item->appId();
7987 case RoleName:
8088 return item->name();
8189 case RoleIcon:
@@ -90,6 +98,8 @@ QVariant MockLauncherModel::data(const QModelIndex& index, int role) const
9098 return item->progress();
9199 case RoleCount:
92100 return item->count();
101+ case RoleCountVisible:
102+ return item->countVisible();
93103 case RoleFocused:
94104 return item->focused();
95105 }
tests/mocks/Unity/fake_categories.cpp+95−136
@@ -29,25 +29,12 @@ Categories::Categories(int category_count, QObject* parent)
2929
3030int Categories::rowCount(const QModelIndex& /*parent*/) const
3131{
32− return m_category_count + m_specialCategories.count();
32+ return m_category_count;
3333}
3434
35−void Categories::addSpecialCategory(QString const& categoryId, QString const& name, QString const& icon, QString const& rawTemplate, QObject* countObject)
35+void Categories::addSpecialCategory(QString const& /*categoryId*/, QString const& /*name*/, QString const& /*icon*/, QString const& /*rawTemplate*/, QObject* /*countObject*/)
3636{
37− CategoryData catData;
38− catData.categoryId = categoryId;
39− catData.name = name;
40− catData.icon = icon;
41− catData.rawTemplate = rawTemplate;
42− catData.countObject = countObject;
43−
44− beginInsertRows(QModelIndex(), 0, 0);
45− m_specialCategories.prepend(catData);
46− endInsertRows();
47−
48− if (countObject) {
49− connect(countObject, SIGNAL(countChanged()), this, SLOT(countChanged()));
50− }
37+ qFatal("Using un-implemented Categories::addSpecialCategory");
5138}
5239
5340bool Categories::overrideCategoryJson(QString const& /* categoryId */, QString const& /* json */)
@@ -55,23 +42,6 @@ bool Categories::overrideCategoryJson(QString const& /* categoryId */, QString c
5542 qFatal("Using un-implemented Categories::overrideCategoryJson");
5643}
5744
58−void Categories::countChanged()
59−{
60− QObject* countObject = sender();
61−
62− for (int i = 0; i < m_specialCategories.count(); ++i) {
63− const CategoryData &catData = m_specialCategories.at(i);
64− if (catData.countObject == countObject) {
65− QVector<int> roles;
66− roles.append(RoleCount);
67−
68− QModelIndex changedIndex(index(i));
69− dataChanged(changedIndex, changedIndex, roles);
70− break;
71− }
72− }
73−}
74−
7545QVariant
7646Categories::data(const QModelIndex& index, int role) const
7747{
@@ -79,115 +49,74 @@ Categories::data(const QModelIndex& index, int role) const
7949 return QVariant();
8050 }
8151
82− ResultsModel *resultsModel = m_resultsModels[index.row()];
83− if (!resultsModel) {
84− resultsModel = new ResultsModel(15, index.row());
85− m_resultsModels[index.row()] = resultsModel;
86− }
87− if (index.row() < m_specialCategories.count()) {
88− const CategoryData &catData = m_specialCategories.at(index.row());
89− switch (role) {
90− case RoleCategoryId:
91− return catData.categoryId;
92− case RoleName:
93− return catData.name;
94− case RoleIcon:
95− return catData.icon;
96− case RoleRawRendererTemplate:
97− return catData.rawTemplate;
98− case RoleRenderer:
99− {
100− QVariantMap map;
101− if (index.row() % 2 == 0) {
102− map["category-layout"] = "grid";
103− } else {
104− map["category-layout"] = "carousel";
105− map["overlay"] = true;
106− }
107− map["card-size"] = "small";
108− return map;
52+ ResultsModel *resultsModel = resultModel(index.row());
53+ switch (role) {
54+ case RoleCategoryId:
55+ return QString("%1").arg(index.row());
56+ case RoleName:
57+ return QString("Category %1").arg(index.row());
58+ case RoleIcon:
59+ return "gtk-apply";
60+ case RoleRawRendererTemplate:
61+ qFatal("Using un-implemented RoleRawRendererTemplate Categories role");
62+ return QVariant();
63+ case RoleRenderer:
64+ {
65+ QVariantMap map;
66+ if (index.row() % 2 == 0) {
67+ map["category-layout"] = "grid";
68+ } else {
69+ map["category-layout"] = "carousel";
10970 }
110− case RoleComponents:
111− {
112− QVariantMap map, artMap, attributeMap;
113− artMap["aspect-ratio"] = "1.0";
114− artMap["field"] = "art";
115− map["art"] = artMap;
116− map["title"] = "HOLA";
117− attributeMap["field"] = "attribute";
118− map["attributes"] = attributeMap;
119− return map;
71+ if (index.row() == 18) {
72+ map["category-layout"] = "horizontal-list";
73+ }
74+ if (index.row() == 19) {
75+ map["category-layout"] = "grid";
76+ map["collapsed-rows"] = 0;
77+ }
78+ map["card-size"] = "small";
79+
80+ map["category-layout"] = m_layouts.value(index.row(), map["category-layout"].toString());
81+
82+ if (map["category-layout"] == "carousel") {
83+ map["overlay"] = true;
12084 }
121− case RoleHeaderLink:
122− return QString();
123− case RoleResults:
124− return QVariant();
125− case RoleCount:
126− return catData.countObject->property("count");
127− default:
128− qFatal("Using un-implemented Categories role");
129− return QVariant();
85+
86+ return map;
13087 }
131− } else {
132− switch (role) {
133− case RoleCategoryId:
134− return QString("%1").arg(index.row());
135− case RoleName:
136− return QString("Category %1").arg(index.row());
137− case RoleIcon:
138− return "gtk-apply";
139− case RoleRawRendererTemplate:
140− qFatal("Using un-implemented RoleRawRendererTemplate Categories role");
141− return QVariant();
142− case RoleRenderer:
143− {
144− QVariantMap map;
145− if (index.row() % 2 == 0) {
146− map["category-layout"] = "grid";
147− } else {
148− map["category-layout"] = "carousel";
149− map["card-size"] = "medium";
150− map["overlay"] = true;
151− }
152− if (index.row() == 18) {
153− map["category-layout"] = "horizontal-list";
154− }
155− if (index.row() == 19) {
156− map["category-layout"] = "grid";
157− map["collapsed-rows"] = 0;
158− }
159− map["card-size"] = "small";
160− return map;
88+ case RoleComponents:
89+ {
90+ QVariantMap map, artMap, attributeMap;
91+ if (index.row() % 2 != 0) {
92+ artMap["aspect-ratio"] = QString("1.%1").arg(index.row());
93+ } else {
94+ artMap["aspect-ratio"] = "1.0";
16195 }
162− case RoleComponents:
163− {
164− QVariantMap map, artMap, attributeMap;
165− if (index.row() % 2 != 0) {
166− artMap["aspect-ratio"] = QString("1.%1").arg(index.row());
167− } else {
168− artMap["aspect-ratio"] = "1.0";
169− }
170− artMap["field"] = "art";
171− map["art"] = artMap;
172− map["title"] = "HOLA";
173− map["subtitle"] = "HOLA";
174− attributeMap["field"] = "attribute";
175− map["attributes"] = attributeMap;
176− return map;
96+ artMap["field"] = "art";
97+ map["art"] = artMap;
98+ map["title"] = "HOLA";
99+ map["subtitle"] = "HOLA";
100+ attributeMap["field"] = "attribute";
101+ map["attributes"] = attributeMap;
102+ return map;
103+ }
104+ case RoleHeaderLink:
105+ {
106+ QString res;
107+ if (index.row() == 1 || index.row() == 4) {
108+ res = QString("scope://query/1");
177109 }
178− case RoleHeaderLink:
179− if (index.row() == 1 || index.row() == 4) {
180− return QString("scope://query/1");
181− }
182− return QString();
183− case RoleResults:
184− return QVariant::fromValue(resultsModel);
185− case RoleCount:
186− return resultsModel->rowCount();
187− default:
188− qFatal("Using un-implemented Categories role");
189− return QVariant();
110+ res = m_headerLinks.value(index.row(), res);
111+ return res;
190112 }
113+ case RoleResults:
114+ return QVariant::fromValue(resultsModel);
115+ case RoleCount:
116+ return resultsModel->rowCount();
117+ default:
118+ qFatal("Using un-implemented Categories role");
119+ return QVariant();
191120 }
192121}
193122
@@ -196,3 +125,33 @@ Categories::data(int row, int role) const
196125{
197126 return data(index(row, 0), role);
198127}
128+
129+void Categories::setCount(int count)
130+{
131+ if (m_category_count != count) {
132+ beginResetModel(); // This is just for test setup so we can be lazy and reset
133+ m_category_count = count;
134+ endResetModel();
135+ }
136+}
137+
138+ResultsModel* Categories::resultModel(int row) const
139+{
140+ ResultsModel *result = m_resultsModels[row];
141+ if (!result) {
142+ Categories *that = const_cast<Categories*>(this);
143+ result = new ResultsModel(15, row, that);
144+ m_resultsModels[row] = result;
145+ }
146+ return result;
147+}
148+
149+void Categories::setLayout(int row, const QString &layout)
150+{
151+ m_layouts[row] = layout;
152+}
153+
154+void Categories::setHeaderLink(int row, const QString &headerLink)
155+{
156+ m_headerLinks[row] = headerLink;
157+}
tests/mocks/Unity/fake_categories.h+7−12
@@ -39,22 +39,17 @@ public:
3939
4040 Q_INVOKABLE QVariant data(int row, int role) const;
4141
42−private Q_SLOTS:
43− void countChanged();
42+ // For testing purposes
43+ Q_INVOKABLE void setCount(int count);
44+ Q_INVOKABLE ResultsModel* resultModel(int row) const;
45+ Q_INVOKABLE void setLayout(int row, const QString &layout);
46+ Q_INVOKABLE void setHeaderLink(int row, const QString &headerLink);
4447
4548private:
4649 mutable QHash<int, ResultsModel*> m_resultsModels;
50+ QHash<int, QString> m_layouts;
51+ QHash<int, QString> m_headerLinks;
4752 int m_category_count;
48−
49− struct CategoryData {
50− QString categoryId;
51− QString name;
52− QString icon;
53− QString rawTemplate;
54− QObject* countObject;
55− };
56−
57− QList<CategoryData> m_specialCategories;
5853};
5954
6055#endif // FAKE_CATEGORIES_H
tests/mocks/Unity/fake_resultsmodel.cpp+9
@@ -44,6 +44,15 @@ void ResultsModel::setCategoryId(QString const& /*id*/)
4444 qFatal("Calling un-implemented ResultsModel::setCategoryId");
4545}
4646
47+void ResultsModel::setResultCount(int result_count)
48+{
49+ if (m_result_count != result_count) {
50+ beginResetModel(); // This is just for test setup so we can be lazy and reset
51+ m_result_count = result_count;
52+ endResetModel();
53+ }
54+}
55+
4756int ResultsModel::rowCount(const QModelIndex& parent) const
4857{
4958 Q_UNUSED(parent);
tests/mocks/Unity/fake_resultsmodel.h+3
@@ -41,6 +41,9 @@ public:
4141 /* setters */
4242 void setCategoryId(QString const& id) override;
4343
44+ // For testing purposes
45+ Q_INVOKABLE void setResultCount(int result_count);
46+
4447private:
4548 int m_result_count;
4649 int m_categoryId;
tests/mocks/Unity/fake_scope.cpp+26−4
@@ -28,7 +28,7 @@ Scope::Scope(Scopes* parent) : Scope("MockScope5", "Mock Scope", false, parent)
2828{
2929}
3030
31−Scope::Scope(QString const& id, QString const& name, bool favorite, Scopes* parent, int categories)
31+Scope::Scope(QString const& id, QString const& name, bool favorite, Scopes* parent, int categories, bool returnNullPreview)
3232 : unity::shell::scopes::ScopeInterface(parent)
3333 , m_id(id)
3434 , m_name(name)
@@ -41,6 +41,7 @@ Scope::Scope(QString const& id, QString const& name, bool favorite, Scopes* pare
4141 , m_categories(new Categories(categories, this))
4242 , m_openScope(nullptr)
4343 , m_settings(new SettingsModel(this))
44+ , m_returnNullPreview(returnNullPreview)
4445{
4546}
4647
@@ -145,6 +146,23 @@ void Scope::setFavorite(const bool favorite)
145146 Q_EMIT favoriteChanged();
146147 }
147148}
149+
150+void Scope::setId(const QString &id)
151+{
152+ if (id != m_id) {
153+ m_id = id;
154+ Q_EMIT idChanged();
155+ }
156+}
157+
158+void Scope::setName(const QString &name)
159+{
160+ if (name != m_name) {
161+ m_name = name;
162+ Q_EMIT nameChanged();
163+ }
164+}
165+
148166void Scope::setSearchInProgress(const bool inProg)
149167{
150168 if (inProg != m_searching) {
@@ -175,9 +193,13 @@ PreviewStack* Scope::preview(QVariant const& result)
175193{
176194 Q_UNUSED(result);
177195
178− // This probably leaks, do we don't care
179− // it's a test after all
180− return new PreviewStack;
196+ if (m_returnNullPreview) {
197+ return nullptr;
198+ } else {
199+ // This probably leaks, do we don't care
200+ // it's a test after all
201+ return new PreviewStack;
202+ }
181203}
182204
183205void Scope::cancelActivation()
tests/mocks/Unity/fake_scope.h+5−1
@@ -32,7 +32,7 @@ class Scope : public unity::shell::scopes::ScopeInterface
3232
3333public:
3434 Scope(Scopes* parent = 0);
35− Scope(QString const& id, QString const& name, bool favorite, Scopes* parent = 0, int categories = 20);
35+ Scope(QString const& id, QString const& name, bool favorite, Scopes* parent = 0, int categories = 20, bool returnNullPreview = false);
3636
3737 /* getters */
3838 QString id() const override;
@@ -56,6 +56,8 @@ public:
5656 void setFormFactor(const QString& form_factor) override;
5757 void setActive(const bool) override;
5858 void setFavorite(const bool) override;
59+ Q_INVOKABLE void setId(const QString &id); // This is not invokable in the Interface, here for testing benefits
60+ Q_INVOKABLE void setName(const QString &name); // This is not invokable in the Interface, here for testing benefits
5961 Q_INVOKABLE void setSearchInProgress(const bool inProg); // This is not invokable in the Interface, here for testing benefits
6062
6163 Q_INVOKABLE void activate(QVariant const& result) override;
@@ -97,6 +99,8 @@ protected:
9799 unity::shell::scopes::CategoriesInterface* m_categories;
98100 unity::shell::scopes::ScopeInterface* m_openScope;
99101 unity::shell::scopes::SettingsModelInterface* m_settings;
102+
103+ bool m_returnNullPreview;
100104};
101105
102106#endif // FAKE_SCOPE_H
tests/mocks/Unity/fake_scopes.cpp+1
@@ -59,6 +59,7 @@ void Scopes::updateScopes()
5959 addScope(new Scope("MockScope7", "MS7", false, this));
6060 addScope(new Scope("MockScope8", "MS8", false, this));
6161 addScope(new Scope("MockScope9", "MS9", false, this));
62+ addScope(new Scope("NullPreviewScope", "NPS", false, this, 1, true));
6263 m_scopesOverview = new ScopesOverview(this);
6364
6465 if (!m_loaded) {
tests/plugins/Dash/cardcreator/1.res+2−2
@@ -40,8 +40,8 @@ Item {
4040 width = root.fixedArtShapeSize.width;
4141 height = root.fixedArtShapeSize.height;
4242 } else {
43− width = Qt.binding(function() { return !visible ? 0 : image.width });
44− height = Qt.binding(function() { return !visible ? 0 : image.height });
43+ width = Qt.binding(function() { return image.status !== Image.Ready ? 0 : image.width });
44+ height = Qt.binding(function() { return image.status !== Image.Ready ? 0 : image.height });
4545 }
4646 }
4747 image: Image {
tests/plugins/Dash/cardcreator/3.res+3−3
@@ -40,9 +40,9 @@ Item {
4040 width = root.fixedArtShapeSize.width;
4141 height = root.fixedArtShapeSize.height;
4242 } else {
43− width = Qt.binding(function() { return !visible ? 0 : image.width });
44− height = Qt.binding(function() { return !visible ? 0 : image.height });
45− }
43+ width = Qt.binding(function() { return image.status !== Image.Ready ? 0 : image.width });
44+ height = Qt.binding(function() { return image.status !== Image.Ready ? 0 : image.height });
45+ }
4646 }
4747 image: Image {
4848 objectName: "artImage";
tests/plugins/Dash/cardcreator/5.res+3−3
@@ -40,9 +40,9 @@ Item {
4040 width = root.fixedArtShapeSize.width;
4141 height = root.fixedArtShapeSize.height;
4242 } else {
43− width = Qt.binding(function() { return !visible ? 0 : image.width });
44− height = Qt.binding(function() { return !visible ? 0 : image.height });
45− }
43+ width = Qt.binding(function() { return image.status !== Image.Ready ? 0 : image.width });
44+ height = Qt.binding(function() { return image.status !== Image.Ready ? 0 : image.height });
45+ }
4646 }
4747 image: Image {
4848 objectName: "artImage";
tests/plugins/Unity/Launcher/CMakeLists.txt+9−24
@@ -1,12 +1,11 @@
11pkg_check_modules(GSETTINGS_QT REQUIRED gsettings-qt)
2−pkg_check_modules(LAUNCHER_API REQUIRED unity-shell-launcher=3)
2+pkg_check_modules(LAUNCHER_API REQUIRED unity-shell-launcher=4)
33pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=4)
44
55include_directories(
66 ${CMAKE_CURRENT_BINARY_DIR}
77 ${CMAKE_SOURCE_DIR}/plugins/AccountsService
88 ${CMAKE_SOURCE_DIR}/plugins/Unity/Launcher
9− ${CMAKE_SOURCE_DIR}/plugins/Unity/Launcher/backend
109 ${GSETTINGS_QT_INCLUDE_DIRS}
1110 )
1211
@@ -14,35 +13,21 @@ add_definitions(-DSM_BUSNAME=sessionBus)
1413add_definitions(-DSRCDIR="${CMAKE_CURRENT_SOURCE_DIR}")
1514add_definitions(-DLAUNCHER_TESTING)
1615
17−### LauncherBackendTest
18−set(testBackendCommand dbus-test-runner --task ${CMAKE_CURRENT_BINARY_DIR}/launcherbackendtestExec
19− --parameter -o --parameter ${CMAKE_BINARY_DIR}/launcherbackendtest.xml,xunitxml
20− --parameter -o --parameter -,txt)
21−add_test(NAME launcherbackendtest COMMAND ${testBackendCommand})
22−add_custom_target(launcherbackendtest ${testBackendCommand})
23−add_executable(launcherbackendtestExec
24− launcherbackendtest.cpp
25− ${CMAKE_SOURCE_DIR}/plugins/AccountsService/AccountsServiceDBusAdaptor.cpp
26− ${CMAKE_SOURCE_DIR}/plugins/Unity/Launcher/backend/launcherbackend.cpp
27− ${CMAKE_SOURCE_DIR}/plugins/Unity/Launcher/common/quicklistentry.cpp
28− )
29−target_link_libraries(launcherbackendtestExec ${GSETTINGS_QT_LDFLAGS})
30−qt5_use_modules(launcherbackendtestExec Test Core DBus)
31−
3216### LauncherModelTest
33−set(testModelCommand ${CMAKE_CURRENT_BINARY_DIR}/launchermodeltestExec
34− -o ${CMAKE_BINARY_DIR}/launchermodeltest.xml,xunitxml
35− -o -,txt)
17+set(testModelCommand dbus-test-runner --task ${CMAKE_CURRENT_BINARY_DIR}/launchermodeltestExec
18+ --parameter -o --parameter ${CMAKE_BINARY_DIR}/launchermodeltest.xml,xunitxml
19+ --parameter -o --parameter -,txt)
3620add_test(NAME launchermodeltest COMMAND ${testModelCommand})
3721add_custom_target(launchermodeltest ${testModelCommand})
3822add_executable(launchermodeltestExec
3923 launchermodeltest.cpp
24+ gsettings.cpp
4025 ${CMAKE_SOURCE_DIR}/plugins/Unity/Launcher/launchermodel.cpp
4126 ${CMAKE_SOURCE_DIR}/plugins/Unity/Launcher/launcheritem.cpp
4227 ${CMAKE_SOURCE_DIR}/plugins/Unity/Launcher/quicklistmodel.cpp
43− ${CMAKE_SOURCE_DIR}/plugins/AccountsService/AccountsServiceDBusAdaptor.cpp
44− ${CMAKE_SOURCE_DIR}/plugins/Unity/Launcher/backend/launcherbackend.cpp
45− ${CMAKE_SOURCE_DIR}/plugins/Unity/Launcher/common/quicklistentry.cpp
28+ ${CMAKE_SOURCE_DIR}/plugins/Unity/Launcher/dbusinterface.cpp
29+ ${CMAKE_SOURCE_DIR}/plugins/Unity/Launcher/desktopfilehandler.cpp
30+ ${CMAKE_SOURCE_DIR}/plugins/Unity/Launcher/quicklistentry.cpp
4631 ${LAUNCHER_API_INCLUDEDIR}/unity/shell/launcher/LauncherItemInterface.h
4732 ${LAUNCHER_API_INCLUDEDIR}/unity/shell/launcher/LauncherModelInterface.h
4833 ${LAUNCHER_API_INCLUDEDIR}/unity/shell/launcher/QuickListModelInterface.h
@@ -50,7 +35,7 @@ add_executable(launchermodeltestExec
5035 ${LAUNCHER_API_INCLUDEDIR}/unity/shell/application/ApplicationInfoInterface.h
5136 )
5237target_link_libraries(launchermodeltestExec ${GSETTINGS_QT_LDFLAGS})
53−qt5_use_modules(launchermodeltestExec Test Core DBus Gui)
38+qt5_use_modules(launchermodeltestExec Test Core DBus Xml Gui)
5439
5540# copy .desktop files into build directory for shadow builds
5641file(GLOB DESKTOP_FILES *.desktop)
tests/plugins/Unity/Launcher/gsettings.cppnew file+40
@@ -0,0 +1,40 @@
1+/*
2+ * Copyright 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 Lesser General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ *
16+ * Authors:
17+ * Michael Zanetti <michael.zanetti@canonical.com>
18+ */
19+
20+#include "gsettings.h"
21+
22+#include <QDebug>
23+
24+// This is a mock implementation to not touch GSettings for real during tests
25+
26+GSettings::GSettings(QObject *parent):
27+ QObject(parent)
28+{
29+
30+}
31+
32+QStringList GSettings::storedApplications() const
33+{
34+ return QStringList();
35+}
36+
37+void GSettings::setStoredApplications(const QStringList &storedApplications)
38+{
39+ Q_UNUSED(storedApplications)
40+}
tests/plugins/Unity/Launcher/gsettings.hnew file+38
@@ -0,0 +1,38 @@
1+/*
2+ * Copyright 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 Lesser General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ *
16+ * Authors:
17+ * Michael Zanetti <michael.zanetti@canonical.com>
18+ */
19+
20+#ifndef GSETTINGS_H
21+#define GSETTINGS_H
22+
23+#include <QObject>
24+#include <QStringList>
25+
26+// This is a mock implementation to not touch GSettings for real during tests
27+
28+class GSettings: public QObject
29+{
30+ Q_OBJECT
31+public:
32+ GSettings(QObject *parent = nullptr);
33+
34+ QStringList storedApplications() const;
35+ void setStoredApplications(const QStringList &storedApplications);
36+};
37+
38+#endif
tests/plugins/Unity/Launcher/launcherbackendtest.cppdeleted−239
@@ -1,239 +0,0 @@
1−/*
2− * Copyright 2013 Canonical Ltd.
3− *
4− * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU Lesser General Public License as published by
6− * the Free Software Foundation; version 3.
7− *
8− * This program is distributed in the hope that it will be useful,
9− * but WITHOUT ANY WARRANTY; without even the implied warranty of
10− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU Lesser General Public License for more details.
12− *
13− * You should have received a copy of the GNU Lesser General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− *
16− * Authors:
17− * Michael Terry <michael.terry@canonical.com>
18− */
19−
20−#include "launcherbackend.h"
21−
22−#include <QtTest>
23−#include <QDBusConnection>
24−#include <QDBusMessage>
25−#include <QDBusVariant>
26−
27−
28−class LauncherBackendTest : public QObject
29−{
30− Q_OBJECT
31−
32−private Q_SLOTS:
33−
34− void initTestCase()
35− {
36− // As we want to test absolute file paths in the .desktop file, we need to modify the
37− // sample file and replace the path in there with the current build dir.
38− QSettings settings(QDir::currentPath() + "/click-icon.desktop", QSettings::IniFormat);
39− settings.setValue("Desktop Entry/Path", QDir::currentPath());
40− }
41−
42− void testFileNames()
43− {
44− LauncherBackend backend;
45−
46− backend.setStoredApplications(QStringList() << "rel-icon" << "abs-icon" << "invalid");
47− QCOMPARE(backend.storedApplications(), QStringList() << "rel-icon" << "abs-icon");
48− }
49−
50− void testIcon_data() {
51− QTest::addColumn<QString>("appId");
52− QTest::addColumn<QString>("expectedIcon");
53−
54− // Needs to expand a relative icon to the absolute path
55− QTest::newRow("relative icon path") << "rel-icon" << QDir::currentPath() + "/rel-icon.svg";
56−
57− // In case an icon is not found on disk, it needs to fallback on image://theme/ for it
58− QTest::newRow("fallback on theme") << "abs-icon" << "image://theme//path/to/icon.png";
59−
60− // Click packages have a relative icon path but an absolute path as a separate entry
61− QTest::newRow("click package icon") << "click-icon" << QDir::currentPath() + "/click-icon.svg";
62− }
63−
64− void testIcon() {
65− QFETCH(QString, appId);
66− QFETCH(QString, expectedIcon);
67−
68− LauncherBackend backend;
69− backend.setStoredApplications(QStringList() << appId);
70−
71− QCOMPARE(backend.icon(appId), expectedIcon);
72− }
73−
74− void testGetItem_data() {
75− QTest::addColumn<QString>("appId");
76− QTest::addColumn<bool>("exists");
77−
78− QTest::newRow("Exists") << "rel-icon" << true;
79− QTest::newRow("Doesn't Exist") << "does-not-exist" << false;
80− }
81−
82− void testGetItem() {
83− QFETCH(QString, appId);
84− QFETCH(bool, exists);
85−
86− LauncherBackend backend;
87− auto item = backend.getItem(appId);
88−
89− if (exists) {
90− QVERIFY(item != nullptr);
91− } else {
92− QVERIFY(item == nullptr);
93− }
94− }
95−
96− void testCount_data() {
97− QTest::addColumn<QString>("appId");
98− QTest::addColumn<bool>("setCount");
99− QTest::addColumn<int>("inCount");
100− QTest::addColumn<bool>("countVisible");
101− QTest::addColumn<int>("expectedCount");
102−
103− /* Get baseline data on things working */
104− QTest::newRow("Baseline") << "rel-icon" << false << 0 << false << -1;
105−
106− /* Valid count, but not visible */
107− QTest::newRow("Not visible") << "rel-icon" << true << 42 << false << -1;
108−
109− /* Turn it on */
110− QTest::newRow("Visible Count") << "rel-icon" << true << 42 << true << 42;
111−
112− /* Invalide app to load */
113− QTest::newRow("Invalid App ID") << "this-app-doesnt-exist" << true << 42 << true << -1;
114− }
115−
116− void testCount() {
117− QFETCH(QString, appId);
118− QFETCH(bool, setCount);
119− QFETCH(int, inCount);
120− QFETCH(bool, countVisible);
121− QFETCH(int, expectedCount);
122−
123− LauncherBackend backend;
124−
125− if (setCount)
126− backend.setCount(appId, inCount);
127− backend.setCountVisible(appId, countVisible);
128−
129− QCOMPARE(backend.count(appId), expectedCount);
130− }
131−
132− void testDbusName_data() {
133− QTest::addColumn<QString>("decoded");
134− QTest::addColumn<QString>("encoded");
135−
136− /* Passthrough test */
137− QTest::newRow("Passthrough") << "fine" << "fine";
138−
139− /* Number as first characeter */
140− QTest::newRow("Number first") << "31337" << "_331337";
141−
142− /* Underscore test */
143− QTest::newRow("Underscore test") << "this_is_c_style_namespacing" << "this_5Fis_5Fc_5Fstyle_5Fnamespacing";
144−
145− /* Hyphen test */
146− QTest::newRow("Hyphen test") << "typical-application" << "typical_2Dapplication";
147−
148− /* Japanese test */
149− QTest::newRow("日本語 test") << "日本語" << "_E6_97_A5_E6_9C_AC_E8_AA_9E";
150− }
151−
152− void testDbusName() {
153− QFETCH(QString, decoded);
154− QFETCH(QString, encoded);
155−
156− QString encodeOut = LauncherBackend::encodeAppId(decoded);
157− QCOMPARE(encoded, encodeOut);
158−
159− QString decodeOut = LauncherBackend::decodeAppId(encoded);
160− QCOMPARE(decoded, decodeOut);
161− }
162−
163− void testDbusIface_data() {
164− QTest::addColumn<QString>("appId");
165− QTest::addColumn<bool>("setCount");
166− QTest::addColumn<int>("inCount");
167− QTest::addColumn<bool>("countVisible");
168− QTest::addColumn<int>("expectedCount");
169−
170− /* Get baseline data on things working */
171− QTest::newRow("Baseline") << "rel-icon" << false << 0 << false << -1;
172−
173− /* Turn it on */
174− QTest::newRow("Visible Count") << "rel-icon" << true << 42 << true << 42;
175−
176− /* Invalide app to load */
177− QTest::newRow("Invalid App ID") << "this-app-doesnt-exist" << true << 42 << true << -1;
178− }
179−
180− void testDbusIface() {
181− QFETCH(QString, appId);
182− QFETCH(bool, setCount);
183− QFETCH(int, inCount);
184− QFETCH(bool, countVisible);
185− QFETCH(int, expectedCount);
186−
187− QDBusConnection con = QDBusConnection::sessionBus();
188− QDBusMessage message;
189− QDBusMessage reply;
190−
191− LauncherBackend backend;
192−
193− if (setCount) {
194− message = QDBusMessage::createMethodCall("com.canonical.Unity.Launcher",
195− "/com/canonical/Unity/Launcher/" + LauncherBackend::encodeAppId(appId),
196− "org.freedesktop.DBus.Properties",
197− "Set");
198− QVariantList cargs;
199− cargs.append(QString("com.canonical.Unity.Launcher.Item"));
200− cargs.append(QString("count"));
201− cargs.append(QVariant::fromValue(QDBusVariant(inCount)));
202−
203− message.setArguments(cargs);
204− reply = con.call(message);
205− QCOMPARE(reply.type(), QDBusMessage::ReplyMessage);
206− }
207−
208− /* Set countVisible */
209− message = QDBusMessage::createMethodCall("com.canonical.Unity.Launcher",
210− "/com/canonical/Unity/Launcher/" + LauncherBackend::encodeAppId(appId),
211− "org.freedesktop.DBus.Properties",
212− "Set");
213− QVariantList cvargs;
214− cvargs.append(QString("com.canonical.Unity.Launcher.Item"));
215− cvargs.append(QString("countVisible"));
216− cvargs.append(QVariant::fromValue(QDBusVariant(countVisible)));
217−
218− message.setArguments(cvargs);
219− reply = con.call(message);
220− QCOMPARE(reply.type(), QDBusMessage::ReplyMessage);
221−
222− /* Get value */
223− message = QDBusMessage::createMethodCall("com.canonical.Unity.Launcher",
224− "/com/canonical/Unity/Launcher/" + LauncherBackend::encodeAppId(appId),
225− "org.freedesktop.DBus.Properties",
226− "Get");
227− QVariantList getargs;
228− getargs.append(QString("com.canonical.Unity.Launcher.Item"));
229− getargs.append(QString("count"));
230−
231− message.setArguments(getargs);
232− reply = con.call(message);
233− QCOMPARE(reply.type(), QDBusMessage::ReplyMessage);
234− QCOMPARE(reply.arguments()[0].value<QDBusVariant>().variant().toInt(), expectedCount);
235− }
236−};
237−
238−QTEST_GUILESS_MAIN(LauncherBackendTest)
239−#include "launcherbackendtest.moc"
tests/plugins/Unity/Launcher/launchermodeltest.cpp+81−1
@@ -23,8 +23,12 @@
2323
2424#include "launcheritem.h"
2525#include "launchermodel.h"
26+#include "dbusinterface.h"
2627
2728#include <QtTest>
29+#include <QDBusInterface>
30+#include <QDBusReply>
31+#include <QDomDocument>
2832
2933// This is a mock, specifically to test the LauncherModel
3034class MockApp: public unity::shell::application::ApplicationInfoInterface
@@ -287,10 +291,86 @@ private Q_SLOTS:
287291 QCOMPARE(launcherModel->getUrlForAppId(QString()), QString());
288292 QCOMPARE(launcherModel->getUrlForAppId(""), QString());
289293 QCOMPARE(launcherModel->getUrlForAppId("no-name"), QString("application:///no-name.desktop"));
290− QCOMPARE(launcherModel->getUrlForAppId("com.test.good"), QString("appid://com.test.good/first-listed-app/current-user-version"));
294+ QCOMPARE(launcherModel->getUrlForAppId("com.test.good"), QString("application:///com.test.good.desktop"));
291295 QCOMPARE(launcherModel->getUrlForAppId("com.test.good_application"), QString("appid://com.test.good/application/current-user-version"));
292296 QCOMPARE(launcherModel->getUrlForAppId("com.test.good_application_1.2.3"), QString("appid://com.test.good/application/current-user-version"));
293297 }
298+
299+ void testIntrospection() {
300+ QDBusInterface interface("com.canonical.Unity.Launcher", "/com/canonical/Unity/Launcher", "org.freedesktop.DBus.Introspectable");
301+ QDBusReply<QString> reply = interface.call("Introspect");
302+ QStringList nodes = extractNodes(reply.value());
303+ QCOMPARE(nodes.count(), launcherModel->rowCount());
304+
305+ appManager->addApplication(new MockApp("foobar"));
306+ reply = interface.call("Introspect");
307+ nodes = extractNodes(reply.value());
308+ QCOMPARE(nodes.contains("foobar"), true);
309+
310+ appManager->removeApplication(2);
311+ reply = interface.call("Introspect");
312+ nodes = extractNodes(reply.value());
313+ QCOMPARE(nodes.contains("foobar"), false);
314+ }
315+
316+ QStringList extractNodes(const QString &introspectionXml) {
317+ QXmlStreamReader introspectReply(introspectionXml);
318+
319+ QStringList ret;
320+ while (!introspectReply.atEnd() && !introspectReply.hasError()) {
321+ QXmlStreamReader::TokenType token = introspectReply.readNext();
322+
323+ if (token == QXmlStreamReader::StartElement) {
324+ if (introspectReply.name() == "node" && introspectReply.attributes().count() > 0) {
325+ ret << introspectReply.attributes().value("name").toString();
326+ }
327+ }
328+ }
329+ return ret;
330+ }
331+
332+ void testCountEmblems() {
333+ // Call GetAll on abs-icon
334+ QDBusInterface interface("com.canonical.Unity.Launcher", "/com/canonical/Unity/Launcher/abs_2Dicon", "org.freedesktop.DBus.Properties");
335+ QDBusReply<QVariantMap> reply = interface.call("GetAll");
336+ QVariantMap map = reply.value();
337+
338+ // Make sure GetAll returns a map with count and countVisible props
339+ QCOMPARE(map.contains("count"), true);
340+ QCOMPARE(map.contains("countVisible"), true);
341+
342+ // Make sure count is intitilized to 0 and non-visible
343+ QCOMPARE(map.value("count").toInt(), 0);
344+ QCOMPARE(map.value("countVisible").toBool(), false);
345+
346+ // Now make it visible and set it to 55 through D-Bus
347+ interface.call("Set", "com.canonical.Unity.Launcher.Item", "count", 55);
348+ interface.call("Set", "com.canonical.Unity.Launcher.Item", "countVisible", true);
349+
350+ // Fetch it again using GetAll
351+ reply = interface.call("GetAll");
352+ map = reply.value();
353+
354+ // Make sure values have changed on the D-Bus interface
355+ QCOMPARE(map.value("count").toInt(), 55);
356+ QCOMPARE(map.value("countVisible").toBool(), true);
357+
358+ // Now the item on the upper side of the API
359+ int index = launcherModel->findApplication("abs-icon");
360+ QCOMPARE(index >= 0, true);
361+
362+ // And make sure values have changed there as well
363+ QCOMPARE(launcherModel->get(index)->countVisible(), true);
364+ QCOMPARE(launcherModel->get(index)->count(), 55);
365+ }
366+
367+ void testRefresh() {
368+ QDBusInterface interface("com.canonical.Unity.Launcher", "/com/canonical/Unity/Launcher", "com.canonical.Unity.Launcher");
369+ QDBusReply<void> reply = interface.call("Refresh");
370+
371+ // Make sure the call to Refresh returned without error.
372+ QCOMPARE(reply.isValid(), true);
373+ }
294374};
295375
296376QTEST_GUILESS_MAIN(LauncherModelTest)
tests/plugins/Utils/CMakeLists.txt−1
@@ -11,7 +11,6 @@ macro(run_tests)
1111 -o ${CMAKE_BINARY_DIR}/${_test}.xml,xunitxml
1212 -o -,txt)
1313 add_test(NAME ${_test} COMMAND ${testCommand})
14− set_tests_properties(${_test} PROPERTIES ENVIRONMENT "LC_ALL=C")
1514 add_custom_target(${_test} env "LC_ALL=C" ${testCommand})
1615 add_executable(${_test}Exec ${_test}.cpp modeltest.cpp)
1716 qt5_use_modules(${_test}Exec Test Core Qml)
tests/qmltests/Dash/Previews/tst_PreviewPayments.qml+39
@@ -33,6 +33,10 @@ Rectangle {
3333 "source": { "price" : 0.99, "currency": "USD", "store_item_id": "com.example.invalid" }
3434 }
3535
36+ property var jsonPurchaseCancel: {
37+ "source": { "price" : 0.99, "currency": "USD", "store_item_id": "com.example.cancel" }
38+ }
39+
3640 SignalSpy {
3741 id: spy
3842 target: previewPayments
@@ -160,5 +164,40 @@ Rectangle {
160164 compare(args[1], "purchaseError");
161165 compare(args[2], jsonPurchaseError["source"]);
162166 }
167+
168+ function test_purchase_cancelled() {
169+ // The mock Payments triggers cancellation when com.example.cancel
170+ // is passed to it as store_item_id. Exercise it here
171+ previewPayments.widgetData = jsonPurchaseCancel;
172+
173+ var button = findChild(previewPayments, "paymentButton");
174+ var progress = findChild(previewPayments, "loadingBar");
175+ verify(button, "Button not found.");
176+ verify(progress, "Progress not found.");
177+
178+ mouseClick(button, button.width / 2, button.height / 2);
179+
180+ tryCompare(progress, "visible", true);
181+ tryCompare(progress, "opacity", 1);
182+ tryCompare(button, "visible", false);
183+ tryCompare(button, "opacity", 0);
184+
185+ paymentClient.process();
186+
187+ // Signal is not used at the moment, to avoid preview refresh.
188+ /*
189+ *spy.wait();
190+ *
191+ * var args = spy.signalArguments[0];
192+ * compare(args[0], "previewPayments");
193+ * compare(args[1], "purchaseCancelled");
194+ * compare(args[2], jsonPurchaseCancel["source"]);
195+ */
196+
197+ tryCompare(progress, "visible", false);
198+ tryCompare(progress, "opacity", 0);
199+ tryCompare(button, "visible", true);
200+ tryCompare(button, "opacity", 1);
201+ }
163202 }
164203}
tests/qmltests/Dash/tst_Card.qml+3−3
@@ -29,9 +29,9 @@ Rectangle {
2929
3030 property string cardData: '
3131 {
32− "art": "../../../tests/qmltests/Dash/artwork/music-player-design.png",
33− "mascot": "../../../tests/qmltests/Dash/artwork/avatar.png",
34− "emblem": "../../../tests/qmltests/Dash/artwork/emblem.png",
32+ "art": "'+Qt.resolvedUrl("artwork/music-player-design.png").toString()+'",
33+ "mascot": "'+Qt.resolvedUrl("artwork/avatar.png").toString()+'",
34+ "emblem": "'+Qt.resolvedUrl("artwork/emblem.png").toString()+'",
3535 "title": "foo",
3636 "subtitle": "bar",
3737 "summary": "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
tests/qmltests/Dash/tst_GenericScopeView.qml+129−1
@@ -1,5 +1,5 @@
11/*
2− * Copyright 2013 Canonical Ltd.
2+ * Copyright 2014 Canonical Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -43,6 +43,10 @@ Item {
4343 onLoadedChanged: if (loaded) genericScopeView.scope = scopes.getScope(2);
4444 }
4545
46+ MockScope {
47+ id: mockScope
48+ }
49+
4650 SignalSpy {
4751 id: spy
4852 }
@@ -92,6 +96,17 @@ Item {
9296 return findChild(genericScopeView, category);
9397 }
9498
99+ function scrollToEnd()
100+ {
101+ var categoryListView = findChild(genericScopeView, "categoryListView");
102+ waitForRendering(categoryListView);
103+ while (!categoryListView.atYEnd) {
104+ mouseFlick(genericScopeView, genericScopeView.width/2, genericScopeView.height - units.gu(8),
105+ genericScopeView.width/2, genericScopeView.y)
106+ tryCompare(categoryListView, "moving", false);
107+ }
108+ }
109+
95110 function test_isActive() {
96111 tryCompare(genericScopeView.scope, "isActive", false)
97112 genericScopeView.isCurrent = true
@@ -280,6 +295,7 @@ Item {
280295 mouseClick(tile, tile.width / 2, tile.height / 2);
281296 tryCompare(testCase.subPageLoader, "open", true);
282297 tryCompare(testCase.subPageLoader, "x", 0);
298+ tryCompare(findChild(genericScopeView, "categoryListView"), "visible", false);
283299 }
284300
285301 function closePreview() {
@@ -288,6 +304,9 @@ Item {
288304
289305 tryCompare(testCase.subPageLoader, "open", false);
290306 tryCompare(testCase.subPageLoader, "visible", false);
307+ var categoryListView = findChild(genericScopeView, "categoryListView");
308+ tryCompare(categoryListView, "visible", true);
309+ tryCompare(categoryListView, "x", 0);
291310 }
292311
293312 function test_previewOpenClose() {
@@ -301,6 +320,34 @@ Item {
301320 closePreview();
302321 }
303322
323+ function test_tryOpenNullPreview() {
324+ genericScopeView.scope = scopes.getScope("NullPreviewScope");
325+
326+ tryCompareFunction(function() {
327+ var cardGrid = findChild(genericScopeView, 0);
328+ if (cardGrid != null) {
329+ var tile = findChild(cardGrid, 0);
330+ return tile != null;
331+ }
332+ return false;
333+ },
334+ true);
335+ var tile = findChild(findChild(genericScopeView, 0), 0);
336+
337+ tryCompare(testCase.subPageLoader, "open", false);
338+ tryCompare(testCase.subPageLoader, "visible", false);
339+
340+ mouseClick(tile, tile.width / 2, tile.height / 2);
341+
342+ tryCompare(testCase.subPageLoader, "open", false);
343+ tryCompare(testCase.subPageLoader, "visible", false);
344+
345+ mousePress(tile, tile.width / 2, tile.height / 2);
346+ tryCompare(testCase.subPageLoader, "open", false);
347+ tryCompare(testCase.subPageLoader, "visible", false);
348+ mouseRelease(tile, tile.width / 2, tile.height / 2);
349+ }
350+
304351 function test_showPreviewCarousel() {
305352 var category = scrollToCategory("dashCategory1");
306353
@@ -426,6 +473,87 @@ Item {
426473 compare(image.source, data.logo, "Title image has the wrong source");
427474 }
428475
476+ function test_seeAllTwoCategoriesScenario1() {
477+ mockScope.setId("mockScope");
478+ mockScope.setName("Mock Scope");
479+ mockScope.categories.setCount(2);
480+ mockScope.categories.resultModel(0).setResultCount(50);
481+ mockScope.categories.resultModel(1).setResultCount(15);
482+ mockScope.categories.setLayout(0, "grid");
483+ mockScope.categories.setLayout(1, "grid");
484+ mockScope.categories.setHeaderLink(0, "");
485+ mockScope.categories.setHeaderLink(1, "");
486+ genericScopeView.scope = mockScope;
487+ waitForRendering(genericScopeView.categoryView);
488+
489+ var category0 = findChild(genericScopeView, "dashCategory0")
490+ var seeAll0 = findChild(category0, "seeAll")
491+
492+ waitForRendering(seeAll0);
493+ verify(category0.expandable);
494+ verify(!category0.expanded);
495+
496+ mouseClick(seeAll0, seeAll0.width / 2, seeAll0.height / 2);
497+ verify(category0.expanded);
498+ tryCompare(category0, "height", category0.item.expandedHeight + seeAll0.height);
499+ tryCompare(genericScopeView.categoryView, "contentY", units.gu(13.5));
500+
501+ scrollToEnd();
502+
503+ tryCompareFunction(function() { return findChild(genericScopeView, "dashCategory1") !== null; }, true);
504+ var category1 = findChild(genericScopeView, "dashCategory1")
505+ var seeAll1 = findChild(category1, "seeAll")
506+ verify(category1.expandable);
507+ verify(!category1.expanded);
508+
509+ mouseClick(seeAll1, seeAll1.width / 2, seeAll1.height / 2);
510+ verify(!category0.expanded);
511+ verify(category1.expanded);
512+ tryCompare(category1, "height", category1.item.expandedHeight + seeAll1.height);
513+ tryCompareFunction(function() {
514+ return genericScopeView.categoryView.contentY + category1.y + category1.height
515+ == genericScopeView.categoryView.contentHeight;}
516+ , true);
517+ }
518+
519+ function test_seeAllTwoCategoriesScenario2() {
520+ mockScope.setId("mockScope");
521+ mockScope.setName("Mock Scope");
522+ mockScope.categories.setCount(2);
523+ mockScope.categories.resultModel(0).setResultCount(15);
524+ mockScope.categories.resultModel(1).setResultCount(50);
525+ mockScope.categories.setLayout(0, "grid");
526+ mockScope.categories.setLayout(1, "grid");
527+ mockScope.categories.setHeaderLink(0, "");
528+ mockScope.categories.setHeaderLink(1, "");
529+ genericScopeView.scope = mockScope;
530+ waitForRendering(genericScopeView.categoryView);
531+
532+ var category0 = findChild(genericScopeView, "dashCategory0")
533+ var seeAll0 = findChild(category0, "seeAll")
534+
535+ waitForRendering(seeAll0);
536+ verify(category0.expandable);
537+ verify(!category0.expanded);
538+
539+ mouseClick(seeAll0, seeAll0.width / 2, seeAll0.height / 2);
540+ verify(category0.expanded);
541+ tryCompare(category0, "height", category0.item.expandedHeight + seeAll0.height);
542+
543+ scrollToEnd();
544+
545+ var category1 = findChild(genericScopeView, "dashCategory1")
546+ var seeAll1 = findChild(category1, "seeAll")
547+ verify(category1.expandable);
548+ verify(!category1.expanded);
549+
550+ mouseClick(seeAll1, seeAll1.width / 2, seeAll1.height / 2);
551+ verify(!category0.expanded);
552+ verify(category1.expanded);
553+ tryCompare(category1, "height", category1.item.expandedHeight + seeAll1.height);
554+ tryCompare(category1, "y", units.gu(5));
555+ }
556+
429557 function test_favorite_data() {
430558 return [
431559 { tag: "People", id: "MockScope1", favorite: true },
tests/qmltests/Launcher/tst_Launcher.qml+3−1
@@ -194,7 +194,9 @@ Item {
194194 var launcherListView = findChild(launcher, "launcherListView");
195195 for (var i = 0; i < launcherListView.count; ++i) {
196196 var delegate = findChild(launcherListView, "launcherDelegate" + i)
197− compare(findChild(delegate, "countEmblem").visible, LauncherModel.get(i).count > 0)
197+ compare(findChild(delegate, "countEmblem").visible, LauncherModel.get(i).countVisible)
198+ // Intentionally allow type coercion (string/number)
199+ compare(findChild(delegate, "countLabel").text == LauncherModel.get(i).count, true)
198200 }
199201 }
200202
tests/qmltests/Notifications/tst_Notifications.qml+1−1
@@ -476,7 +476,7 @@ Row {
476476 compare(buttonRow.visible, data.buttonRowVisible, "button visibility is incorrect")
477477
478478 var audioItem = findInvisibleChild(notification, "sound")
479− tryCompare(audioItem, "playbackState", data.hasSound ? Audio.PlayingState : Audio.StoppedState)
479+ compare(audioItem.playbackState, data.hasSound ? Audio.PlayingState : Audio.StoppedState, "Audio has wrong state")
480480
481481 if(data.buttonRowVisible) {
482482 var buttonCancel = findChild(buttonRow, "notify_button1")
tests/qmltests/Stages/RecursingChildSessionControl.qml+1−1
@@ -93,7 +93,7 @@ ColumnLayout {
9393 onCheckedChanged: {
9494 if (checked) {
9595 root.session.createSurface();
96− } else if (root.session.surface) {
96+ } else if (root.session && root.session.surface) {
9797 ApplicationTest.removeSurface(root.session.surface);
9898 }
9999 }
tests/qmltests/Stages/tst_ApplicationWindow.qml+30−12
@@ -50,22 +50,18 @@ Rectangle {
5050 ApplicationWindow {
5151 anchors.fill: parent
5252 application: fakeApplication
53+ orientation: Qt.PortraitOrientation
5354 }
5455 }
55− FocusScope {
56− id: appFocus
56+ Loader {
57+ id: applicationWindowLoader
5758 anchors {
5859 top: parent.top
5960 bottom: parent.bottom
6061 left: parent.left
6162 }
6263 width: units.gu(40)
63−
64− Loader {
65− id: applicationWindowLoader
66− anchors.fill: parent
67− sourceComponent: applicationWindowComponent
68− }
64+ sourceComponent: applicationWindowComponent
6965 }
7066
7167 Rectangle {
@@ -73,7 +69,7 @@ Rectangle {
7369 anchors {
7470 top: parent.top
7571 bottom: parent.bottom
76− left: appFocus.right
72+ left: applicationWindowLoader.right
7773 right: parent.right
7874 }
7975
@@ -147,6 +143,24 @@ Rectangle {
147143 }
148144 }
149145
146+ Button {
147+ anchors { left: parent.left; right: parent.right }
148+ text: "Rotate device \u27F3"
149+ onClicked: {
150+ var orientation = applicationWindowLoader.item.orientation
151+ if (orientation == Qt.PortraitOrientation) {
152+ orientation = Qt.LandscapeOrientation;
153+ } else if (orientation == Qt.LandscapeOrientation) {
154+ orientation = Qt.InvertedPortraitOrientation;
155+ } else if (orientation == Qt.InvertedPortraitOrientation) {
156+ orientation = Qt.InvertedLandscapeOrientation;
157+ } else {
158+ orientation = Qt.PortraitOrientation;
159+ }
160+ applicationWindowLoader.item.orientation = orientation;
161+ }
162+ }
163+
150164 }
151165 }
152166
@@ -384,15 +398,19 @@ Rectangle {
384398 verify(stateGroup.state === "void");
385399 }
386400
387− // should be forcing active focus when interactive changes,
388− // which will cause a FocusScope higher in the object hierarchy tp be given the focus
389401 function test_forceActiveFocusFollowsInterative() {
390402 fakeApplication.createSession();
391403 applicationWindowLoader.item.interactive = false;
392404 applicationWindowLoader.item.interactive = true;
393405 fakeSession.createSurface();
394406
395− compare(appFocus.focus, true);
407+ compare(fakeSession.surface.activeFocus, true);
408+
409+ applicationWindowLoader.item.interactive = false;
410+ compare(fakeSession.surface.activeFocus, false);
411+
412+ applicationWindowLoader.item.interactive = true;
413+ compare(fakeSession.surface.activeFocus, true);
396414 }
397415 }
398416}
tests/qmltests/Stages/tst_PhoneStage.qml+58
@@ -31,6 +31,8 @@ Item {
3131 anchors { fill: parent; rightMargin: units.gu(30) }
3232 dragAreaWidth: units.gu(2)
3333 maximizedAppTopMargin: units.gu(3) + units.dp(2)
34+ interactive: true
35+ orientation: Qt.PortraitOrientation
3436 }
3537
3638 Binding {
@@ -67,6 +69,21 @@ Item {
6769 ApplicationManager.get(appList.selectedAppIndex).setState(ApplicationInfoInterface.Stopped);
6870 }
6971 }
72+ Button {
73+ anchors { left: parent.left; right: parent.right }
74+ text: "Rotate device \u27F3"
75+ onClicked: {
76+ if (phoneStage.orientation == Qt.PortraitOrientation) {
77+ phoneStage.orientation = Qt.LandscapeOrientation;
78+ } else if (phoneStage.orientation == Qt.LandscapeOrientation) {
79+ phoneStage.orientation = Qt.InvertedPortraitOrientation;
80+ } else if (phoneStage.orientation == Qt.InvertedPortraitOrientation) {
81+ phoneStage.orientation = Qt.InvertedLandscapeOrientation;
82+ } else {
83+ phoneStage.orientation = Qt.PortraitOrientation;
84+ }
85+ }
86+ }
7087 }
7188 ListView {
7289 id: appList
@@ -256,6 +273,46 @@ Item {
256273 compare(ApplicationManager.focusedApplicationId, selectedApp.appId);
257274 }
258275
276+ function test_orientation_change_sent_to_focused_app() {
277+ phoneStage.orientation = Qt.PortraitOrientation;
278+ addApps(1);
279+
280+ var spreadView = findChild(phoneStage, "spreadView");
281+ var app = findChild(spreadView, "appDelegate0");
282+ tryCompare(app, "orientation", Qt.PortraitOrientation);
283+
284+ phoneStage.orientation = Qt.LandscapeOrientation;
285+ tryCompare(app, "orientation", Qt.LandscapeOrientation);
286+ }
287+
288+ function test_orientation_change_not_sent_to_apps_while_spread_open() {
289+ phoneStage.orientation = Qt.PortraitOrientation;
290+ addApps(1);
291+
292+ var spreadView = findChild(phoneStage, "spreadView");
293+ var app = findChild(spreadView, "appDelegate0");
294+ tryCompare(app, "orientation", Qt.PortraitOrientation);
295+
296+ goToSpread();
297+ phoneStage.orientation = Qt.LandscapeOrientation;
298+ tryCompare(app, "orientation", Qt.PortraitOrientation);
299+ }
300+
301+ function test_orientation_change_not_sent_to_unfocused_app_until_it_focused() {
302+ phoneStage.orientation = Qt.PortraitOrientation;
303+ addApps(1);
304+
305+ var spreadView = findChild(phoneStage, "spreadView");
306+ var app = findChild(spreadView, "appDelegate0");
307+
308+ goToSpread();
309+ phoneStage.orientation = Qt.LandscapeOrientation;
310+ tryCompare(app, "orientation", Qt.PortraitOrientation);
311+
312+ phoneStage.select(app.application.appId);
313+ tryCompare(app, "orientation", Qt.LandscapeOrientation);
314+ }
315+
259316 function cleanup() {
260317 while (ApplicationManager.count > 1) {
261318 var oldCount = ApplicationManager.count;
@@ -263,6 +320,7 @@ Item {
263320 ApplicationManager.stopApplication(ApplicationManager.get(closingIndex).appId)
264321 tryCompare(ApplicationManager, "count", oldCount - 1)
265322 }
323+ phoneStage.orientation = Qt.PortraitOrientation;
266324 }
267325 }
268326}
tests/qmltests/Stages/tst_SessionContainer.qml+63
@@ -41,6 +41,7 @@ Rectangle {
4141 SessionContainer {
4242 id: sessionContainer
4343 anchors.fill: parent
44+ orientation: Qt.PortraitOrientation
4445 }
4546 }
4647
@@ -112,6 +113,24 @@ Rectangle {
112113 session: sessionContainerLoader.item ? sessionContainerLoader.item.session : null
113114 }
114115 }
116+
117+ Button {
118+ anchors { left: parent.left; right: parent.right }
119+ text: "Rotate device \u27F3"
120+ onClicked: {
121+ var orientation = sessionContainerLoader.item.orientation
122+ if (orientation == Qt.PortraitOrientation) {
123+ orientation = Qt.LandscapeOrientation;
124+ } else if (orientation == Qt.LandscapeOrientation) {
125+ orientation = Qt.InvertedPortraitOrientation;
126+ } else if (orientation == Qt.InvertedPortraitOrientation) {
127+ orientation = Qt.InvertedLandscapeOrientation;
128+ } else {
129+ orientation = Qt.PortraitOrientation;
130+ }
131+ sessionContainerLoader.item.orientation = orientation;
132+ }
133+ }
115134 }
116135 }
117136
@@ -263,5 +282,49 @@ Rectangle {
263282 // wait for animation to end
264283 tryCompareFunction(function() { return isContainerAnimating(childContainer); }, false);
265284 }
285+
286+ function test_orientationPropagatedToChildren_data() {
287+ return [ { tag: "count=1", count: 1 },
288+ { tag: "count=4", count: 4 } ];
289+ }
290+
291+ /* Test orientation changes are propagated to all children immediately */
292+ function test_orientationPropagatedToChildren(data) {
293+ sessionCheckbox.checked = true;
294+ var rootSessionContainer = sessionContainerLoader.item;
295+ compare(rootSessionContainer.childSessions.count(), 0);
296+
297+ var i;
298+ var sessions = [];
299+ for (i = 0; i < data.count; i++) {
300+ var session = ApplicationTest.addChildSession(rootSessionContainer.session,
301+ "gallery");
302+ session.createSurface();
303+ rootSessionContainer.session.addChildSession(session);
304+
305+ // Check child SessionContainer has orientation matching the parent
306+ var delegate = findChild(rootSessionContainer, "childDelegate" + i);
307+ var childSessionContainer = findChild(delegate, "sessionContainer");
308+
309+ tryCompare(rootSessionContainer, "orientation", childSessionContainer.orientation);
310+
311+ sessions.push(session);
312+ }
313+
314+ // Change orientation and verify all children updated
315+ rootSessionContainer.orientation = Qt.LandscapeOrientation;
316+
317+ for (i = 0; i < data.count; i++) {
318+ var delegate = findChild(rootSessionContainer, "childDelegate" + i);
319+ var childSessionContainer = findChild(delegate, "sessionContainer");
320+
321+ tryCompare(rootSessionContainer, "orientation", childSessionContainer.orientation);
322+ }
323+
324+ // Clean up
325+ for (i = data.count-1; i >= 0; i--) {
326+ ApplicationTest.removeSession(sessions[i]);
327+ }
328+ }
266329 }
267330}
tests/qmltests/tst_Shell.qml+103−28
@@ -21,6 +21,7 @@ import QtQuick 2.0
2121import QtTest 1.0
2222import GSettings 1.0
2323import LightDM 0.1 as LightDM
24+import Ubuntu.Components 1.1
2425import Ubuntu.Telephony 0.1 as Telephony
2526import Unity.Application 0.1
2627import Unity.Connectivity 0.1
@@ -29,10 +30,9 @@ import Powerd 0.1
2930
3031import "../../qml"
3132
32−Item {
33+Row {
3334 id: root
34− width: shell.width
35− height: shell.height
35+ spacing: 0
3636
3737 QtObject {
3838 id: applicationArguments
@@ -50,13 +50,52 @@ Item {
5050 }
5151 }
5252
53− Shell {
54− id: shell
53+ Loader {
54+ id: shellLoader
55+
56+ // Copied from Shell.qml
57+ property bool tablet: false
58+ width: tablet ? units.gu(160)
59+ : applicationArguments.hasGeometry() ? applicationArguments.width()
60+ : units.gu(40)
61+ height: tablet ? units.gu(100)
62+ : applicationArguments.hasGeometry() ? applicationArguments.height()
63+ : units.gu(71)
64+
65+ property bool itemDestroyed: false
66+ sourceComponent: Component {
67+ Shell {
68+ Component.onDestruction: {
69+ shellLoader.itemDestroyed = true;
70+ }
71+ }
72+ }
5573 }
5674
57− Component {
58− id: shellComponent
59− Shell {}
75+ Rectangle {
76+ color: "white"
77+ width: units.gu(30)
78+ height: shellLoader.height
79+
80+ Column {
81+ anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
82+ spacing: units.gu(1)
83+ Row {
84+ anchors { left: parent.left; right: parent.right }
85+ Button {
86+ text: "Show Greeter"
87+ onClicked: {
88+ if (shellLoader.status !== Loader.Ready)
89+ return;
90+
91+ var greeter = testCase.findChild(shellLoader.item, "greeter");
92+ if (!greeter.shown) {
93+ greeter.show();
94+ }
95+ }
96+ }
97+ }
98+ }
6099 }
61100
62101 SignalSpy {
@@ -81,10 +120,13 @@ Item {
81120 }
82121
83122 UT.UnityTestCase {
123+ id: testCase
84124 name: "Shell"
85125 when: windowShown
86126
87− function initTestCase() {
127+ property Item shell: shellLoader.status === Loader.Ready ? shellLoader.item : null
128+
129+ function init() {
88130 swipeAwayGreeter();
89131
90132 sessionSpy.target = findChild(shell, "greeter")
@@ -92,17 +134,30 @@ Item {
92134 }
93135
94136 function cleanup() {
95− // If a test invoked the greeter, make sure we swipe it away again
96− var greeter = findChild(shell, "greeter");
97− if (greeter.shown) {
98− swipeAwayGreeter();
99− }
137+ shellLoader.itemDestroyed = false;
138+
139+ shellLoader.active = false;
140+
141+ tryCompare(shellLoader, "status", Loader.Null);
142+ tryCompare(shellLoader, "item", null);
143+ // Loader.status might be Loader.Null and Loader.item might be null but the Loader
144+ // item might still be alive. So if we set Loader.active back to true
145+ // again right now we will get the very same Shell instance back. So no reload
146+ // actually took place. Likely because Loader waits until the next event loop
147+ // iteration to do its work. So to ensure the reload, we will wait until the
148+ // Shell instance gets destroyed.
149+ tryCompare(shellLoader, "itemDestroyed", true);
100150
101151 // kill all (fake) running apps
102152 killApps(ApplicationManager);
103153
104− waitForUIToSettle();
105− hideIndicators();
154+ unlockAllModemsSpy.clear()
155+
156+ // reload our test subject to get it in a fresh state once again
157+ shellLoader.active = true;
158+
159+ tryCompare(shellLoader, "status", Loader.Ready);
160+ removeTimeConstraintsFromDirectionalDragAreas(shellLoader.item);
106161 }
107162
108163 function killApps() {
@@ -115,10 +170,11 @@ Item {
115170
116171 function test_leftEdgeDrag_data() {
117172 return [
118− {tag: "without launcher", revealLauncher: false, swipeLength: units.gu(27), appHides: true},
119− {tag: "with launcher", revealLauncher: true, swipeLength: units.gu(27), appHides: true},
120− {tag: "small swipe", revealLauncher: false, swipeLength: units.gu(25), appHides: false},
121− {tag: "long swipe", revealLauncher: false, swipeLength: units.gu(27), appHides: true}
173+ {tag: "without launcher", revealLauncher: false, swipeLength: units.gu(27), appHides: true, focusedApp: "dialer-app", launcherHides: true},
174+ {tag: "with launcher", revealLauncher: true, swipeLength: units.gu(27), appHides: true, focusedApp: "dialer-app", launcherHides: true},
175+ {tag: "small swipe", revealLauncher: false, swipeLength: units.gu(25), appHides: false, focusedApp: "dialer-app", launcherHides: false},
176+ {tag: "long swipe", revealLauncher: false, swipeLength: units.gu(27), appHides: true, focusedApp: "dialer-app", launcherHides: true},
177+ {tag: "long swipe", revealLauncher: false, swipeLength: units.gu(27), appHides: true, focusedApp: "unity8-dash", launcherHides: false}
122178 ];
123179 }
124180
@@ -126,16 +182,22 @@ Item {
126182 dragLauncherIntoView();
127183 tapOnAppIconInLauncher();
128184 waitUntilApplicationWindowIsFullyVisible();
185+ ApplicationManager.focusApplication(data.focusedApp)
186+ waitUntilApplicationWindowIsFullyVisible();
129187
130188 if (data.revealLauncher) {
131189 dragLauncherIntoView();
132190 }
133191
134192 swipeFromLeftEdge(data.swipeLength);
135− if (data.appHides)
193+ if (data.appHides) {
136194 waitUntilDashIsFocused();
137− else
195+ } else {
138196 waitUntilApplicationWindowIsFullyVisible();
197+ }
198+
199+ var launcher = findChild(shell, "launcherPanel");
200+ tryCompare(launcher, "x", data.launcherHides ? -launcher.width : 0)
139201 }
140202
141203 function test_suspend() {
@@ -241,10 +303,27 @@ Item {
241303 tryCompare(item, "visible", false);
242304 }
243305
306+ // wait until any transition animation has finished
307+ function waitUntilTransitionsEnd(stateGroup) {
308+ var transitions = stateGroup.transitions;
309+ for (var i = 0; i < transitions.length; ++i) {
310+ var transition = transitions[i];
311+ tryCompare(transition, "running", false, 2000);
312+ }
313+ }
314+
315+ // Wait until the ApplicationWindow for the given Application object is fully loaded
316+ // (ie, the real surface has replaced the splash screen)
317+ function waitUntilAppWindowIsFullyLoaded(app) {
318+ var appWindow = findChild(shell, "appWindow_" + app.appId);
319+ var appWindowStateGroup = findInvisibleChild(appWindow, "applicationWindowStateGroup");
320+ tryCompareFunction(function() { return appWindowStateGroup.state === "surface" }, true);
321+ waitUntilTransitionsEnd(appWindowStateGroup);
322+ }
323+
244324 function test_surfaceLosesFocusWhilePanelIsOpen() {
245325 var app = ApplicationManager.startApplication("dialer-app");
246− // wait until the app is fully loaded (ie, real surface replaces splash screen)
247− tryCompareFunction(function() { return app.session !== null && app.session.surface !== null }, true);
326+ waitUntilAppWindowIsFullyLoaded(app);
248327
249328 tryCompare(app.session.surface, "focus", true);
250329
@@ -465,13 +544,9 @@ Item {
465544 }
466545
467546 function test_unlockAllModemsOnBoot() {
468− unlockAllModemsSpy.clear()
469− // actually create an object so we notice the onCompleted signal
470− var greeter = shellComponent.createObject(root)
471547 // TODO reenable when service ready (LP: #1361074)
472548 expectFail("", "Unlock on boot temporarily disabled");
473549 tryCompare(unlockAllModemsSpy, "count", 1)
474− greeter.destroy()
475550 }
476551 }
477552}