on xenial help sign in
[ Kevin Gunn ]

* No change, rebuild for gcc5 [ CI Train Bot ] * Resync trunk. added: po/cy.po * Resync trunk. added: po/cy.po [ Daniel d'Andrada ] * Stages now control the "requestedState" property of applications * Stages now control the "requestedState" property of applications arrow label (LP: #1431564) [ Daniel d'Andrada ] * TouchRegistry: remove null candidates from list of candidates (LP: #1473492) [ Lukáš Tinkl ] * Fix power dialogs on desktop * Provide DBUS compatibility with various session services (suspend/hibernate, lock/unlock, screensaver, etc) * React on PrtScr keyboard shortcut for taking screenshots on desktop (LP: #1474149) * launcher parity: close apps from quicklist (LP: #1457201) [ Michael Zanetti ] * Implement first edition for a desktop Alt+Tab spread * drop the gcc-4.9 dependency (LP: #1452348) [ Mirco Müller ] * Added corresponding tests and visual tweaks to a launcher-item's progress-overlay. * Added corresponding tests and visual tweaks to a launcher-item's progress-overlay. * Implemented alert/wiggle feature for launcher-icons. * Implemented alert/wiggle feature for launcher-icons. * makes left swip reset the search string. (LP: #1413791) * Don't expand indicators when tap to return to call. (LP: #1453217) * makes left swip reset the search string. (LP: #1413791)

commitd285966f74e4c83ffa625f066541c7801cfc7af7 authorAndrea Cimitan <andrea.cimitan@gmail.com> dateTue 11 Aug 2015, 14:55 (11 years ago) parent6222adcf137158
99 files changed+4241−510
CMakeLists.txt+1−1 debian/changelog+41−28 debian/control+6−8 libs/UbuntuGestures/TouchRegistry.cpp+60−14 libs/UbuntuGestures/TouchRegistry.h+4−1 plugins/AccountsService/AccountsService.cpp+1−2 plugins/Greeter/Unity/Launcher/CMakeLists.txt+1−2 plugins/Greeter/Unity/Launcher/launcheritem.cpp+14 plugins/Greeter/Unity/Launcher/launcheritem.h+4 plugins/Greeter/Unity/Launcher/launchermodelas.cpp+28−12 plugins/Greeter/Unity/Launcher/launchermodelas.h+1 plugins/ScreenGrabber/screengrabber.cpp+31−20 plugins/ScreenGrabber/screengrabber.h+8−6 plugins/Unity/Launcher/CMakeLists.txt+1−2 plugins/Unity/Launcher/asadapter.cpp+4−2 plugins/Unity/Launcher/asadapter.h+1−4 plugins/Unity/Launcher/dbusinterface.cpp+42−12 plugins/Unity/Launcher/dbusinterface.h+2 plugins/Unity/Launcher/gsettings.cpp+1−1 plugins/Unity/Launcher/launcheritem.cpp+33 plugins/Unity/Launcher/launcheritem.h+4 plugins/Unity/Launcher/launchermodel.cpp+78−26 plugins/Unity/Launcher/launchermodel.h+3−1 plugins/Unity/Launcher/quicklistentry.cpp+6−2 plugins/Unity/Launcher/quicklistentry.h+3−1 plugins/Unity/Launcher/quicklistmodel.cpp+12−1 plugins/Unity/Launcher/quicklistmodel.h+3−1 plugins/Unity/Session/CMakeLists.txt+1 plugins/Unity/Session/dbusunitysessionservice.cpp+372−24 plugins/Unity/Session/dbusunitysessionservice.h+200−16 plugins/Unity/Session/plugin.cpp+2 plugins/Utils/CMakeLists.txt+1 po/ar.po+18−14 po/cy.po+722 po/gl.po+4−4 po/hu.po+4−4 po/pt.po+17−17 po/pt_BR.po+12−12 po/zh_TW.po+17−20 qml/Components/PhysicalKeysMapper.qml+24 qml/Components/ScreenGrabber.qml+1−5 qml/Components/Unity8Settings.qml+50 qml/Dash/Dash.qml+1−1 qml/Dash/DashContent.qml+1 qml/Dash/GenericScopeView.qml+5 qml/Launcher/LauncherDelegate.qml+100−14 qml/Launcher/LauncherPanel.qml+93−13 qml/Launcher/graphics/progressbar-fill@30.png qml/Launcher/graphics/progressbar-fill@30.sci−5 qml/Launcher/graphics/progressbar-trough@30.png qml/Launcher/graphics/progressbar-trough@30.sci−5 qml/OrientedShell.qml+3−3 qml/Panel/IndicatorsMenu.qml+3−1 qml/Panel/Panel.qml+1 qml/Shell.qml+16−13 qml/Stages/ApplicationWindow.qml+3 qml/Stages/DecoratedWindow.qml+72−8 qml/Stages/DesktopStage.qml+511−57 qml/Stages/PhoneStage.qml+10 qml/Stages/SpreadMaths.qml+164 qml/Stages/TabletStage.qml+14 qml/Stages/graphics/window-close.svg+64 tests/libs/UbuntuGestures/tst_TouchRegistry.cpp+47 tests/mocks/Unity/Application/ApplicationInfo.cpp+40−6 tests/mocks/Unity/Application/ApplicationInfo.h+7 tests/mocks/Unity/Application/ApplicationManager.cpp+2−49 tests/mocks/Unity/Application/ApplicationManager.h−7 tests/mocks/Unity/Launcher/CMakeLists.txt+1−1 tests/mocks/Unity/Launcher/MockLauncherItem.cpp+22 tests/mocks/Unity/Launcher/MockLauncherItem.h+4 tests/mocks/Unity/Launcher/MockLauncherModel.cpp+54−1 tests/mocks/Unity/Launcher/MockLauncherModel.h+4 tests/mocks/Utils/CMakeLists.txt+3−1 tests/mocks/Utils/plugin.cpp+9−1 tests/plugins/CMakeLists.txt+1 tests/plugins/Greeter/Unity/Launcher/CMakeLists.txt+1−2 tests/plugins/Greeter/Unity/Launcher/launchermodelastest.cpp+3 tests/plugins/ScreenGrabber/CMakeLists.txt+14 tests/plugins/ScreenGrabber/ScreenGrabberTest.cpp+65 tests/plugins/ScreenGrabber/grabber.qml+31 tests/plugins/Unity/Launcher/CMakeLists.txt+1−1 tests/plugins/Unity/Launcher/launchermodeltest.cpp+67−11 tests/plugins/Unity/Session/sessionbackendtest.cpp+54−8 tests/qmltests/Components/tst_PhysicalKeysMapper.qml+5 tests/qmltests/Launcher/tst_Launcher.qml+210−7 tests/qmltests/Panel/tst_Panel.qml+18 tests/qmltests/Stages/ApplicationCheckBox.qml+21 tests/qmltests/Stages/tst_DesktopStage.qml+2−2 tests/qmltests/Stages/tst_PhoneStage.qml+75−3 tests/qmltests/Stages/tst_TabletStage.qml+161−1 tests/qmltests/tst_OrientedShell.qml+3−3 tests/qmltests/tst_Shell.qml+390−23 tests/scripts/README+9 tests/scripts/alert-launcher-icon.sh+3 tests/scripts/get-progress.sh+3 tests/scripts/list-launcher-icons.sh+3 tests/scripts/set-count-visible.sh+3 tests/scripts/set-count.sh+3 tests/scripts/set-progress.sh+3
CMakeLists.txt+1−1
@@ -56,7 +56,7 @@ find_package(Qt5DBus 5.2 REQUIRED)
5656find_package(Qt5Concurrent 5.2 REQUIRED)
5757find_package(Qt5Sql 5.2 REQUIRED)
5858
59−pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=6)
59+pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=7)
6060
6161# Standard install paths
6262include(GNUInstallDirs)
debian/changelog+41−28
@@ -1,44 +1,57 @@
1−unity8 (8.10+15.10.20150721-0ubuntu1) wily; urgency=medium
1+unity8 (8.10+15.10.20150804-0ubuntu1) wily; urgency=medium
22
3− [ Albert Astals Cid ]
4− * Add context for "Please re-enter" (LP: #1377195)
5− * Fix i18n in scope tool (LP: #1317541)
6− * Fix warning when doing make try
7− * New phone background provided by design (LP: #1364537)
8− * PreviewExpandable should be not expanded on startup
9− * Update state of indicators bar only when the user is actively
10− dragging himself (LP: #1457044)
11− * icon-actions preview widget
12− * make testPageHeader more stable
3+ [ Kevin Gunn ]
4+ * No change, rebuild for gcc5
135
146 [ CI Train Bot ]
15− * Resync trunk.
7+ * Resync trunk. added: po/cy.po
8+ * Resync trunk. added: po/cy.po
169
17− [ Josh Arenson ]
18− * Add handling for the location page to the autopilot settings wizard
19− test
10+ [ Daniel d'Andrada ]
11+ * Stages now control the "requestedState" property of applications
12+ * Stages now control the "requestedState" property of applications
2013
21− [ Michael Zanetti ]
14+ -- CI Train Bot <ci-train-bot@canonical.com> Tue, 04 Aug 2015 20:56:05 +0000
15+
16+unity8 (8.10+15.10.20150729-0ubuntu1) wily; urgency=medium
17+
18+ [ CI Train Bot ]
19+ * Resync trunk.
2220 * allow opening the manage dash area by clicking with a mouse on the
23− arrow label
21+ arrow label (LP: #1431564)
2422
25− [ handsome_feng ]
26− * Added touch state on navigation. (LP: #1383213)
23+ [ Daniel d'Andrada ]
24+ * TouchRegistry: remove null candidates from list of candidates (LP:
25+ #1473492)
2726
28− [ handsome_feng<445865575@qq.com> ]
29− * Added touch state on navigation. (LP: #1383213)
27+ [ Lukáš Tinkl ]
28+ * Fix power dialogs on desktop
29+ * Provide DBUS compatibility with various session services
30+ (suspend/hibernate, lock/unlock, screensaver, etc)
31+ * React on PrtScr keyboard shortcut for taking screenshots on desktop
32+ (LP: #1474149)
33+ * launcher parity: close apps from quicklist (LP: #1457201)
3034
31− -- CI Train Bot <ci-train-bot@canonical.com> Tue, 21 Jul 2015 09:10:05 +0000
35+ [ Michael Zanetti ]
36+ * Implement first edition for a desktop Alt+Tab spread
37+ * drop the gcc-4.9 dependency (LP: #1452348)
3238
33−unity8 (8.10+15.10.20150713-0ubuntu1) wily; urgency=medium
39+ [ Mirco Müller ]
40+ * Added corresponding tests and visual tweaks to a launcher-item's
41+ progress-overlay.
42+ * Added corresponding tests and visual tweaks to a launcher-item's
43+ progress-overlay.
44+ * Implemented alert/wiggle feature for launcher-icons.
45+ * Implemented alert/wiggle feature for launcher-icons.
3446
35− [ Albert Astals Cid ]
36− * Revert Session Screenshotter (LP: #1473476)
47+ [ handsome_feng ]
48+ * makes left swip reset the search string. (LP: #1413791)
3749
38− [ Daniel d'Andrada ]
39− * Dismiss the apps spread if greeter gets shown (LP: #1471609)
50+ [ handsome_feng<445865575@qq.com> ]
51+ * Don't expand indicators when tap to return to call. (LP: #1453217)
52+ * makes left swip reset the search string. (LP: #1413791)
4053
41− -- CI Train Bot <ci-train-bot@canonical.com> Mon, 13 Jul 2015 14:17:20 +0000
54+ -- CI Train Bot <ci-train-bot@canonical.com> Wed, 29 Jul 2015 12:39:01 +0000
4255
4356unity8 (8.10+15.10.20150708.2-0ubuntu1) wily; urgency=medium
4457
debian/control+6−8
@@ -6,12 +6,10 @@ Build-Depends: cmake,
66 dbus-test-runner,
77 debhelper (>= 9),
88 doxyqml,
9−# We rely on C++11 features, and to prevent from ABI breaks
10−# in libstdc++ causing us issues, we explicitly select a G++
11−# version. To allow cross-compiling to work, we also must
12−# append :native to g++-4.9 so we don't try to run armhf g++
9+# To allow cross-compiling to work, we also must append :native
10+# to g++ so we don't try to run armhf g++
1311# on an x86 CPU for eaxmple, when cross-compiling.
14− g++-4.9:native,
12+ g++:native,
1513 libandroid-properties-dev,
1614 graphviz,
1715 gsettings-ubuntu-schemas (>= 0.0.2+14.10.20140815),
@@ -30,7 +28,7 @@ Build-Depends: cmake,
3028 libqt5xmlpatterns5-dev,
3129 libsystemsettings-dev,
3230 libudev-dev,
33− libunity-api-dev (>= 7.97),
31+ libunity-api-dev (>= 7.99),
3432 libusermetricsoutput1-dev,
3533 libxcb1-dev,
3634 pkg-config,
@@ -101,7 +99,7 @@ Depends: gsettings-desktop-schemas,
10199 qtdeclarative5-ubuntu-telephony0.1,
102100 qtdeclarative5-ubuntu-web-plugin,
103101 ubuntu-system-settings,
104− unity-launcher-impl-4,
102+ unity-launcher-impl-7,
105103 unity8-common (= ${source:Version}),
106104 unity8-private (= ${binary:Version}),
107105 unity8-private | unity-launcher-impl,
@@ -191,7 +189,7 @@ Depends: accountsservice-ubuntu-schemas (>= 0.0.3),
191189 ${misc:Depends},
192190 ${shlibs:Depends},
193191Provides: unity-launcher-impl,
194− unity-launcher-impl-4,
192+ unity-launcher-impl-7,
195193Description: Unity 8 private libs
196194 The Unity 8 shell is the primary user interface for Ubuntu devices.
197195 .
libs/UbuntuGestures/TouchRegistry.cpp+60−14
@@ -271,6 +271,8 @@ void TouchRegistry::addCandidateOwnerForTouch(int id, QQuickItem *candidate)
271271 this, &TouchRegistry::rejectCandidateOwnerForTouch);
272272
273273 touchInfo->candidates.append(candidateInfo);
274+
275+ connect(candidate, &QObject::destroyed, this, [=](){ pruneNullCandidatesForTouch(id); });
274276}
275277
276278void TouchRegistry::addTouchWatcher(int touchId, QQuickItem *watcher)
@@ -296,25 +298,53 @@ void TouchRegistry::removeCandidateOwnerForTouch(int id, QQuickItem *candidate)
296298 Pool<TouchInfo>::Iterator touchInfo = findTouchInfo(id);
297299 if (!touchInfo) { qFatal("TouchRegistry: Failed to find TouchInfo"); }
298300
299− int indexRemoved = -1;
300301
301302 // TODO: check if the candidate is in fact the owner of the touch
302303
303− for (int i = 0; i < touchInfo->candidates.count() && indexRemoved == -1; ++i) {
304− CandidateInfo &candidateInfo = touchInfo->candidates[i];
305− if (candidateInfo.item == candidate) {
306− Q_ASSERT(i > 0 || candidateInfo.state == CandidateInfo::Undecided);
307− if (i == 0 && candidateInfo.state != CandidateInfo::Undecided) {
308− qCritical("TouchRegistry: touch owner is being removed.");
309− }
310− delete candidateInfo.inactivityTimer;
311− candidateInfo.inactivityTimer = nullptr;
312− touchInfo->candidates.removeAt(i);
313− indexRemoved = i;
304+ bool removed = false;
305+ for (int i = 0; i < touchInfo->candidates.count() && !removed; ++i) {
306+ if (touchInfo->candidates[i].item == candidate) {
307+ removeCandidateOwnerForTouchByIndex(touchInfo, i);
308+ removed = true;
314309 }
315310 }
311+}
312+
313+void TouchRegistry::pruneNullCandidatesForTouch(int touchId)
314+{
315+ #if TOUCHREGISTRY_DEBUG
316+ UG_DEBUG << "pruneNullCandidatesForTouch touchId" << touchId;
317+ #endif
318+
319+ Pool<TouchInfo>::Iterator touchInfo = findTouchInfo(touchId);
320+ if (!touchInfo) {
321+ // doesn't matter as touch is already gone.
322+ return;
323+ }
316324
317− if (indexRemoved == 0) {
325+ int i = 0;
326+ while (i < touchInfo->candidates.count()) {
327+ if (touchInfo->candidates[i].item.isNull()) {
328+ removeCandidateOwnerForTouchByIndex(touchInfo, i);
329+ } else {
330+ ++i;
331+ }
332+ }
333+}
334+
335+void TouchRegistry::removeCandidateOwnerForTouchByIndex(Pool<TouchRegistry::TouchInfo>::Iterator &touchInfo,
336+ int candidateIndex)
337+{
338+ // TODO: check if the candidate is in fact the owner of the touch
339+
340+ Q_ASSERT(candidateIndex < touchInfo->candidates.count());
341+
342+ if (candidateIndex == 0 && touchInfo->candidates[candidateIndex].state != CandidateInfo::Undecided) {
343+ qCritical("TouchRegistry: touch owner is being removed.");
344+ }
345+ removeCandidateHelper(touchInfo, candidateIndex);
346+
347+ if (candidateIndex == 0) {
318348 // the top candidate has been removed. if the new top candidate
319349 // wants the touch let him know he's now the owner.
320350 if (touchInfo->isOwned()) {
@@ -359,6 +389,7 @@ void TouchRegistry::requestTouchOwnership(int id, QQuickItem *candidate)
359389 touchInfo->candidates.append(candidateInfo);
360390 // it's the last one
361391 candidateIndex = touchInfo->candidates.count() - 1;
392+ connect(candidate, &QObject::destroyed, this, [=](){ pruneNullCandidatesForTouch(id); });
362393 }
363394
364395 // If it's the top candidate it means it's now the owner. Let
@@ -430,7 +461,7 @@ void TouchRegistry::rejectCandidateOwnerForTouch(int id, QQuickItem *candidate)
430461 QCoreApplication::sendEvent(candidate, &lostOwnershipEvent);
431462 }
432463
433− touchInfo->candidates.removeAt(rejectedCandidateIndex);
464+ removeCandidateHelper(touchInfo, rejectedCandidateIndex);
434465
435466 if (rejectedCandidateIndex == 0) {
436467 // the top candidate has been removed. if the new top candidate
@@ -441,6 +472,21 @@ void TouchRegistry::rejectCandidateOwnerForTouch(int id, QQuickItem *candidate)
441472 }
442473}
443474
475+void TouchRegistry::removeCandidateHelper(Pool<TouchInfo>::Iterator &touchInfo, int candidateIndex)
476+{
477+ {
478+ CandidateInfo &candidateInfo = touchInfo->candidates[candidateIndex];
479+
480+ delete candidateInfo.inactivityTimer;
481+ candidateInfo.inactivityTimer = nullptr;
482+
483+ if (candidateInfo.item) {
484+ disconnect(candidateInfo.item.data(), nullptr, this, nullptr);
485+ }
486+ }
487+ touchInfo->candidates.removeAt(candidateIndex);
488+}
489+
444490////////////////////////////////////// TouchRegistry::TouchInfo ////////////////////////////////////
445491
446492TouchRegistry::TouchInfo::TouchInfo(int id)
libs/UbuntuGestures/TouchRegistry.h+4−1
@@ -149,7 +149,6 @@ private:
149149 // yet because of undecided candidates higher up.
150150 InterimOwner = 2
151151 } state;
152− // TODO: Prune candidates that become null and resolve ownership accordingly.
153152 QPointer<QQuickItem> item;
154153 QPointer<UbuntuGestures::CandidateInactivityTimer> inactivityTimer;
155154 };
@@ -172,6 +171,10 @@ private:
172171 QList<QPointer<QQuickItem>> watchers;
173172 };
174173
174+ void pruneNullCandidatesForTouch(int touchId);
175+ void removeCandidateOwnerForTouchByIndex(Pool<TouchInfo>::Iterator &touchInfo, int candidateIndex);
176+ void removeCandidateHelper(Pool<TouchInfo>::Iterator &touchInfo, int candidateIndex);
177+
175178 Pool<TouchInfo>::Iterator findTouchInfo(int id);
176179
177180 void deliverTouchUpdatesToUndecidedCandidatesAndWatchers(const QTouchEvent *event);
plugins/AccountsService/AccountsService.cpp+1−2
@@ -141,9 +141,9 @@ void AccountsService::updateDemoEdges(bool async)
141141 this, [this](QDBusPendingCallWatcher* watcher) {
142142
143143 QDBusPendingReply<QDBusVariant> reply = *watcher;
144+ watcher->deleteLater();
144145 if (reply.isError()) {
145146 qWarning() << "Failed to get 'demo-edges' property - " << reply.error().message();
146− watcher->deleteLater();
147147 return;
148148 }
149149
@@ -152,7 +152,6 @@ void AccountsService::updateDemoEdges(bool async)
152152 m_demoEdges = demoEdges;
153153 Q_EMIT demoEdgesChanged();
154154 }
155− watcher->deleteLater();
156155 });
157156 if (!async) {
158157 watcher->waitForFinished();
plugins/Greeter/Unity/Launcher/CMakeLists.txt+1−2
@@ -1,5 +1,4 @@
1−pkg_check_modules(LAUNCHER_API REQUIRED unity-shell-launcher=6)
2−pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=6)
1+pkg_check_modules(LAUNCHER_API REQUIRED unity-shell-launcher=7)
32pkg_check_modules(GSETTINGS_QT REQUIRED gsettings-qt)
43
54add_definitions(-DSM_BUSNAME=systemBus)
plugins/Greeter/Unity/Launcher/launcheritem.cpp+14
@@ -31,6 +31,7 @@ LauncherItem::LauncherItem(const QString &appId, const QString &name, const QStr
3131 m_count(0),
3232 m_countVisible(false),
3333 m_focused(false),
34+ m_alerting(false),
3435 m_quickList(new QuickListModel(this))
3536{
3637 QuickListEntry nameAction;
@@ -165,6 +166,19 @@ void LauncherItem::setFocused(bool focused)
165166 }
166167}
167168
169+bool LauncherItem::alerting() const
170+{
171+ return m_alerting;
172+}
173+
174+void LauncherItem::setAlerting(bool alerting)
175+{
176+ if (m_alerting != alerting) {
177+ m_alerting = alerting;
178+ Q_EMIT alertingChanged(alerting);
179+ }
180+}
181+
168182unity::shell::launcher::QuickListModelInterface *LauncherItem::quickList() const
169183{
170184 return m_quickList;
plugins/Greeter/Unity/Launcher/launcheritem.h+4
@@ -41,6 +41,7 @@ public:
4141 int count() const override;
4242 bool countVisible() const override;
4343 bool focused() const override;
44+ bool alerting() const override;
4445
4546 unity::shell::launcher::QuickListModelInterface *quickList() const override;
4647
@@ -54,6 +55,8 @@ private:
5455 void setCount(int count);
5556 void setCountVisible(bool countVisible);
5657 void setFocused(bool focused);
58+ void setAlerting(bool alerting);
59+
5760
5861private:
5962 QString m_appId;
@@ -66,6 +69,7 @@ private:
6669 int m_count;
6770 bool m_countVisible;
6871 bool m_focused;
72+ bool m_alerting;
6973 QuickListModel *m_quickList;
7074
7175 friend class LauncherModel;
plugins/Greeter/Unity/Launcher/launchermodelas.cpp+28−12
@@ -67,11 +67,20 @@ QVariant LauncherModel::data(const QModelIndex &index, int role) const
6767 return item->progress();
6868 case RoleFocused:
6969 return item->focused();
70+ case RoleRunning:
71+ return item->running();
7072 }
7173
7274 return QVariant();
7375}
7476
77+void LauncherModel::setAlerting(const QString &appId, bool alerting)
78+{
79+ Q_UNUSED(appId)
80+ Q_UNUSED(alerting)
81+ qWarning() << "This is a read only implementation. Cannot set alert-state of items.";
82+}
83+
7584unity::shell::launcher::LauncherItemInterface *LauncherModel::get(int index) const
7685{
7786 if (index < 0 || index >= m_list.count()) {
@@ -200,16 +209,20 @@ void LauncherModel::refresh()
200209 Q_FOREACH (LauncherItem* item, m_list) {
201210 bool found = false;
202211 Q_FOREACH(const QVariant &asItem, items) {
203− if (asItem.toMap().value("id").toString() == item->appId()) {
212+ QVariantMap cachedMap = asItem.toMap();
213+ if (cachedMap.value("id").toString() == item->appId()) {
204214 // Only keep and update it if we either show unpinned or it is pinned
205− if (!m_onlyPinned || asItem.toMap().value("pinned").toBool()) {
215+ if (!m_onlyPinned || cachedMap.value("pinned").toBool()) {
206216 found = true;
207− item->setName(asItem.toMap().value("name").toString());
208− item->setIcon(asItem.toMap().value("icon").toString());
209− item->setCount(asItem.toMap().value("count").toInt());
210− item->setCountVisible(asItem.toMap().value("countVisible").toBool());
217+ item->setName(cachedMap.value("name").toString());
218+ item->setIcon(cachedMap.value("icon").toString());
219+ item->setCount(cachedMap.value("count").toInt());
220+ item->setCountVisible(cachedMap.value("countVisible").toBool());
221+ item->setProgress(cachedMap.value("progress").toInt());
222+ item->setRunning(cachedMap.value("running").toBool());
223+
211224 int idx = m_list.indexOf(item);
212− Q_EMIT dataChanged(index(idx), index(idx), QVector<int>() << RoleName << RoleIcon);
225+ Q_EMIT dataChanged(index(idx), index(idx), {RoleName, RoleIcon, RoleCount, RoleCountVisible, RoleRunning, RoleProgress});
213226 }
214227 break;
215228 }
@@ -247,14 +260,17 @@ void LauncherModel::refresh()
247260 }
248261
249262 if (itemIndex == -1) {
263+ QVariantMap cachedMap = entry.toMap();
250264 // Need to add it. Just add it into the addedIndex to keep same ordering as the list in AS.
251− LauncherItem *item = new LauncherItem(entry.toMap().value("id").toString(),
252− entry.toMap().value("name").toString(),
253− entry.toMap().value("icon").toString(),
265+ LauncherItem *item = new LauncherItem(cachedMap.value("id").toString(),
266+ cachedMap.value("name").toString(),
267+ cachedMap.value("icon").toString(),
254268 this);
255269 item->setPinned(true);
256− item->setCount(entry.toMap().value("count").toInt());
257− item->setCountVisible(entry.toMap().value("countVisible").toBool());
270+ item->setCount(cachedMap.value("count").toInt());
271+ item->setCountVisible(cachedMap.value("countVisible").toBool());
272+ item->setProgress(cachedMap.value("progress").toInt());
273+ item->setRunning(cachedMap.value("running").toBool());
258274 beginInsertRows(QModelIndex(), newPosition, newPosition);
259275 m_list.insert(newPosition, item);
260276 endInsertRows();
plugins/Greeter/Unity/Launcher/launchermodelas.h+1
@@ -41,6 +41,7 @@ public:
4141
4242 QVariant data(const QModelIndex &index, int role) const override;
4343
44+ Q_INVOKABLE void setAlerting(const QString &appId, bool alerting) override;
4445 Q_INVOKABLE unity::shell::launcher::LauncherItemInterface* get(int index) const override;
4546 Q_INVOKABLE void move(int oldIndex, int newIndex) override;
4647 Q_INVOKABLE void pin(const QString &appId, int index = -1) override;
plugins/ScreenGrabber/screengrabber.cpp+31−20
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2014 Canonical, Ltd.
2+ * Copyright (C) 2014-2015 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -12,8 +12,6 @@
1212 *
1313 * You should have received a copy of the GNU General Public License
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− *
16− * Authors: Alberto Aguirre <alberto.aguirre@canonical.com>
1715 */
1816
1917#include "screengrabber.h"
@@ -28,27 +26,35 @@
2826
2927#include <QDebug>
3028
31−void saveScreenshot(QImage screenshot, QString filename, QString format, int quality)
29+bool saveScreenshot(const QImage &screenshot, const QString &filename, const QString &format, int quality)
3230{
33− if (!screenshot.save(filename, format.toLatin1().data(), quality))
34− qWarning() << "ScreenShotter: failed to save snapshot!";
31+ if (!screenshot.save(filename, format.toLatin1().data(), quality)) {
32+ qWarning() << "ScreenGrabber: failed to save snapshot!";
33+ return false;
34+ }
35+
36+ return true;
3537}
3638
3739ScreenGrabber::ScreenGrabber(QObject *parent)
38− : QObject(parent),
39− screenshotQuality(0)
40+ : QObject(parent)
4041{
41− QDir screenshotsDir(QStandardPaths::displayName(QStandardPaths::PicturesLocation));
42− screenshotsDir.mkdir("Screenshots");
42+ QDir screenshotsDir;
43+ if (qEnvironmentVariableIsSet("UNITY_TESTING")) {
44+ qDebug() << "Using test environment";
45+ QStandardPaths::setTestModeEnabled(true);
46+ screenshotsDir = QStandardPaths::writableLocation(QStandardPaths::DataLocation);
47+ } else {
48+ qDebug() << "Using real environment";
49+ screenshotsDir = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
50+ }
51+ screenshotsDir.mkpath("Screenshots");
4352 screenshotsDir.cd("Screenshots");
44− if (screenshotsDir.exists())
45− {
53+ if (screenshotsDir.exists()) {
4654 fileNamePrefix = screenshotsDir.absolutePath();
4755 fileNamePrefix.append("/screenshot");
48− }
49− else
50− {
51− qWarning() << "ScreenShotter: failed to create directory at: " << screenshotsDir.absolutePath();
56+ } else {
57+ qWarning() << "ScreenGrabber: failed to create directory at: " << screenshotsDir.absolutePath();
5258 }
5359}
5460
@@ -74,11 +80,16 @@ void ScreenGrabber::captureAndSave()
7480 return;
7581 }
7682
77− QImage screenshot = main_window->grabWindow();
78− QtConcurrent::run(saveScreenshot, screenshot, makeFileName(), getFormat(), screenshotQuality);
83+ const QImage screenshot = main_window->grabWindow();
84+ const QString filename = makeFileName();
85+ qDebug() << "Saving screenshot to" << filename;
86+ auto saveOp = QtConcurrent::run(saveScreenshot, screenshot, filename, getFormat(), screenshotQuality);
87+ if (saveOp.result()) {
88+ Q_EMIT screenshotSaved(filename);
89+ }
7990}
8091
81−QString ScreenGrabber::makeFileName()
92+QString ScreenGrabber::makeFileName() const
8293{
8394 QString fileName(fileNamePrefix);
8495 fileName.append(QDateTime::currentDateTime().toString("yyyyMMdd_hhmmsszzz"));
@@ -87,7 +98,7 @@ QString ScreenGrabber::makeFileName()
8798 return fileName;
8899}
89100
90−QString ScreenGrabber::getFormat()
101+QString ScreenGrabber::getFormat() const
91102{
92103 //TODO: This should be configurable (perhaps through gsettings?)
93104 return "png";
plugins/ScreenGrabber/screengrabber.h+8−6
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2014 Canonical, Ltd.
2+ * Copyright (C) 2014-2015 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -12,8 +12,6 @@
1212 *
1313 * You should have received a copy of the GNU General Public License
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− *
16− * Authors: Alberto Aguirre <alberto.aguirre@canonical.com>
1715 */
1816
1917#ifndef SNAPSHOTTER_H
@@ -28,15 +26,19 @@ class ScreenGrabber: public QObject
2826
2927public:
3028 explicit ScreenGrabber(QObject *parent = 0);
29+ ~ScreenGrabber() = default;
3130
3231public Q_SLOTS:
3332 void captureAndSave();
3433
34+Q_SIGNALS:
35+ void screenshotSaved(const QString &filename);
36+
3537private:
36− QString makeFileName();
37− QString getFormat();
38+ QString makeFileName() const;
39+ QString getFormat() const;
3840 QString fileNamePrefix;
39− int screenshotQuality;
41+ int screenshotQuality = -1; // default quality for the format
4042};
4143
4244#endif
plugins/Unity/Launcher/CMakeLists.txt+1−2
@@ -1,5 +1,4 @@
1−pkg_check_modules(LAUNCHER_API REQUIRED unity-shell-launcher=6)
2−pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=6)
1+pkg_check_modules(LAUNCHER_API REQUIRED unity-shell-launcher=7)
32pkg_check_modules(GSETTINGS_QT REQUIRED gsettings-qt)
43
54add_definitions(-DSM_BUSNAME=systemBus)
plugins/Unity/Launcher/asadapter.cpp+4−2
@@ -34,12 +34,12 @@ ASAdapter::~ASAdapter()
3434 m_accounts->deleteLater();
3535}
3636
37−void ASAdapter::syncItems(QList<LauncherItem *> m_list)
37+void ASAdapter::syncItems(const QList<LauncherItem*> &list)
3838{
3939 if (m_accounts && !m_user.isEmpty()) {
4040 QList<QVariantMap> items;
4141
42− Q_FOREACH(LauncherItem *item, m_list) {
42+ Q_FOREACH(LauncherItem *item, list) {
4343 items << itemToVariant(item);
4444 }
4545
@@ -56,5 +56,7 @@ QVariantMap ASAdapter::itemToVariant(LauncherItem *item) const
5656 details.insert("count", item->count());
5757 details.insert("countVisible", item->countVisible());
5858 details.insert("pinned", item->pinned());
59+ details.insert("running", item->running());
60+ details.insert("progress", item->progress());
5961 return details;
6062}
plugins/Unity/Launcher/asadapter.h+1−4
@@ -21,7 +21,6 @@
2121
2222class LauncherItem;
2323class AccountsServiceDBusAdaptor;
24−class QDBusInterface;
2524
2625class ASAdapter
2726{
@@ -29,7 +28,7 @@ public:
2928 ASAdapter();
3029 ~ASAdapter();
3130
32− void syncItems(QList<LauncherItem*> m_list);
31+ void syncItems(const QList<LauncherItem*> &list);
3332
3433private:
3534 QVariantMap itemToVariant(LauncherItem *item) const;
@@ -38,8 +37,6 @@ private:
3837 AccountsServiceDBusAdaptor *m_accounts;
3938 QString m_user;
4039
41− QDBusInterface *m_userInterface;
42−
4340 friend class LauncherModelTest;
4441};
4542
plugins/Unity/Launcher/dbusinterface.cpp+42−12
@@ -62,6 +62,8 @@ QString DBusInterface::introspect(const QString &path) const
6262 "<interface name=\"com.canonical.Unity.Launcher.Item\">"
6363 "<property name=\"count\" type=\"i\" access=\"readwrite\" />"
6464 "<property name=\"countVisible\" type=\"b\" access=\"readwrite\" />"
65+ "<property name=\"progress\" type=\"i\" access=\"readwrite\" />"
66+ "<method name=\"Alert\" />"
6567 "</interface>";
6668 return nodeiface;
6769}
@@ -122,6 +124,20 @@ bool DBusInterface::handleMessage(const QDBusMessage& message, const QDBusConnec
122124 return false;
123125 }
124126
127+ /* Break down the path to just the app id */
128+ bool validpath = true;
129+ QString pathtemp = message.path();
130+ if (!pathtemp.startsWith("/com/canonical/Unity/Launcher/")) {
131+ validpath = false;
132+ }
133+ pathtemp.remove("/com/canonical/Unity/Launcher/");
134+ if (pathtemp.indexOf('/') >= 0) {
135+ validpath = false;
136+ }
137+
138+ /* Find ourselves an appid */
139+ QString appid = decodeAppId(pathtemp);
140+
125141 // First handle methods of the Launcher interface
126142 if (message.interface() == "com.canonical.Unity.Launcher") {
127143 if (message.member() == "Refresh") {
@@ -129,6 +145,13 @@ bool DBusInterface::handleMessage(const QDBusMessage& message, const QDBusConnec
129145 Q_EMIT refreshCalled();
130146 return connection.send(reply);
131147 }
148+ } else if (message.interface() == "com.canonical.Unity.Launcher.Item") {
149+ // Handle methods of the Launcher-Item interface
150+ if (message.member() == "Alert" && validpath) {
151+ QDBusMessage reply = message.createReply();
152+ Q_EMIT alertCalled(appid);
153+ return connection.send(reply);
154+ }
132155 }
133156
134157 // Now handle dynamic properties (for launcher emblems)
@@ -136,54 +159,61 @@ bool DBusInterface::handleMessage(const QDBusMessage& message, const QDBusConnec
136159 return false;
137160 }
138161
139− if (message.member() != "GetAll" && message.arguments()[0].toString() != "com.canonical.Unity.Launcher.Item") {
162+ if (message.member() == "Get" && (message.arguments().count() != 2 || message.arguments()[0].toString() != "com.canonical.Unity.Launcher.Item")) {
140163 return false;
141164 }
142165
143− /* Break down the path to just the app id */
144− QString pathtemp = message.path();
145− if (!pathtemp.startsWith("/com/canonical/Unity/Launcher/")) {
166+ if (message.member() == "Set" && (message.arguments().count() != 3 || message.arguments()[0].toString() != "com.canonical.Unity.Launcher.Item")) {
146167 return false;
147168 }
148− pathtemp.remove("/com/canonical/Unity/Launcher/");
149− if (pathtemp.indexOf('/') >= 0) {
169+
170+ if (!validpath) {
150171 return false;
151172 }
152173
153− /* Find ourselves an appid */
154− QString appid = decodeAppId(pathtemp);
155174 int index = m_launcherModel->findApplication(appid);
156175 LauncherItem *item = static_cast<LauncherItem*>(m_launcherModel->get(index));
157176
158177 QVariantList retval;
159178 if (message.member() == "Get") {
179+ QString cachedString = message.arguments()[1].toString();
160180 if (!item) {
161181 return false;
162182 }
163− if (message.arguments()[1].toString() == "count") {
183+ if (cachedString == "count") {
164184 retval.append(QVariant::fromValue(QDBusVariant(item->count())));
165− } else if (message.arguments()[1].toString() == "countVisible") {
185+ } else if (cachedString == "countVisible") {
166186 retval.append(QVariant::fromValue(QDBusVariant(item->countVisible())));
187+ } else if (cachedString == "progress") {
188+ retval.append(QVariant::fromValue(QDBusVariant(item->progress())));
167189 }
168190 } else if (message.member() == "Set") {
169− if (message.arguments()[1].toString() == "count") {
191+ QString cachedString = message.arguments()[1].toString();
192+ if (cachedString == "count") {
170193 int newCount = message.arguments()[2].value<QDBusVariant>().variant().toInt();
171194 if (!item || newCount != item->count()) {
172195 Q_EMIT countChanged(appid, newCount);
173196 notifyPropertyChanged("com.canonical.Unity.Launcher.Item", encodeAppId(appid), "count", QVariant(newCount));
174197 }
175− } else if (message.arguments()[1].toString() == "countVisible") {
198+ } else if (cachedString == "countVisible") {
176199 bool newVisible = message.arguments()[2].value<QDBusVariant>().variant().toBool();
177200 if (!item || newVisible != item->countVisible()) {
178201 Q_EMIT countVisibleChanged(appid, newVisible);
179202 notifyPropertyChanged("com.canonical.Unity.Launcher.Item", encodeAppId(appid), "countVisible", newVisible);
180203 }
204+ } else if (cachedString == "progress") {
205+ int newProgress = message.arguments()[2].value<QDBusVariant>().variant().toInt();
206+ if (!item || newProgress != item->progress()) {
207+ Q_EMIT progressChanged(appid, newProgress);
208+ notifyPropertyChanged("com.canonical.Unity.Launcher.Item", encodeAppId(appid), "progress", QVariant(newProgress));
209+ }
181210 }
182211 } else if (message.member() == "GetAll") {
183212 if (item) {
184213 QVariantMap all;
185214 all.insert("count", item->count());
186215 all.insert("countVisible", item->countVisible());
216+ all.insert("progress", item->progress());
187217 retval.append(all);
188218 }
189219 } else {
plugins/Unity/Launcher/dbusinterface.h+2
@@ -36,7 +36,9 @@ public:
3636Q_SIGNALS:
3737 void countChanged(const QString &appId, int count);
3838 void countVisibleChanged(const QString &appId, bool countVisible);
39+ void progressChanged(const QString &appId, int progress);
3940 void refreshCalled();
41+ void alertCalled(const QString &appId);
4042
4143private:
4244 static QString decodeAppId(const QString& path);
plugins/Unity/Launcher/gsettings.cpp+1−1
@@ -68,7 +68,7 @@ void GSettings::setStoredApplications(const QStringList &storedApplications)
6868void GSettings::onSettingsChanged(const QString &key)
6969{
7070 if (key == "items") {
71− QStringList cachedItems = m_gSettings->get("items").toStringList();
71+ const QStringList cachedItems = m_gSettings->get("items").toStringList();
7272 if (m_cachedItems != cachedItems) {
7373 m_cachedItems = cachedItems;
7474 Q_EMIT changed();
plugins/Unity/Launcher/launcheritem.cpp+33
@@ -34,16 +34,22 @@ LauncherItem::LauncherItem(const QString &appId, const QString &name, const QStr
3434 m_count(0),
3535 m_countVisible(false),
3636 m_focused(false),
37+ m_alerting(false),
3738 m_quickList(new QuickListModel(this))
3839{
3940 QuickListEntry nameAction;
4041 nameAction.setActionId("launch_item");
4142 nameAction.setText(m_name);
4243 m_quickList->appendAction(nameAction);
44+
4345 QuickListEntry pinningAction;
4446 pinningAction.setActionId("pin_item");
4547 pinningAction.setText(gettext("Pin shortcut"));
4648 m_quickList->appendAction(pinningAction);
49+
50+ m_quitAction.setActionId("stop_item");
51+ m_quitAction.setIcon("application-exit");
52+ m_quitAction.setText(gettext("Quit"));
4753}
4854
4955QString LauncherItem::appId() const
@@ -110,6 +116,11 @@ void LauncherItem::setRunning(bool running)
110116{
111117 if (m_running != running) {
112118 m_running = running;
119+ if (m_running) { // add the quit action
120+ m_quickList->appendAction(m_quitAction);
121+ } else { // remove the quit action
122+ m_quickList->removeAction(m_quitAction);
123+ }
113124 Q_EMIT runningChanged(running);
114125 }
115126}
@@ -150,6 +161,9 @@ void LauncherItem::setCount(int count)
150161 if (m_count != count) {
151162 m_count = count;
152163 Q_EMIT countChanged(count);
164+ if (m_countVisible) {
165+ setAlerting(true);
166+ }
153167 }
154168}
155169
@@ -163,6 +177,9 @@ void LauncherItem::setCountVisible(bool countVisible)
163177 if (m_countVisible != countVisible) {
164178 m_countVisible = countVisible;
165179 Q_EMIT countVisibleChanged(countVisible);
180+ if (countVisible) {
181+ setAlerting(true);
182+ }
166183 }
167184}
168185
@@ -175,10 +192,26 @@ void LauncherItem::setFocused(bool focused)
175192{
176193 if (m_focused != focused) {
177194 m_focused = focused;
195+ if (focused) {
196+ setAlerting(false);
197+ }
178198 Q_EMIT focusedChanged(focused);
179199 }
180200}
181201
202+bool LauncherItem::alerting() const
203+{
204+ return m_alerting;
205+}
206+
207+void LauncherItem::setAlerting(bool alerting)
208+{
209+ if (m_alerting != alerting) {
210+ m_alerting = alerting;
211+ Q_EMIT alertingChanged(alerting);
212+ }
213+}
214+
182215unity::shell::launcher::QuickListModelInterface *LauncherItem::quickList() const
183216{
184217 return m_quickList;
plugins/Unity/Launcher/launcheritem.h+4
@@ -44,6 +44,7 @@ public:
4444 int count() const override;
4545 bool countVisible() const override;
4646 bool focused() const override;
47+ bool alerting() const override;
4748
4849 unity::shell::launcher::QuickListModelInterface *quickList() const override;
4950
@@ -57,6 +58,7 @@ private:
5758 void setCount(int count);
5859 void setCountVisible(bool countVisible);
5960 void setFocused(bool focused);
61+ void setAlerting(bool alerting);
6062
6163private:
6264 QString m_appId;
@@ -69,7 +71,9 @@ private:
6971 int m_count;
7072 bool m_countVisible;
7173 bool m_focused;
74+ bool m_alerting;
7275 QuickListModel *m_quickList;
76+ QuickListEntry m_quitAction;
7377
7478 friend class LauncherModel;
7579};
plugins/Unity/Launcher/launchermodel.cpp+78−26
@@ -36,11 +36,13 @@ LauncherModel::LauncherModel(QObject *parent):
3636 m_settings(new GSettings(this)),
3737 m_dbusIface(new DBusInterface(this)),
3838 m_asAdapter(new ASAdapter()),
39− m_appManager(0)
39+ m_appManager(nullptr)
4040{
4141 connect(m_dbusIface, &DBusInterface::countChanged, this, &LauncherModel::countChanged);
4242 connect(m_dbusIface, &DBusInterface::countVisibleChanged, this, &LauncherModel::countVisibleChanged);
43+ connect(m_dbusIface, &DBusInterface::progressChanged, this, &LauncherModel::progressChanged);
4344 connect(m_dbusIface, &DBusInterface::refreshCalled, this, &LauncherModel::refresh);
45+ connect(m_dbusIface, &DBusInterface::alertCalled, this, &LauncherModel::alert);
4446
4547 connect(m_settings, &GSettings::changed, this, &LauncherModel::refresh);
4648
@@ -82,11 +84,30 @@ QVariant LauncherModel::data(const QModelIndex &index, int role) const
8284 return item->progress();
8385 case RoleFocused:
8486 return item->focused();
87+ case RoleAlerting:
88+ return item->alerting();
89+ case RoleRunning:
90+ return item->running();
91+ default:
92+ qWarning() << Q_FUNC_INFO << "missing role, implement me";
93+ return QVariant();
8594 }
8695
8796 return QVariant();
8897}
8998
99+void LauncherModel::setAlerting(const QString &appId, bool alerting) {
100+ int index = findApplication(appId);
101+ if (index >= 0) {
102+ QModelIndex modelIndex = this->index(index);
103+ LauncherItem *item = m_list.at(index);
104+ if (!item->focused()) {
105+ item->setAlerting(alerting);
106+ Q_EMIT dataChanged(modelIndex, modelIndex, QVector<int>() << RoleAlerting);
107+ }
108+ }
109+}
110+
90111unity::shell::launcher::LauncherItemInterface *LauncherModel::get(int index) const
91112{
92113 if (index < 0 || index >= m_list.count()) {
@@ -134,7 +155,7 @@ void LauncherModel::pin(const QString &appId, int index)
134155 if (index == -1 || index == currentIndex) {
135156 m_list.at(currentIndex)->setPinned(true);
136157 QModelIndex modelIndex = this->index(currentIndex);
137− Q_EMIT dataChanged(modelIndex, modelIndex, QVector<int>() << RolePinned);
158+ Q_EMIT dataChanged(modelIndex, modelIndex, {RolePinned});
138159 } else {
139160 move(currentIndex, index);
140161 // move() will store the list to the backend itself, so just exit at this point.
@@ -147,7 +168,7 @@ void LauncherModel::pin(const QString &appId, int index)
147168
148169 DesktopFileHandler desktopFile(appId);
149170 if (!desktopFile.isValid()) {
150− qWarning() << "Can't pin this application, there is no .destkop file available.";
171+ qWarning() << "Can't pin this application, there is no .desktop file available.";
151172 return;
152173 }
153174
@@ -172,7 +193,7 @@ void LauncherModel::requestRemove(const QString &appId)
172193
173194void LauncherModel::quickListActionInvoked(const QString &appId, int actionIndex)
174195{
175− int index = findApplication(appId);
196+ const int index = findApplication(appId);
176197 if (index < 0) {
177198 return;
178199 }
@@ -180,7 +201,7 @@ void LauncherModel::quickListActionInvoked(const QString &appId, int actionIndex
180201 LauncherItem *item = m_list.at(index);
181202 QuickListModel *model = qobject_cast<QuickListModel*>(item->quickList());
182203 if (model) {
183− QString actionId = model->get(actionIndex).actionId();
204+ const QString actionId = model->get(actionIndex).actionId();
184205
185206 // Check if this is one of the launcher actions we handle ourselves
186207 if (actionId == "pin_item") {
@@ -191,7 +212,10 @@ void LauncherModel::quickListActionInvoked(const QString &appId, int actionIndex
191212 }
192213 } else if (actionId == "launch_item") {
193214 QDesktopServices::openUrl(getUrlForAppId(appId));
194−
215+ } else if (actionId == "stop_item") { // Quit
216+ if (m_appManager) {
217+ m_appManager->stopApplication(appId);
218+ }
195219 // Nope, we don't know this action, let the backend forward it to the application
196220 } else {
197221 // TODO: forward quicklist action to app, possibly via m_dbusIface
@@ -289,7 +313,7 @@ void LauncherModel::storeAppList()
289313
290314void LauncherModel::unpin(const QString &appId)
291315{
292− int index = findApplication(appId);
316+ const int index = findApplication(appId);
293317 if (index < 0) {
294318 return;
295319 }
@@ -298,7 +322,7 @@ void LauncherModel::unpin(const QString &appId)
298322 if (m_list.at(index)->pinned()) {
299323 m_list.at(index)->setPinned(false);
300324 QModelIndex modelIndex = this->index(index);
301− Q_EMIT dataChanged(modelIndex, modelIndex, QVector<int>() << RolePinned);
325+ Q_EMIT dataChanged(modelIndex, modelIndex, {RolePinned});
302326 }
303327 } else {
304328 beginRemoveRows(QModelIndex(), index, index);
@@ -320,32 +344,38 @@ int LauncherModel::findApplication(const QString &appId)
320344
321345void LauncherModel::progressChanged(const QString &appId, int progress)
322346{
323− int idx = findApplication(appId);
347+ const int idx = findApplication(appId);
324348 if (idx >= 0) {
325349 LauncherItem *item = m_list.at(idx);
326350 item->setProgress(progress);
327− Q_EMIT dataChanged(index(idx), index(idx), QVector<int>() << RoleProgress);
351+ Q_EMIT dataChanged(index(idx), index(idx), {RoleProgress});
328352 }
329353}
330354
331355void LauncherModel::countChanged(const QString &appId, int count)
332356{
333− int idx = findApplication(appId);
357+ const int idx = findApplication(appId);
334358 if (idx >= 0) {
335359 LauncherItem *item = m_list.at(idx);
336360 item->setCount(count);
337− Q_EMIT dataChanged(index(idx), index(idx), QVector<int>() << RoleCount);
361+ if (item->countVisible()) {
362+ setAlerting(item->appId(), true);
363+ }
338364 m_asAdapter->syncItems(m_list);
365+ Q_EMIT dataChanged(index(idx), index(idx), {RoleCount});
339366 }
340367}
341368
342−void LauncherModel::countVisibleChanged(const QString &appId, int countVisible)
369+void LauncherModel::countVisibleChanged(const QString &appId, bool countVisible)
343370{
344371 int idx = findApplication(appId);
345372 if (idx >= 0) {
346373 LauncherItem *item = m_list.at(idx);
347374 item->setCountVisible(countVisible);
348− Q_EMIT dataChanged(index(idx), index(idx), QVector<int>() << RoleCountVisible);
375+ if (countVisible) {
376+ setAlerting(item->appId(), true);
377+ }
378+ Q_EMIT dataChanged(index(idx), index(idx), {RoleCountVisible});
349379
350380 // If countVisible goes to false, and the item is neither pinned nor recent we can drop it
351381 if (!countVisible && !item->pinned() && !item->recent()) {
@@ -364,7 +394,6 @@ void LauncherModel::countVisibleChanged(const QString &appId, int countVisible)
364394 beginInsertRows(QModelIndex(), m_list.count(), m_list.count());
365395 m_list.append(item);
366396 endInsertRows();
367− Q_EMIT hint();
368397 }
369398 }
370399 m_asAdapter->syncItems(m_list);
@@ -387,7 +416,8 @@ void LauncherModel::refresh()
387416 item->setName(desktopFile.displayName());
388417 item->setIcon(desktopFile.icon());
389418 item->setPinned(item->pinned()); // update pinned text if needed
390− Q_EMIT dataChanged(index(idx), index(idx), QVector<int>() << RoleName << RoleIcon);
419+ item->setRunning(item->running());
420+ Q_EMIT dataChanged(index(idx), index(idx), {RoleName, RoleIcon, RoleRunning});
391421 }
392422 }
393423
@@ -405,7 +435,7 @@ void LauncherModel::refresh()
405435
406436 // Now walk through settings and see if we need to add something
407437 for (int settingsIndex = 0; settingsIndex < m_settings->storedApplications().count(); ++settingsIndex) {
408− QString entry = m_settings->storedApplications().at(settingsIndex);
438+ const QString entry = m_settings->storedApplications().at(settingsIndex);
409439 int itemIndex = -1;
410440 for (int i = 0; i < m_list.count(); ++i) {
411441 if (m_list.at(i)->appId() == entry) {
@@ -453,6 +483,16 @@ void LauncherModel::refresh()
453483 m_asAdapter->syncItems(m_list);
454484}
455485
486+void LauncherModel::alert(const QString &appId)
487+{
488+ int idx = findApplication(appId);
489+ if (idx >= 0) {
490+ LauncherItem *item = m_list.at(idx);
491+ setAlerting(item->appId(), true);
492+ Q_EMIT dataChanged(index(idx), index(idx), QVector<int>() << RoleAlerting);
493+ }
494+}
495+
456496void LauncherModel::applicationAdded(const QModelIndex &parent, int row)
457497{
458498 Q_UNUSED(parent);
@@ -468,25 +508,27 @@ void LauncherModel::applicationAdded(const QModelIndex &parent, int row)
468508 return;
469509 }
470510
471− int itemIndex = findApplication(app->appId());
511+ const int itemIndex = findApplication(app->appId());
472512 if (itemIndex != -1) {
473513 LauncherItem *item = m_list.at(itemIndex);
474514 if (!item->recent()) {
475515 item->setRecent(true);
476− m_asAdapter->syncItems(m_list);
477− Q_EMIT dataChanged(index(itemIndex), index(itemIndex), QVector<int>() << RoleRecent);
516+ Q_EMIT dataChanged(index(itemIndex), index(itemIndex), {RoleRecent});
478517 }
479− // Shall we paint some running/recent app highlight? If yes, do it here.
518+ item->setRunning(true);
519+ // TODO Shall we paint some running/recent app highlight? If yes, do it here.
480520 } else {
481521 LauncherItem *item = new LauncherItem(app->appId(), app->name(), app->icon().toString(), this);
482522 item->setRecent(true);
523+ item->setRunning(true);
483524 item->setFocused(app->focused());
484525
485526 beginInsertRows(QModelIndex(), m_list.count(), m_list.count());
486527 m_list.append(item);
487528 endInsertRows();
488− m_asAdapter->syncItems(m_list);
489529 }
530+ m_asAdapter->syncItems(m_list);
531+ Q_EMIT dataChanged(index(itemIndex), index(itemIndex), {RoleRunning});
490532}
491533
492534void LauncherModel::applicationRemoved(const QModelIndex &parent, int row)
@@ -501,25 +543,35 @@ void LauncherModel::applicationRemoved(const QModelIndex &parent, int row)
501543 }
502544 }
503545
504− if (appIndex > -1 && !m_list.at(appIndex)->pinned()) {
546+ if (appIndex < 0) {
547+ qWarning() << Q_FUNC_INFO << "appIndex not found";
548+ return;
549+ }
550+
551+ LauncherItem * item = m_list.at(appIndex);
552+ item->setRunning(false);
553+
554+ if (!item->pinned()) {
505555 beginRemoveRows(QModelIndex(), appIndex, appIndex);
506556 m_list.takeAt(appIndex)->deleteLater();
507557 endRemoveRows();
508558 m_asAdapter->syncItems(m_list);
559+ Q_EMIT dataChanged(index(appIndex), index(appIndex), {RolePinned});
509560 }
561+ Q_EMIT dataChanged(index(appIndex), index(appIndex), {RoleRunning});
510562}
511563
512564void LauncherModel::focusedAppIdChanged()
513565{
514− QString appId = m_appManager->focusedApplicationId();
566+ const QString appId = m_appManager->focusedApplicationId();
515567 for (int i = 0; i < m_list.count(); ++i) {
516568 LauncherItem *item = m_list.at(i);
517569 if (!item->focused() && item->appId() == appId) {
518570 item->setFocused(true);
519− Q_EMIT dataChanged(index(i), index(i), QVector<int>() << RoleFocused);
571+ Q_EMIT dataChanged(index(i), index(i), {RoleFocused});
520572 } else if (item->focused() && item->appId() != appId) {
521573 item->setFocused(false);
522− Q_EMIT dataChanged(index(i), index(i), QVector<int>() << RoleFocused);
574+ Q_EMIT dataChanged(index(i), index(i), {RoleFocused});
523575 }
524576 }
525577}
plugins/Unity/Launcher/launchermodel.h+3−1
@@ -45,6 +45,7 @@ public:
4545
4646 QVariant data(const QModelIndex &index, int role) const override;
4747
48+ Q_INVOKABLE void setAlerting(const QString &appId, bool alerting) override;
4849 Q_INVOKABLE unity::shell::launcher::LauncherItemInterface* get(int index) const override;
4950 Q_INVOKABLE void move(int oldIndex, int newIndex) override;
5051 Q_INVOKABLE void pin(const QString &appId, int index = -1) override;
@@ -63,6 +64,7 @@ public:
6364public Q_SLOTS:
6465 void requestRemove(const QString &appId) override;
6566 Q_INVOKABLE void refresh();
67+ Q_INVOKABLE void alert(const QString &appId);
6668
6769private:
6870 void storeAppList();
@@ -71,7 +73,7 @@ private:
7173
7274private Q_SLOTS:
7375 void countChanged(const QString &appId, int count);
74− void countVisibleChanged(const QString &appId, int count);
76+ void countVisibleChanged(const QString &appId, bool count);
7577 void progressChanged(const QString &appId, int progress);
7678
7779 void applicationAdded(const QModelIndex &parent, int row);
plugins/Unity/Launcher/quicklistentry.cpp+6−2
@@ -1,4 +1,4 @@
1−/* Copyright (C) 2013 Canonical, Ltd.
1+/* Copyright (C) 2013, 2015 Canonical, Ltd.
22 *
33 * Authors:
44 * Michael Zanetti <michael.zanetti@canonical.com>
@@ -20,7 +20,6 @@
2020
2121QuickListEntry::QuickListEntry()
2222{
23−
2423}
2524
2625QString QuickListEntry::actionId() const
@@ -57,3 +56,8 @@ bool QuickListEntry::clickable() const
5756{
5857 return !m_actionId.isEmpty();
5958}
59+
60+bool QuickListEntry::operator==(const QuickListEntry &other)
61+{
62+ return !other.actionId().isEmpty() && other.actionId() == m_actionId;
63+}
plugins/Unity/Launcher/quicklistentry.h+3−1
@@ -1,4 +1,4 @@
1−/* Copyright (C) 2013 Canonical, Ltd.
1+/* Copyright (C) 2013, 2015 Canonical, Ltd.
22 *
33 * Authors:
44 * Michael Zanetti <michael.zanetti@canonical.com>
@@ -37,6 +37,8 @@ public:
3737
3838 bool clickable() const;
3939
40+ bool operator==(const QuickListEntry & other);
41+
4042private:
4143 QString m_actionId;
4244 QString m_text;
plugins/Unity/Launcher/quicklistmodel.cpp+12−1
@@ -1,5 +1,5 @@
11/*
2− * Copyright 2013 Canonical Ltd.
2+ * Copyright 2013, 2015 Canonical Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU Lesser General Public License as published by
@@ -48,6 +48,17 @@ void QuickListModel::updateAction(const QuickListEntry &entry)
4848 }
4949}
5050
51+void QuickListModel::removeAction(const QuickListEntry &entry)
52+{
53+ const int start = m_list.indexOf(entry);
54+ if (start > -1) {
55+ beginRemoveRows(QModelIndex(), start, start);
56+ m_list.removeOne(entry);
57+ Q_EMIT dataChanged(index(start), index(start));
58+ endRemoveRows();
59+ }
60+}
61+
5162QuickListEntry QuickListModel::get(int index) const
5263{
5364 return m_list.at(index);
plugins/Unity/Launcher/quicklistmodel.h+3−1
@@ -1,5 +1,5 @@
11/*
2− * Copyright 2013 Canonical Ltd.
2+ * Copyright 201, 2015 Canonical Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU Lesser General Public License as published by
@@ -46,6 +46,8 @@ public:
4646 */
4747 void updateAction(const QuickListEntry &entry);
4848
49+ void removeAction(const QuickListEntry &entry);
50+
4951 QuickListEntry get(int index) const;
5052
5153 int rowCount(const QModelIndex &parent = QModelIndex()) const override;
plugins/Unity/Session/CMakeLists.txt+1
@@ -2,6 +2,7 @@ pkg_search_module(GIO REQUIRED gio-2.0)
22
33include_directories(
44 ${CMAKE_CURRENT_SOURCE_DIR}
5+ ${CMAKE_CURRENT_BINARY_DIR}
56 ${GIO_INCLUDE_DIRS}
67 ${libunity8-private_SOURCE_DIR}
78)
plugins/Unity/Session/dbusunitysessionservice.cpp+372−24
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2014 Canonical, Ltd.
2+ * Copyright (C) 2014, 2015 Canonical, Ltd.
33 *
44 * This program is free software: you can redistribute it and/or modify it under
55 * the terms of the GNU Lesser General Public License version 3, as published by
@@ -17,17 +17,186 @@
1717// local
1818#include "dbusunitysessionservice.h"
1919
20+// system
21+#include <sys/types.h>
22+#include <unistd.h>
23+#include <pwd.h>
24+
2025// Qt
26+#include <QDebug>
2127#include <QDBusPendingCall>
28+#include <QDBusReply>
29+#include <QElapsedTimer>
30+#include <QDateTime>
31+
32+#define LOGIN1_SERVICE QStringLiteral("org.freedesktop.login1")
33+#define LOGIN1_PATH QStringLiteral("/org/freedesktop/login1")
34+#define LOGIN1_IFACE QStringLiteral("org.freedesktop.login1.Manager")
35+#define LOGIN1_SESSION_IFACE QStringLiteral("org.freedesktop.login1.Session")
36+
37+#define ACTIVE_KEY QStringLiteral("Active")
38+#define IDLE_SINCE_KEY QStringLiteral("IdleSinceHint")
39+
40+class DBusUnitySessionServicePrivate: public QObject
41+{
42+ Q_OBJECT
43+public:
44+ QString logindSessionPath;
45+ bool isSessionActive = true;
46+ QElapsedTimer screensaverActiveTimer;
47+
48+ DBusUnitySessionServicePrivate(): QObject() {
49+ init();
50+ checkActive();
51+ }
52+
53+ void init()
54+ {
55+ // get our logind session path
56+ QDBusMessage msg = QDBusMessage::createMethodCall(LOGIN1_SERVICE,
57+ LOGIN1_PATH,
58+ LOGIN1_IFACE,
59+ "GetSessionByPID");
60+ msg << (quint32) getpid();
61+
62+ QDBusReply<QDBusObjectPath> reply = QDBusConnection::systemBus().asyncCall(msg);
63+ if (reply.isValid()) {
64+ logindSessionPath = reply.value().path();
65+
66+ // start watching the Active property
67+ QDBusConnection::systemBus().connect(LOGIN1_SERVICE, logindSessionPath, "org.freedesktop.DBus.Properties", "PropertiesChanged",
68+ this, SLOT(onPropertiesChanged(QString,QVariantMap,QStringList)));
69+ } else {
70+ qWarning() << "Failed to get logind session path" << reply.error().message();
71+ }
72+ }
73+
74+ bool checkLogin1Call(const QString &method) const
75+ {
76+ QDBusMessage msg = QDBusMessage::createMethodCall(LOGIN1_SERVICE, LOGIN1_PATH, LOGIN1_IFACE, method);
77+ QDBusReply<QString> reply = QDBusConnection::systemBus().asyncCall(msg);
78+ return reply.isValid() && (reply == QStringLiteral("yes") || reply == QStringLiteral("challenge"));
79+ }
80+
81+ void makeLogin1Call(const QString &method, const QVariantList &args)
82+ {
83+ QDBusMessage msg = QDBusMessage::createMethodCall(LOGIN1_SERVICE,
84+ LOGIN1_PATH,
85+ LOGIN1_IFACE,
86+ method);
87+ msg.setArguments(args);
88+ QDBusConnection::systemBus().asyncCall(msg);
89+ }
90+
91+ void checkActive()
92+ {
93+ if (logindSessionPath.isEmpty()) {
94+ qWarning() << "Invalid session path";
95+ return;
96+ }
97+
98+ QDBusMessage msg = QDBusMessage::createMethodCall(LOGIN1_SERVICE,
99+ logindSessionPath,
100+ "org.freedesktop.DBus.Properties",
101+ "Get");
102+ msg << LOGIN1_SESSION_IFACE;
103+ msg << ACTIVE_KEY;
104+
105+ QDBusReply<QVariant> reply = QDBusConnection::systemBus().asyncCall(msg);
106+ if (reply.isValid()) {
107+ isSessionActive = reply.value().toBool();
108+ qDebug() << "Session" << logindSessionPath << "is active:" << isSessionActive;
109+ } else {
110+ qWarning() << "Failed to get Active property" << reply.error().message();
111+ }
112+ }
113+
114+ quint32 screensaverActiveTime() const
115+ {
116+ if (!isSessionActive && screensaverActiveTimer.isValid()) {
117+ return screensaverActiveTimer.elapsed() / 1000;
118+ }
119+
120+ return 0;
121+ }
122+
123+ quint64 idleSinceUSecTimestamp() const
124+ {
125+ QDBusMessage msg = QDBusMessage::createMethodCall(LOGIN1_SERVICE,
126+ logindSessionPath,
127+ "org.freedesktop.DBus.Properties",
128+ "Get");
129+ msg << LOGIN1_SESSION_IFACE;
130+ msg << IDLE_SINCE_KEY;
131+
132+ QDBusReply<QVariant> reply = QDBusConnection::systemBus().asyncCall(msg);
133+ if (reply.isValid()) {
134+ return reply.value().value<quint64>();
135+ } else {
136+ qWarning() << "Failed to get IdleSinceHint property" << reply.error().message();
137+ }
138+
139+ return 0;
140+ }
141+
142+ void setIdleHint(bool idle)
143+ {
144+ QDBusMessage msg = QDBusMessage::createMethodCall(LOGIN1_SERVICE,
145+ logindSessionPath,
146+ LOGIN1_SESSION_IFACE,
147+ "SetIdleHint");
148+ msg << idle;
149+ QDBusConnection::systemBus().asyncCall(msg);
150+ }
151+
152+private Q_SLOTS:
153+ void onPropertiesChanged(const QString &iface, const QVariantMap &changedProps, const QStringList &invalidatedProps)
154+ {
155+ Q_UNUSED(iface)
156+
157+ if (changedProps.contains(ACTIVE_KEY) || invalidatedProps.contains(ACTIVE_KEY)) {
158+ if (changedProps.value(ACTIVE_KEY).isValid()) {
159+ isSessionActive = changedProps.value(ACTIVE_KEY).toBool();
160+ } else {
161+ checkActive();
162+ }
163+
164+ Q_EMIT screensaverActiveChanged(!isSessionActive);
165+
166+ if (isSessionActive) {
167+ screensaverActiveTimer.invalidate();
168+ setIdleHint(false);
169+ } else {
170+ screensaverActiveTimer.start();
171+ setIdleHint(true);
172+ }
173+ }
174+ }
175+
176+Q_SIGNALS:
177+ void screensaverActiveChanged(bool active);
178+};
179+
180+Q_GLOBAL_STATIC(DBusUnitySessionServicePrivate, d)
22181
23182DBusUnitySessionService::DBusUnitySessionService()
24183 : UnityDBusObject("/com/canonical/Unity/Session", "com.canonical.Unity")
25184{
185+ if (!d->logindSessionPath.isEmpty()) {
186+ // connect our Lock() slot to the logind's session Lock() signal
187+ QDBusConnection::systemBus().connect(LOGIN1_SERVICE, d->logindSessionPath, LOGIN1_SESSION_IFACE, "Lock", this, SLOT(Lock()));
188+ // ... and our Unlocked() signal to the logind's session Unlock() signal
189+ // (lightdm handles the unlocking by calling logind's Unlock method which in turn emits this signal we connect to)
190+ QDBusConnection::systemBus().connect(LOGIN1_SERVICE, d->logindSessionPath, LOGIN1_SESSION_IFACE, "Unlock", this, SIGNAL(Unlocked()));
191+ } else {
192+ qWarning() << "Failed to connect to logind's session Lock/Unlock signals";
193+ }
26194}
27195
28196void DBusUnitySessionService::Logout()
29197{
30198 // TODO ask the apps to quit and then emit the signal
199+ Q_EMIT LogoutReady();
31200 Q_EMIT logoutReady();
32201}
33202
@@ -40,38 +209,139 @@ void DBusUnitySessionService::EndSession()
40209 QDBusConnection::sessionBus().asyncCall(msg);
41210}
42211
212+bool DBusUnitySessionService::CanHibernate() const
213+{
214+ return d->checkLogin1Call("CanHibernate");
215+}
216+
217+bool DBusUnitySessionService::CanSuspend() const
218+{
219+ return d->checkLogin1Call("CanSuspend");
220+}
221+
222+bool DBusUnitySessionService::CanHybridSleep() const
223+{
224+ return d->checkLogin1Call("CanHybridSleep");
225+}
226+
227+bool DBusUnitySessionService::CanReboot() const
228+{
229+ return d->checkLogin1Call("CanReboot");
230+}
231+
232+bool DBusUnitySessionService::CanShutdown() const
233+{
234+ return d->checkLogin1Call("CanPowerOff");
235+}
236+
237+bool DBusUnitySessionService::CanLock() const
238+{
239+ return true; // FIXME
240+}
241+
242+QString DBusUnitySessionService::UserName() const
243+{
244+ struct passwd *p = getpwuid(geteuid());
245+ if (p) {
246+ return QString::fromUtf8(p->pw_name);
247+ }
248+
249+ return QString();
250+}
251+
252+QString DBusUnitySessionService::RealName() const
253+{
254+ struct passwd *p = getpwuid(geteuid());
255+ if (p) {
256+ const QString gecos = QString::fromLocal8Bit(p->pw_gecos);
257+ if (!gecos.isEmpty()) {
258+ return gecos.split(QLatin1Char(',')).first();
259+ }
260+ }
261+
262+ return QString();
263+}
264+
265+QString DBusUnitySessionService::HostName() const
266+{
267+ char hostName[512];
268+ if (gethostname(hostName, sizeof(hostName)) == -1) {
269+ qWarning() << "Could not determine local hostname";
270+ return QString();
271+ }
272+ hostName[sizeof(hostName) - 1] = '\0';
273+ return QString::fromLocal8Bit(hostName);
274+}
275+
276+void DBusUnitySessionService::PromptLock()
277+{
278+ Q_EMIT LockRequested();
279+ Q_EMIT lockRequested();
280+}
281+
282+void DBusUnitySessionService::Lock()
283+{
284+ // lock the session using the org.freedesktop.DisplayManager system DBUS service
285+ const QString sessionPath = QString::fromLocal8Bit(qgetenv("XDG_SESSION_PATH"));
286+ QDBusMessage msg = QDBusMessage::createMethodCall("org.freedesktop.DisplayManager",
287+ sessionPath,
288+ "org.freedesktop.DisplayManager.Session",
289+ "Lock");
290+ qDebug() << "Locking session" << msg.path();
291+ QDBusReply<void> reply = QDBusConnection::systemBus().asyncCall(msg);
292+ if (!reply.isValid()) {
293+ qWarning() << "Lock call failed" << reply.error().message();
294+ } else {
295+ // emit Locked when the call succeeds
296+ Q_EMIT Locked();
297+ }
298+}
299+
300+bool DBusUnitySessionService::IsLocked() const
301+{
302+ return !d->isSessionActive;
303+}
304+
43305void DBusUnitySessionService::RequestLogout()
44306{
307+ Q_EMIT LogoutRequested(false);
45308 Q_EMIT logoutRequested(false);
46309}
47310
48311void DBusUnitySessionService::Reboot()
49312{
50− QDBusMessage msg = QDBusMessage::createMethodCall("org.freedesktop.login1",
51− "/org/freedesktop/login1",
52− "org.freedesktop.login1.Manager",
53− "Reboot");
54− msg << false;
55− QDBusConnection::systemBus().asyncCall(msg);
313+ d->makeLogin1Call("Reboot", {false});
56314}
57315
58316void DBusUnitySessionService::RequestReboot()
59317{
318+ Q_EMIT RebootRequested(false);
60319 Q_EMIT rebootRequested(false);
61320}
62321
63322void DBusUnitySessionService::Shutdown()
64323{
65− QDBusMessage msg = QDBusMessage::createMethodCall("org.freedesktop.login1",
66− "/org/freedesktop/login1",
67− "org.freedesktop.login1.Manager",
68− "PowerOff");
69− msg << false;
70− QDBusConnection::systemBus().asyncCall(msg);
324+ d->makeLogin1Call("PowerOff", {false});
325+}
326+
327+void DBusUnitySessionService::Suspend()
328+{
329+ d->makeLogin1Call("Suspend", {false});
330+}
331+
332+void DBusUnitySessionService::Hibernate()
333+{
334+ d->makeLogin1Call("Hibernate", {false});
335+}
336+
337+void DBusUnitySessionService::HybridSleep()
338+{
339+ d->makeLogin1Call("HybridSleep", {false});
71340}
72341
73342void DBusUnitySessionService::RequestShutdown()
74343{
344+ Q_EMIT ShutdownRequested(false);
75345 Q_EMIT shutdownRequested(false);
76346}
77347
@@ -83,12 +353,8 @@ enum class Action : unsigned
83353 NONE
84354};
85355
86−DBusGnomeSessionManagerWrapper::DBusGnomeSessionManagerWrapper()
87− : UnityDBusObject("/org/gnome/SessionManager/EndSessionDialog", "com.canonical.Unity")
88−{
89−}
90356
91−void DBusGnomeSessionManagerWrapper::performAsyncCall(const QString &method)
357+void performAsyncUnityCall(const QString &method)
92358{
93359 const QDBusMessage msg = QDBusMessage::createMethodCall("com.canonical.Unity",
94360 "/com/canonical/Unity/Session",
@@ -97,29 +363,111 @@ void DBusGnomeSessionManagerWrapper::performAsyncCall(const QString &method)
97363 QDBusConnection::sessionBus().asyncCall(msg);
98364}
99365
366+
367+DBusGnomeSessionManagerWrapper::DBusGnomeSessionManagerWrapper()
368+ : UnityDBusObject("/org/gnome/SessionManager/EndSessionDialog", "com.canonical.Unity")
369+{
370+}
371+
100372void DBusGnomeSessionManagerWrapper::Open(const unsigned type, const unsigned arg_1, const unsigned max_wait, const QList<QDBusObjectPath> &inhibitors)
101373{
102374 Q_UNUSED(arg_1);
103375 Q_UNUSED(max_wait);
104376 Q_UNUSED(inhibitors);
105377
106− const Action action = (Action)type;
107−
108− switch (action)
378+ switch (static_cast<Action>(type))
109379 {
110380 case Action::LOGOUT:
111− performAsyncCall("RequestLogout");
381+ performAsyncUnityCall("RequestLogout");
112382 break;
113383
114384 case Action::REBOOT:
115− performAsyncCall("RequestReboot");
385+ performAsyncUnityCall("RequestReboot");
116386 break;
117387
118388 case Action::SHUTDOWN:
119− performAsyncCall("RequestShutdown");
389+ performAsyncUnityCall("RequestShutdown");
120390 break;
121391
122392 default:
123393 break;
124394 }
125395}
396+
397+
398+DBusGnomeScreensaverWrapper::DBusGnomeScreensaverWrapper()
399+ : UnityDBusObject("/org/gnome/ScreenSaver", "org.gnome.ScreenSaver")
400+{
401+ connect(d, &DBusUnitySessionServicePrivate::screensaverActiveChanged, this, &DBusGnomeScreensaverWrapper::ActiveChanged);
402+}
403+
404+bool DBusGnomeScreensaverWrapper::GetActive() const
405+{
406+ return !d->isSessionActive; // return whether the session is not active
407+}
408+
409+void DBusGnomeScreensaverWrapper::SetActive(bool lock)
410+{
411+ if (lock) {
412+ Lock();
413+ }
414+}
415+
416+void DBusGnomeScreensaverWrapper::Lock()
417+{
418+ performAsyncUnityCall("Lock");
419+}
420+
421+quint32 DBusGnomeScreensaverWrapper::GetActiveTime() const
422+{
423+ return d->screensaverActiveTime();
424+}
425+
426+void DBusGnomeScreensaverWrapper::SimulateUserActivity()
427+{
428+ d->setIdleHint(false);
429+}
430+
431+
432+DBusScreensaverWrapper::DBusScreensaverWrapper()
433+ : UnityDBusObject("/org/freedesktop/ScreenSaver", "org.freedesktop.ScreenSaver")
434+{
435+ QDBusConnection::sessionBus().registerObject("/ScreenSaver", this, QDBusConnection::ExportScriptableContents); // compat path, also register here
436+ connect(d, &DBusUnitySessionServicePrivate::screensaverActiveChanged, this, &DBusScreensaverWrapper::ActiveChanged);
437+}
438+
439+bool DBusScreensaverWrapper::GetActive() const
440+{
441+ return !d->isSessionActive; // return whether the session is not active
442+}
443+
444+bool DBusScreensaverWrapper::SetActive(bool lock)
445+{
446+ if (lock) {
447+ Lock();
448+ return true;
449+ }
450+ return false;
451+}
452+
453+void DBusScreensaverWrapper::Lock()
454+{
455+ performAsyncUnityCall("Lock");
456+}
457+
458+quint32 DBusScreensaverWrapper::GetActiveTime() const
459+{
460+ return d->screensaverActiveTime();
461+}
462+
463+quint32 DBusScreensaverWrapper::GetSessionIdleTime() const
464+{
465+ return QDateTime::fromMSecsSinceEpoch(d->idleSinceUSecTimestamp()/1000).secsTo(QDateTime::currentDateTime());
466+}
467+
468+void DBusScreensaverWrapper::SimulateUserActivity()
469+{
470+ d->setIdleHint(false);
471+}
472+
473+#include "dbusunitysessionservice.moc"
plugins/Unity/Session/dbusunitysessionservice.h+200−16
@@ -1,5 +1,5 @@
1−/*
2− * Copyright (C) 2014 Canonical, Ltd.
1+/*
2+ * Copyright (C) 2014, 2015 Canonical, Ltd.
33 *
44 * This program is free software: you can redistribute it and/or modify it under
55 * the terms of the GNU Lesser General Public License version 3, as published by
@@ -29,7 +29,7 @@ Q_DECLARE_METATYPE(QList<QDBusObjectPath>)
2929 * interface.
3030 *
3131 * com.canonical.Unity.Session interface provides public methods
32− * and signals to handle Logout/Reboot/Shutdown.
32+ * and signals to handle eg. Logout/Reboot/Shutdown.
3333 */
3434class DBusUnitySessionService : public UnityDBusObject
3535{
@@ -46,46 +46,67 @@ public:
4646 Q_INVOKABLE void shutdown() { Shutdown(); }
4747 Q_INVOKABLE void endSession() { EndSession(); }
4848
49+ // TODO: remove duplicate signals and split D-Bus and QML API's
50+ // Apparently QML needs the signals in lowercase, while DBUS spec needs the uppercase version
4951Q_SIGNALS:
5052 /**
51− * logoutRequested signal
53+ * LogoutRequested signal
5254 *
5355 * This signal is emitted when some applications request the system to
5456 * logout.
5557 * @param have_inhibitors if there are any special running applications
5658 * which inhibit the logout.
5759 */
58− Q_SCRIPTABLE void logoutRequested(bool have_inhibitors);
60+ Q_SCRIPTABLE void LogoutRequested(bool have_inhibitors);
61+ void logoutRequested(bool have_inhibitors);
5962
6063 /**
61− * rebootRequested signal
64+ * RebootRequested signal
6265 *
6366 * This signal is emitted when some applications request the system to
6467 * reboot.
6568 * @param have_inhibitors if there are any special running applications
6669 * which inhibit the reboot.
6770 */
68− Q_SCRIPTABLE void rebootRequested(bool have_inhibitors);
71+ Q_SCRIPTABLE void RebootRequested(bool have_inhibitors);
72+ void rebootRequested(bool have_inhibitors);
6973
7074 /**
71− * shutdownRequested signal
75+ * ShutdownRequested signal
7276 *
7377 * This signal is emitted when some applications request the system to
7478 * shutdown.
7579 * @param have_inhibitors if there are any special running applications
7680 * which inhibit the shutdown.
7781 */
78− Q_SCRIPTABLE void shutdownRequested(bool have_inhibitors);
79−
82+ Q_SCRIPTABLE void ShutdownRequested(bool have_inhibitors);
83+ void shutdownRequested(bool have_inhibitors);
8084
8185 /**
82− * logoutReady signal
86+ * LogoutReady signal
8387 *
8488 * This signal is emitted when all the apps are closed. And the system
8589 * is safe to logout.
8690 */
91+ Q_SCRIPTABLE void LogoutReady();
8792 void logoutReady();
8893
94+ /**
95+ * Emitted as a result of calling PromptLock()
96+ */
97+ Q_SCRIPTABLE void LockRequested();
98+ void lockRequested();
99+
100+ /**
101+ * Emitted after the session has been locked.
102+ */
103+ Q_SCRIPTABLE void Locked();
104+
105+ /**
106+ * Emitted after the session has been unlocked.
107+ */
108+ Q_SCRIPTABLE void Unlocked();
109+
89110public Q_SLOTS:
90111 /**
91112 * Logout the system.
@@ -114,10 +135,33 @@ public Q_SLOTS:
114135 */
115136 Q_SCRIPTABLE void Shutdown();
116137
138+ /**
139+ * Suspend the system
140+ *
141+ * This method puts the system into sleep without user's confirmation.
142+ */
143+ Q_SCRIPTABLE void Suspend();
144+
145+ /**
146+ * Hibernate the system
147+ *
148+ * This method puts the system into hibernation without user's confirmation.
149+ */
150+ Q_SCRIPTABLE void Hibernate();
151+
152+ /**
153+ * Hybrid sleep
154+ *
155+ * This method puts the system into hybrid sleep without user's confirmation.
156+ *
157+ * @since unity8
158+ */
159+ Q_SCRIPTABLE void HybridSleep();
160+
117161 /**
118162 * Issue a logout request.
119163 *
120− * This method emits the logoutRequested signal to the shell with a boolean
164+ * This method emits the LogoutRequested signal to the shell with a boolean
121165 * which indicates if there's any inhibitors. The shell should receive
122166 * this signal and display a dialog to ask the user to confirm the logout
123167 * action. If confirmed, shell can call Logout() method to logout.
@@ -127,21 +171,21 @@ public Q_SLOTS:
127171 /**
128172 * Issue a reboot request.
129173 *
130− * This method emits the rebootRequested signal to the shell with a boolean
174+ * This method emits the RebootRequested signal to the shell with a boolean
131175 * which indicates if there's any inhibitors. The shell should receive
132176 * this signal and display a dialog to ask the user to confirm the reboot
133− * action. If confirmed, shell can call Reboot() method to reboot
177+ * action. If confirmed, shell can call Reboot() method to reboot.
134178 */
135179 Q_SCRIPTABLE void RequestReboot();
136180
137181 /**
138182 * Issue a shutdown request.
139183 *
140− * This method emits the shutdownRequested signal to the shell with a
184+ * This method emits the ShutdownRequested signal to the shell with a
141185 * boolean which indicates if there's any inhibitors.
142186 * The shell should receive
143187 * this signal and display a dialog to ask the user to confirm the reboot
144− * action. If confirmed, shell can call Shutdown() method to reboot
188+ * action. If confirmed, shell can call Shutdown() method to shutdown.
145189 */
146190 Q_SCRIPTABLE void RequestShutdown();
147191
@@ -152,6 +196,68 @@ public Q_SLOTS:
152196 * current DBus session bus.
153197 */
154198 Q_SCRIPTABLE void EndSession();
199+
200+ /**
201+ * @return whether the system is capable of hibernating
202+ */
203+ Q_SCRIPTABLE bool CanHibernate() const;
204+
205+ /**
206+ * @return whether the system is capable of suspending
207+ */
208+ Q_SCRIPTABLE bool CanSuspend() const;
209+
210+ /**
211+ * @return whether the system is capable of hybrid sleep
212+ * @since unity8
213+ */
214+ Q_SCRIPTABLE bool CanHybridSleep() const;
215+
216+ /**
217+ * @return whether the system is capable of rebooting
218+ * @since unity8
219+ */
220+ Q_SCRIPTABLE bool CanReboot() const;
221+
222+ /**
223+ * @return whether the system is capable of shutting down
224+ */
225+ Q_SCRIPTABLE bool CanShutdown() const;
226+
227+ /**
228+ * @return whether the system is capable of locking the session
229+ */
230+ Q_SCRIPTABLE bool CanLock() const;
231+
232+ /**
233+ * @return the login name of the current user
234+ */
235+ Q_SCRIPTABLE QString UserName() const;
236+
237+ /**
238+ * @return the real name of the current user
239+ */
240+ Q_SCRIPTABLE QString RealName() const;
241+
242+ /**
243+ * @return the local hostname
244+ */
245+ Q_SCRIPTABLE QString HostName() const;
246+
247+ /**
248+ * Request that the session get locked, emits signal LockRequested()
249+ */
250+ Q_SCRIPTABLE void PromptLock();
251+
252+ /**
253+ * Locks the session immediately
254+ */
255+ Q_SCRIPTABLE void Lock();
256+
257+ /**
258+ * @return whether the session is currently locked
259+ */
260+ Q_SCRIPTABLE bool IsLocked() const;
155261};
156262
157263class DBusGnomeSessionManagerWrapper : public UnityDBusObject
@@ -169,4 +275,82 @@ public Q_SLOTS:
169275private:
170276 void performAsyncCall(const QString &method);
171277};
278+
279+class DBusGnomeScreensaverWrapper: public UnityDBusObject
280+{
281+ Q_OBJECT
282+ Q_CLASSINFO("D-Bus Interface", "org.gnome.ScreenSaver")
283+
284+public:
285+ DBusGnomeScreensaverWrapper();
286+ ~DBusGnomeScreensaverWrapper() = default;
287+
288+public Q_SLOTS:
289+ /**
290+ * @return whether the session is currently locked (screensaver is on)
291+ */
292+ Q_SCRIPTABLE bool GetActive() const;
293+
294+ /**
295+ * Locks the session immediately if @p lock is true
296+ */
297+ Q_SCRIPTABLE void SetActive(bool lock);
298+
299+ /**
300+ * Locks the session immediately
301+ */
302+ Q_SCRIPTABLE void Lock();
303+
304+ /**
305+ * @return the number of seconds elapsed since the screensaver had been active
306+ */
307+ Q_SCRIPTABLE quint32 GetActiveTime() const;
308+
309+ Q_SCRIPTABLE void SimulateUserActivity();
310+
311+Q_SIGNALS:
312+ void ActiveChanged(bool active);
313+};
314+
315+class DBusScreensaverWrapper: public UnityDBusObject
316+{
317+ Q_OBJECT
318+ Q_CLASSINFO("D-Bus Interface", "org.freedesktop.ScreenSaver")
319+
320+public:
321+ DBusScreensaverWrapper();
322+ ~DBusScreensaverWrapper() = default;
323+
324+public Q_SLOTS:
325+ /**
326+ * @return whether the session is currently locked (screensaver is on)
327+ */
328+ Q_SCRIPTABLE bool GetActive() const;
329+
330+ /**
331+ * Locks the session immediately if @p lock is true
332+ */
333+ Q_SCRIPTABLE bool SetActive(bool lock);
334+
335+ /**
336+ * Locks the session immediately
337+ */
338+ Q_SCRIPTABLE void Lock();
339+
340+ /**
341+ * @return the number of seconds elapsed since the screensaver had been active
342+ */
343+ Q_SCRIPTABLE quint32 GetActiveTime() const;
344+
345+ /**
346+ * @return the number of seconds that this session has been idle
347+ */
348+ Q_SCRIPTABLE quint32 GetSessionIdleTime() const;
349+
350+ Q_SCRIPTABLE void SimulateUserActivity();
351+
352+Q_SIGNALS:
353+ void ActiveChanged(bool active);
354+};
355+
172356#endif // DBUSUNITYSESSIONSERVICE_H
plugins/Unity/Session/plugin.cpp+2
@@ -27,6 +27,8 @@
2727static QObject *dbusunitysessionservice_provider(QQmlEngine */*engine*/, QJSEngine */*jsEngine*/)
2828{
2929 new DBusGnomeSessionManagerWrapper();
30+ new DBusGnomeScreensaverWrapper();
31+ new DBusScreensaverWrapper();
3032 return new DBusUnitySessionService();
3133}
3234
plugins/Utils/CMakeLists.txt+1
@@ -4,6 +4,7 @@ include_directories(
44 ${CMAKE_CURRENT_SOURCE_DIR}
55 ${CMAKE_CURRENT_BINARY_DIR}
66 ${Qt5Gui_PRIVATE_INCLUDE_DIRS}
7+ ${Qt5Quick_PRIVATE_INCLUDE_DIRS}
78 ${GIO_INCLUDE_DIRS}
89)
910
po/ar.po+18−14
@@ -8,7 +8,7 @@ msgstr ""
88"Project-Id-Version: unity\n"
99"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
1010"POT-Creation-Date: 2015-07-17 10:10+0200\n"
11−"PO-Revision-Date: 2015-07-17 12:36+0000\n"
11+"PO-Revision-Date: 2015-07-29 09:41+0000\n"
1212"Last-Translator: Ibrahim Saed <ibraheem5000@gmail.com>\n"
1313"Language-Team: Arabic <ar@li.org>\n"
1414"MIME-Version: 1.0\n"
@@ -16,8 +16,8 @@ msgstr ""
1616"Content-Transfer-Encoding: 8bit\n"
1717"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n % 100 >= "
1818"3 && n % 100 <= 10 ? 3 : n % 100 >= 11 && n % 100 <= 99 ? 4 : 5;\n"
19−"X-Launchpad-Export-Date: 2015-07-25 05:38+0000\n"
20−"X-Generator: Launchpad (build 17628)\n"
19+"X-Launchpad-Export-Date: 2015-08-04 05:41+0000\n"
20+"X-Generator: Launchpad (build 17656)\n"
2121
2222#: plugins/LightDM/Greeter.cpp:112
2323msgid "Password: "
@@ -310,7 +310,7 @@ msgstr "الرئيسية"
310310
311311#: qml/Dash/ScopesList.qml:114
312312msgid "Also installed"
313−msgstr ""
313+msgstr "أيضا تم تثبيت"
314314
315315#: qml/Greeter/CoverPage.qml:107
316316msgid "Unlock"
@@ -373,7 +373,7 @@ msgstr "نأسف، عبارة السّر غير صحيحية"
373373#: qml/Greeter/NarrowView.qml:121
374374msgctxt "passphrase"
375375msgid "Please re-enter"
376−msgstr ""
376+msgstr "برجاء إعادة إدخالها"
377377
378378#: qml/Greeter/NarrowView.qml:122
379379msgid "Sorry, incorrect passcode"
@@ -389,11 +389,11 @@ msgstr "انقر للعودة إلى المكالمة..."
389389
390390#: qml/Panel/ActiveCallHint.qml:92
391391msgid "Conference"
392−msgstr ""
392+msgstr "مؤتمر"
393393
394394#: qml/Panel/Indicators/MenuItemFactory.qml:639
395395msgid "Nothing is playing"
396−msgstr ""
396+msgstr "لا شيء مُشغّل"
397397
398398#: qml/Panel/Indicators/MenuItemFactory.qml:768
399399#, qt-format
@@ -460,12 +460,12 @@ msgstr "انتهى"
460460
461461#: qml/Panel/Indicators/MenuItemFactory.qml:800
462462msgid "Failed, tap to retry"
463−msgstr ""
463+msgstr "فشل، انقل لإعادة المحاولة"
464464
465465#: qml/Panel/Indicators/MessageMenuItemFactory.qml:149
466466msgctxt "Label: Hint in message indicator line edit"
467467msgid "Reply"
468−msgstr ""
468+msgstr "رد"
469469
470470#: qml/Panel/Indicators/ModemInfoItem.qml:105
471471msgid "Unlock SIM"
@@ -514,7 +514,7 @@ msgstr "اسحب سحبة قصيرة من الحافة اليسرى."
514514
515515#: qml/Tutorial/TutorialPage.qml:178
516516msgid "You almost got it!"
517−msgstr ""
517+msgstr "انت حصلت على ذلك تقريبا!"
518518
519519#: qml/Tutorial/TutorialPage.qml:195
520520msgid "Try again."
@@ -661,28 +661,32 @@ msgid ""
661661"Your phone is set up to automatically report errors to Canonical and its "
662662"partners, the makers of the operating system."
663663msgstr ""
664+"هاتفك مُعدّ لإرسال تقارير آلية بالأخطاء إلى كانونيكال وشركائها (صانعي نظام "
665+"التشغيل)."
664666
665667#: qml/Wizard/Pages/60-reporting.qml:43
666668msgid ""
667669"This can be disabled in <b>System Settings</b> under <b>Security &amp; "
668670"Privacy</b>"
669671msgstr ""
672+"يمكن تعطيل ذلك بالذهاب إلى <b>إعدادات النظام</b> من خلال <b>الأمان "
673+"والخصوصية</b>"
670674
671675#: qml/Wizard/Pages/80-finished.qml:24
672676msgid "All done"
673−msgstr ""
677+msgstr "تم ذلك"
674678
675679#: qml/Wizard/Pages/80-finished.qml:39
676680msgid "Nice work!"
677−msgstr ""
681+msgstr "عمل جيد!"
678682
679683#: qml/Wizard/Pages/80-finished.qml:46
680684msgid "Your phone is now ready to use."
681−msgstr ""
685+msgstr "هاتفك جاهز الآن للاستخدام."
682686
683687#: qml/Wizard/Pages/80-finished.qml:53
684688msgid "Finish"
685−msgstr ""
689+msgstr "إنهاء"
686690
687691#: qml/Wizard/Pages/here-terms.qml:27
688692msgid "Terms & Conditions"
po/cy.ponew file+722
@@ -0,0 +1,722 @@
1+# Welsh translation for unity8
2+# Copyright (c) 2015 Rosetta Contributors and Canonical Ltd 2015
3+# This file is distributed under the same license as the unity8 package.
4+# FIRST AUTHOR <EMAIL@ADDRESS>, 2015.
5+#
6+msgid ""
7+msgstr ""
8+"Project-Id-Version: unity8\n"
9+"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
10+"POT-Creation-Date: 2015-07-17 10:10+0200\n"
11+"PO-Revision-Date: 2015-07-31 13:21+0000\n"
12+"Last-Translator: Owen Llywelyn <owen.llywelyn@gmail.com>\n"
13+"Language-Team: Welsh <cy@li.org>\n"
14+"MIME-Version: 1.0\n"
15+"Content-Type: text/plain; charset=UTF-8\n"
16+"Content-Transfer-Encoding: 8bit\n"
17+"Plural-Forms: nplurals=4; plural=n==1 ? 0 : n==2 ? 1 : (n != 8 && n != 11) ? "
18+"2 : 3;\n"
19+"X-Launchpad-Export-Date: 2015-08-04 05:41+0000\n"
20+"X-Generator: Launchpad (build 17656)\n"
21+
22+#: plugins/LightDM/Greeter.cpp:112
23+msgid "Password: "
24+msgstr "Cyfrinair: "
25+
26+#: plugins/Unity/Launcher/launcheritem.cpp:45
27+#: plugins/Unity/Launcher/launcheritem.cpp:100
28+msgid "Pin shortcut"
29+msgstr "Pinio llwybr byr"
30+
31+#: plugins/Unity/Launcher/launcheritem.cpp:100
32+msgid "Unpin shortcut"
33+msgstr "Dadbinio llwybr byr"
34+
35+#. Translators, please edit/rearrange these strftime(3) tokens to suit your locale!
36+#. This format string is used for showing, on a 12-hour clock, times that happen yesterday.
37+#. (\u2003 is a unicode em space which is slightly wider than a normal space.)
38+#. en_US example: "Yesterday\u2003%l:%M %p" --> "Yesterday 1:00 PM"
39+#: plugins/Utils/relativetimeformatter.cpp:157
40+msgid "Yesterday %l:%M %p"
41+msgstr "Ddoe %l:%M %p"
42+
43+#. Translators, please edit/rearrange these strftime(3) tokens to suit your locale!
44+#. This format string is used for showing, on a 12-hour clock, times that happened today.
45+#. en_US example: "%l:%M %p" --> "1:00 PM"
46+#: plugins/Utils/relativetimeformatter.cpp:164
47+msgid "%l:%M %p"
48+msgstr "%l:%M %p"
49+
50+#. Translators, please edit/rearrange these strftime(3) tokens to suit your locale!
51+#. This format string is used for showing, on a 12-hour clock, events/appointments that happen tomorrow.
52+#. (\u2003 is a unicode em space which is slightly wider than a normal space.)
53+#. en_US example: "Tomorrow\u2003%l:%M %p" --> "Tomorrow 1:00 PM"
54+#: plugins/Utils/relativetimeformatter.cpp:172
55+msgid "Tomorrow %l:%M %p"
56+msgstr "Yfory %l:%M %p"
57+
58+#. Translators, please edit/rearrange these strftime(3) tokens to suit your locale!
59+#. This format string is used for showing, on a 12-hour clock, times that happened in the last week.
60+#. (\u2003 is a unicode em space which is slightly wider than a normal space.)
61+#. en_US example: "%a\u2003%l:%M %p" --> "Fri 1:00 PM"
62+#: plugins/Utils/relativetimeformatter.cpp:181
63+msgid "%a %l:%M %p"
64+msgstr "%a %l:%M %p"
65+
66+#. Translators, please edit/rearrange these strftime(3) tokens to suit your locale!
67+#. This format string is used for showing, on a 12-hour clock, times that happened before a week from now.
68+#. (\u2003 is a unicode em space which is slightly wider than a normal space.)
69+#. en_US example: "%a %d %b\u2003%l:%M %p" --> "Fri Oct 31 1:00 PM"
70+#. en_GB example: "%a %b %d\u2003%l:%M %p" --> "Fri 31 Oct 1:00 PM"
71+#: plugins/Utils/relativetimeformatter.cpp:190
72+msgid "%a %d %b %l:%M %p"
73+msgstr "%a %d %b %l:%M %p"
74+
75+#. Translators, please edit/rearrange these strftime(3) tokens to suit your locale!
76+#. This format string is used for showing, on a 24-hour clock, times that happen yesterday.
77+#. (\u2003 is a unicode em space which is slightly wider than a normal space.)
78+#. en_US example: "Yesterday\u2003%l:%M %p" --> "Yesterday 13:00"
79+#: plugins/Utils/relativetimeformatter.cpp:201
80+msgid "Yesterday %H:%M"
81+msgstr "Ddoe %H:%M"
82+
83+#. Translators, please edit/rearrange these strftime(3) tokens to suit your locale!
84+#. This format string is used for showing, on a 24-hour clock, times that happened today.
85+#. en_US example: "%H:%M" --> "13:00"
86+#: plugins/Utils/relativetimeformatter.cpp:208
87+msgid "%H:%M"
88+msgstr "%H:%M"
89+
90+#. Translators, please edit/rearrange these strftime(3) tokens to suit your locale!
91+#. This format string is used for showing, on a 24-hour clock, events/appointments that happen tomorrow.
92+#. (\u2003 is a unicode em space which is slightly wider than a normal space.)
93+#. en_US example: "Tomorrow\u2003%l:%M %p" --> "Tomorrow 13:00"
94+#: plugins/Utils/relativetimeformatter.cpp:216
95+msgid "Tomorrow %H:%M"
96+msgstr "Yfory %H:%M"
97+
98+#. Translators, please edit/rearrange these strftime(3) tokens to suit your locale!
99+#. This format string is used for showing, on a 24-hour clock, times that happened in the last week.
100+#. (\u2003 is a unicode em space which is slightly wider than a normal space.)
101+#. en_US example: "%a\u2003%H:%M" --> "Fri 13:00"
102+#: plugins/Utils/relativetimeformatter.cpp:225
103+msgid "%a %H:%M"
104+msgstr "%a %H:%M"
105+
106+#. Translators, please edit/rearrange these strftime(3) tokens to suit your locale!
107+#. This format string is used for showing, on a 24-hour clock, times that happened before a week from now.
108+#. (\u2003 is a unicode em space which is slightly wider than a normal space.)
109+#. en_US example: "%a %d %b\u2003%H:%M" --> "Fri Oct 31 13:00"
110+#. en_GB example: "%a %b %d\u2003%H:%M" --> "Fri 31 Oct 13:00"
111+#: plugins/Utils/relativetimeformatter.cpp:234
112+msgid "%a %d %b %H:%M"
113+msgstr "%a %d %b %H:%M"
114+
115+#: qml/Components/DelayedLockscreen.qml:49
116+msgid "Device Locked"
117+msgstr "Dyfais ar glo"
118+
119+#: qml/Components/DelayedLockscreen.qml:64
120+msgid "You have been locked out due to too many failed passphrase attempts."
121+msgstr ""
122+"Cawsoch eich cloi allan am gyflwyno cyfrinair anghywir ormod o weithiau."
123+
124+#: qml/Components/DelayedLockscreen.qml:65
125+msgid "You have been locked out due to too many failed passcode attempts."
126+msgstr ""
127+"Cawsoch eich cloi allan am gyflwyno côd mynediad anghywir ormod o weithiau."
128+
129+#: qml/Components/DelayedLockscreen.qml:74
130+#, qt-format
131+msgid "Please wait %1 minute and then try again…"
132+msgid_plural "Please wait %1 minutes and then try again…"
133+msgstr[0] "Arhoswch am %1 munud cyn cynnig eto..."
134+msgstr[1] "Arhoswch am %1 munud cyn cynnig eto..."
135+msgstr[2] "Arhoswch am %1 munud cyn cynnig eto..."
136+msgstr[3] "Arhoswch am %1 munud cyn cynnig eto..."
137+
138+#: qml/Components/Dialogs.qml:69
139+msgctxt "Title: Lock/Log out dialog"
140+msgid "Log out"
141+msgstr "Allgofnodi"
142+
143+#: qml/Components/Dialogs.qml:70
144+msgid "Are you sure you want to log out?"
145+msgstr "Ydych chi'n sicr eich bod am allgofnodi?"
146+
147+#: qml/Components/Dialogs.qml:72
148+msgctxt "Button: Lock the system"
149+msgid "Lock"
150+msgstr "Cloi"
151+
152+#: qml/Components/Dialogs.qml:79
153+msgctxt "Button: Log out from the system"
154+msgid "Log Out"
155+msgstr "Allgofnodi"
156+
157+#: qml/Components/Dialogs.qml:86 qml/Components/Dialogs.qml:117
158+#: qml/Components/Dialogs.qml:173
159+msgid "Cancel"
160+msgstr "Diddymu"
161+
162+#: qml/Components/Dialogs.qml:98
163+msgctxt "Title: Reboot/Shut down dialog"
164+msgid "Shut down"
165+msgstr "Diffodd"
166+
167+#: qml/Components/Dialogs.qml:99
168+msgid "Are you sure you want to shut down?"
169+msgstr "Ydych chi'n sicr eich bod am ddiffodd?"
170+
171+#: qml/Components/Dialogs.qml:101
172+msgctxt "Button: Reboot the system"
173+msgid "Reboot"
174+msgstr "Ailgychwyn"
175+
176+#: qml/Components/Dialogs.qml:109
177+msgctxt "Button: Shut down the system"
178+msgid "Shut down"
179+msgstr "Diffodd"
180+
181+#: qml/Components/Dialogs.qml:129
182+msgctxt "Title: Reboot dialog"
183+msgid "Reboot"
184+msgstr "Ailgychwyn"
185+
186+#: qml/Components/Dialogs.qml:130
187+msgid "Are you sure you want to reboot?"
188+msgstr "Ydych chi'n sicr eich bod am ailgychwyn?"
189+
190+#: qml/Components/Dialogs.qml:132
191+msgid "No"
192+msgstr "Na"
193+
194+#: qml/Components/Dialogs.qml:138
195+msgid "Yes"
196+msgstr "Ydw"
197+
198+#: qml/Components/Dialogs.qml:152
199+msgctxt "Title: Power off/Restart dialog"
200+msgid "Power"
201+msgstr "Pŵer"
202+
203+#: qml/Components/Dialogs.qml:153
204+msgid ""
205+"Are you sure you would like\n"
206+"to power off?"
207+msgstr ""
208+"Ydych chi'n sicr eich bod am\n"
209+"ddiffodd y pŵer?"
210+
211+#: qml/Components/Dialogs.qml:155
212+msgctxt "Button: Power off the system"
213+msgid "Power off"
214+msgstr "Diffodd y pŵer"
215+
216+#: qml/Components/Dialogs.qml:164
217+msgctxt "Button: Restart the system"
218+msgid "Restart"
219+msgstr "Ailgychwyn"
220+
221+#: qml/Components/Lockscreen.qml:245
222+msgid "Return to Call"
223+msgstr "Nôl i'r alwad"
224+
225+#: qml/Components/Lockscreen.qml:245
226+msgid "Emergency Call"
227+msgstr "Galwad Frys"
228+
229+#: qml/Components/Lockscreen.qml:277
230+msgid "OK"
231+msgstr "Iawn"
232+
233+#: qml/Dash/GenericScopeView.qml:539 qml/Dash/GenericScopeView.qml:697
234+msgid "See less"
235+msgstr "Gweld llai"
236+
237+#: qml/Dash/GenericScopeView.qml:539
238+msgid "See all"
239+msgstr "Gweld y cyfan"
240+
241+#: qml/Dash/GenericScopeView.qml:601
242+msgctxt "Label: Hint for dash search line edit"
243+msgid "Search"
244+msgstr "Chwilio"
245+
246+#: qml/Dash/PageHeader.qml:269
247+msgctxt "Button: Open the Ubuntu Store"
248+msgid "Store"
249+msgstr "Marchnad"
250+
251+#: qml/Dash/PageHeader.qml:276
252+msgctxt "Button: Start a search in the current dash scope"
253+msgid "Search"
254+msgstr "Chwilio"
255+
256+#: qml/Dash/PageHeader.qml:286
257+msgctxt "Button: Show the current dash scope settings"
258+msgid "Settings"
259+msgstr "Gosodiadau"
260+
261+#: qml/Dash/PageHeader.qml:293
262+msgid "Remove from Favorites"
263+msgstr "Tynnu o Ffefrynnau"
264+
265+#: qml/Dash/PageHeader.qml:293
266+msgid "Add to Favorites"
267+msgstr "Ychwanegu at Ffefrynnau"
268+
269+#: qml/Dash/Previews/PreviewActionCombo.qml:35
270+msgid "More..."
271+msgstr "Mwy..."
272+
273+#: qml/Dash/Previews/PreviewActionCombo.qml:35
274+msgid "Less..."
275+msgstr "Llai..."
276+
277+#: qml/Dash/Previews/PreviewRatingInput.qml:84
278+msgid "Rate this"
279+msgstr "Graddio hwn"
280+
281+#: qml/Dash/Previews/PreviewRatingInput.qml:129
282+msgid "Add a review"
283+msgstr "Ychwanegu adolygiad"
284+
285+#: qml/Dash/Previews/PreviewRatingInput.qml:169
286+msgid "Send"
287+msgstr "Anfon"
288+
289+#: qml/Dash/PullToRefreshScopeStyle.qml:55
290+msgid "Pull to refresh…"
291+msgstr "Tynnwch i adnewyddu..."
292+
293+#: qml/Dash/PullToRefreshScopeStyle.qml:60
294+msgid "Release to refresh…"
295+msgstr "Rhyddhewch i adnewyddu..."
296+
297+#: qml/Dash/ScopeSettings/ScopeSettingBoolean.qml:43
298+msgid "Enable location data"
299+msgstr "Galluogi data lleoliad"
300+
301+#: qml/Dash/ScopesList.qml:67
302+msgid "Manage"
303+msgstr "Rheoli"
304+
305+#: qml/Dash/ScopesList.qml:113
306+msgid "Home"
307+msgstr "Cartref"
308+
309+#: qml/Dash/ScopesList.qml:114
310+msgid "Also installed"
311+msgstr "Hefyd wedi gosod"
312+
313+#: qml/Greeter/CoverPage.qml:107
314+msgid "Unlock"
315+msgstr "Datgloi"
316+
317+#: qml/Greeter/LoginList.qml:236
318+msgid "Retry"
319+msgstr "Ailgynnig"
320+
321+#: qml/Greeter/LoginList.qml:237
322+msgid "Tap to unlock"
323+msgstr "Tapiwch i ddatgloi"
324+
325+#: qml/Greeter/NarrowView.qml:54 qml/Wizard/Pages/passwd-confirm.qml:53
326+msgid "Sorry, incorrect passphrase."
327+msgstr "Cyfrinair anghywir"
328+
329+#: qml/Greeter/NarrowView.qml:55 qml/Wizard/Pages/passwd-confirm.qml:54
330+msgid "Sorry, incorrect passcode."
331+msgstr "Cod cyfrinachol anghywir"
332+
333+#: qml/Greeter/NarrowView.qml:56
334+msgid "This will be your last attempt."
335+msgstr "Hwn yw eich ymgais olaf"
336+
337+#: qml/Greeter/NarrowView.qml:58
338+msgid ""
339+"If passphrase is entered incorrectly, your phone will conduct a factory "
340+"reset and all personal data will be deleted."
341+msgstr ""
342+"Os fydd y cyfrinair yn anghywir bydd eich ffôn yn cael ei ailosod i gyflwr "
343+"ffatri a bydd data personol yn cael ei ddileu."
344+
345+#: qml/Greeter/NarrowView.qml:59
346+msgid ""
347+"If passcode is entered incorrectly, your phone will conduct a factory reset "
348+"and all personal data will be deleted."
349+msgstr ""
350+"Os fydd y cod cyfrinachol yn anghywir bydd eich ffôn yn cael ei ailosod i "
351+"gyflwr ffatri a bydd data personol yn cael ei ddileu."
352+
353+#: qml/Greeter/NarrowView.qml:116
354+#, qt-format
355+msgid "Enter %1"
356+msgstr "Rhowch %1"
357+
358+#: qml/Greeter/NarrowView.qml:117 qml/Wizard/Pages/passwd-set.qml:60
359+msgid "Enter passphrase"
360+msgstr "Rhowch gyfrinair"
361+
362+#: qml/Greeter/NarrowView.qml:118
363+msgid "Enter passcode"
364+msgstr "Rhowch god cyfrinachol"
365+
366+#: qml/Greeter/NarrowView.qml:119
367+#, qt-format
368+msgid "Sorry, incorrect %1"
369+msgstr "%1 anghywir"
370+
371+#: qml/Greeter/NarrowView.qml:120
372+msgid "Sorry, incorrect passphrase"
373+msgstr "Cyfrinair anghywir"
374+
375+#: qml/Greeter/NarrowView.qml:121
376+msgctxt "passphrase"
377+msgid "Please re-enter"
378+msgstr "Ceisiwch eto"
379+
380+#: qml/Greeter/NarrowView.qml:122
381+msgid "Sorry, incorrect passcode"
382+msgstr "Cod cyfrinachol anghywir"
383+
384+#: qml/Notifications/NotificationMenuItemFactory.qml:128
385+msgid "Show password"
386+msgstr "Dangos cyfrinair"
387+
388+#: qml/Panel/ActiveCallHint.qml:79
389+msgid "Tap to return to call..."
390+msgstr "Tapiwch i ffonio nôl"
391+
392+#: qml/Panel/ActiveCallHint.qml:92
393+msgid "Conference"
394+msgstr "Cydalwad"
395+
396+#: qml/Panel/Indicators/MenuItemFactory.qml:639
397+msgid "Nothing is playing"
398+msgstr "Dim byd yn chwarae"
399+
400+#: qml/Panel/Indicators/MenuItemFactory.qml:768
401+#, qt-format
402+msgid "%1 hour"
403+msgid_plural "%1 hours"
404+msgstr[0] "%1 awr"
405+msgstr[1] "%1 awr"
406+msgstr[2] "%1 awr"
407+msgstr[3] "%1 awr"
408+
409+#: qml/Panel/Indicators/MenuItemFactory.qml:772
410+#, qt-format
411+msgid "%1 minute"
412+msgid_plural "%1 minutes"
413+msgstr[0] "%1 munud"
414+msgstr[1] "%1 munud"
415+msgstr[2] "%1 munud"
416+msgstr[3] "%1 munud"
417+
418+#: qml/Panel/Indicators/MenuItemFactory.qml:777
419+#, qt-format
420+msgid "%1 second"
421+msgid_plural "%1 seconds"
422+msgstr[0] "%1 eiliad"
423+msgstr[1] "%1 eiliad"
424+msgstr[2] "%1 eiliad"
425+msgstr[3] "%1 eiliad"
426+
427+#: qml/Panel/Indicators/MenuItemFactory.qml:780
428+msgid "0 seconds"
429+msgstr "0 eiliad"
430+
431+#. Translators: String like "1 hour, 2 minutes, 3 seconds remaining"
432+#: qml/Panel/Indicators/MenuItemFactory.qml:782
433+#, qt-format
434+msgid "%1 remaining"
435+msgstr "%1 yn weddill"
436+
437+#: qml/Panel/Indicators/MenuItemFactory.qml:788
438+msgid "In queue…"
439+msgstr "Mewn ciw..."
440+
441+#: qml/Panel/Indicators/MenuItemFactory.qml:792
442+msgid "Downloading"
443+msgstr "Yn llwytho i lawr"
444+
445+#: qml/Panel/Indicators/MenuItemFactory.qml:794
446+msgid "Paused, tap to resume"
447+msgstr "Seibiwyd, tapiwch i ailafael ynddi"
448+
449+#: qml/Panel/Indicators/MenuItemFactory.qml:796
450+msgid "Canceled"
451+msgstr "Diddymwyd"
452+
453+#: qml/Panel/Indicators/MenuItemFactory.qml:798
454+msgid "Finished"
455+msgstr "Wedi gorffen"
456+
457+#: qml/Panel/Indicators/MenuItemFactory.qml:800
458+msgid "Failed, tap to retry"
459+msgstr "Methwyd, tapiwch i geisio eto"
460+
461+#: qml/Panel/Indicators/MessageMenuItemFactory.qml:149
462+msgctxt "Label: Hint in message indicator line edit"
463+msgid "Reply"
464+msgstr "Ateb"
465+
466+#: qml/Panel/Indicators/ModemInfoItem.qml:105
467+msgid "Unlock SIM"
468+msgstr "Datgloi SIM"
469+
470+#: qml/Panel/Indicators/RoamingIndication.qml:27
471+msgid "Roaming"
472+msgstr "Crwydrol"
473+
474+#: qml/Tutorial/TutorialBottomFinish.qml:24
475+msgid "This action does different things for different apps"
476+msgstr "Mae'r weithred hon yn gwneud pethau gwahanol mewn apiau gwahanol"
477+
478+#: qml/Tutorial/TutorialBottomFinish.qml:25
479+msgid "Tap here to finish."
480+msgstr "Tapiwch yma i orffen"
481+
482+#: qml/Tutorial/TutorialBottom.qml:28
483+msgid "Open special menus"
484+msgstr "Agor dewislenni arbennig"
485+
486+#: qml/Tutorial/TutorialBottom.qml:29
487+msgid "Swipe up from the bottom edge."
488+msgstr "Trawiwch i fyny o'r ymyl gwaelod."
489+
490+#: qml/Tutorial/TutorialContent.qml:81 qml/Tutorial/TutorialLeftFinish.qml:25
491+#: qml/Tutorial/TutorialRight.qml:144
492+msgid "Tap here to continue."
493+msgstr "Tapiwch yma i barhau."
494+
495+#: qml/Tutorial/TutorialContent.qml:81
496+msgid "Click here to finish."
497+msgstr "Cliciwch yma i orffen."
498+
499+#: qml/Tutorial/TutorialLeftFinish.qml:24
500+msgid "These are the shortcuts to favorite apps"
501+msgstr "Rhain yw'r llwybrau byr i'ch hoff apiau"
502+
503+#: qml/Tutorial/TutorialLeft.qml:26
504+msgid "Open the launcher"
505+msgstr "Agor y lansiwr"
506+
507+#: qml/Tutorial/TutorialLeft.qml:27
508+msgid "Short swipe from the left edge."
509+msgstr "Trawiad byr o'r ymyl chwith."
510+
511+#: qml/Tutorial/TutorialPage.qml:178
512+msgid "You almost got it!"
513+msgstr "Bron â bod!"
514+
515+#: qml/Tutorial/TutorialPage.qml:195
516+msgid "Try again."
517+msgstr "Ceisiwch eto."
518+
519+#: qml/Tutorial/TutorialRight.qml:31
520+msgid "To view open apps"
521+msgstr "I weld apiau sydd ar agor"
522+
523+#: qml/Tutorial/TutorialRight.qml:32
524+msgid "Long swipe from the right edge."
525+msgstr "Trawiad hir o'r ymyl dde."
526+
527+#: qml/Tutorial/TutorialRight.qml:122
528+msgid "View all your running tasks."
529+msgstr "Gweld pob tasg sy'n rhedeg."
530+
531+#: qml/Wizard/Page.qml:89
532+msgctxt "Button: Go back one page in the Wizard"
533+msgid "Back"
534+msgstr "Nôl"
535+
536+#: qml/Wizard/Pages/10-welcome.qml:27
537+msgid "Hi!"
538+msgstr "Shwmae!"
539+
540+#: qml/Wizard/Pages/10-welcome.qml:44
541+msgid "Welcome to your Ubuntu phone."
542+msgstr "Croeso i'ch ffôn Ubuntu."
543+
544+#: qml/Wizard/Pages/10-welcome.qml:52
545+msgid "Let’s get started."
546+msgstr "Beth am gychwyn."
547+
548+#: qml/Wizard/Pages/10-welcome.qml:88 qml/Wizard/Pages/30-passwd-type.qml:128
549+#: qml/Wizard/Pages/40-wifi.qml:215 qml/Wizard/Pages/50-location.qml:131
550+#: qml/Wizard/Pages/60-reporting.qml:50 qml/Wizard/Pages/passwd-confirm.qml:82
551+#: qml/Wizard/Pages/passwd-set.qml:90
552+msgid "Continue"
553+msgstr "Parhau"
554+
555+#: qml/Wizard/Pages/20-sim.qml:25
556+msgid "Add a SIM card and restart your device"
557+msgstr "Ychwanegwch gerdyn SIM ac ailgychwyn y ddyfais"
558+
559+#: qml/Wizard/Pages/20-sim.qml:59
560+msgid "Without it, you won’t be able to make calls or use text messaging."
561+msgstr "Hebddo bydd dim modd gwneud galwadau na defnyddio negeseuon testun."
562+
563+#: qml/Wizard/Pages/20-sim.qml:73 qml/Wizard/Pages/40-wifi.qml:215
564+msgid "Skip"
565+msgstr "Neidio"
566+
567+#: qml/Wizard/Pages/30-passwd-type.qml:39
568+msgid "Lock security"
569+msgstr "Diogelwch cloi"
570+
571+#: qml/Wizard/Pages/30-passwd-type.qml:74
572+msgid "Please select how you’d like to unlock your phone."
573+msgstr "Dewiswch sut yr hoffech ddatgloi'r ffôn."
574+
575+#: qml/Wizard/Pages/30-passwd-type.qml:97
576+msgctxt "Label: Type of security method"
577+msgid "Swipe"
578+msgstr "Trawio"
579+
580+#: qml/Wizard/Pages/30-passwd-type.qml:98
581+msgctxt "Label: Description of security method"
582+msgid "No security"
583+msgstr "Dim diogelwch"
584+
585+#: qml/Wizard/Pages/30-passwd-type.qml:100
586+msgctxt "Label: Type of security method"
587+msgid "Passcode"
588+msgstr "Cod cyfrinachol"
589+
590+#: qml/Wizard/Pages/30-passwd-type.qml:101
591+msgctxt "Label: Description of security method"
592+msgid "4 digits only"
593+msgstr "4 digid yn unig"
594+
595+#: qml/Wizard/Pages/30-passwd-type.qml:103
596+msgctxt "Label: Type of security method"
597+msgid "Passphrase"
598+msgstr "Cyfrinair"
599+
600+#: qml/Wizard/Pages/30-passwd-type.qml:104
601+msgctxt "Label: Description of security method"
602+msgid "Numbers and letters"
603+msgstr "Rhifau a llythrennau"
604+
605+#: qml/Wizard/Pages/40-wifi.qml:29
606+msgid "Connect to Wi‑Fi"
607+msgstr "Cysylltu â diwifr"
608+
609+#: qml/Wizard/Pages/40-wifi.qml:168
610+msgid "Available networks…"
611+msgstr "Rhwydweithiau sydd ar gael..."
612+
613+#: qml/Wizard/Pages/40-wifi.qml:169
614+msgid "No available networks."
615+msgstr "Dim rhwydweithiau ar gael"
616+
617+#: qml/Wizard/Pages/50-location.qml:27
618+msgid "Location"
619+msgstr "Lleoliad"
620+
621+#: qml/Wizard/Pages/50-location.qml:62
622+msgid "Let the phone detect your location:"
623+msgstr "Caniatáu'r ffôn i ganfod eich lleoliad:"
624+
625+#: qml/Wizard/Pages/50-location.qml:69
626+msgid "Using GPS only (less accurate)"
627+msgstr "Defnyddio GPS yn unig (llai cywir)"
628+
629+#: qml/Wizard/Pages/50-location.qml:86
630+msgid "Using GPS, anonymized Wi-Fi and cellular network info (recommended)"
631+msgstr ""
632+"Defnyddio GPS, diwifr dienw a gwybodaeth rhwydwaith cellog (argymellir hwn)"
633+
634+#. Translators: HERE is a trademark for Nokia's location service, you probably shouldn't translate it
635+#: qml/Wizard/Pages/50-location.qml:103
636+msgid ""
637+"By selecting this option you agree to the Nokia HERE <a href='#'>terms and "
638+"conditions</a>."
639+msgstr ""
640+"Drwy ddewis yr opsiwn yma rydych yn cytuno i <a href='#'>dermau ac "
641+"amodau</a> Nokia HERE."
642+
643+#: qml/Wizard/Pages/50-location.qml:112
644+msgid "Not at all"
645+msgstr "Dim o gwbl"
646+
647+#: qml/Wizard/Pages/50-location.qml:124
648+msgid "You can change your mind later in <b>System Settings</b>."
649+msgstr "Gallwch newid hwn eto yn <b>Gosodiadau System</b>."
650+
651+#: qml/Wizard/Pages/60-reporting.qml:24
652+msgid "Improving your experience"
653+msgstr "Gwella eich profiad"
654+
655+#: qml/Wizard/Pages/60-reporting.qml:36
656+msgid ""
657+"Your phone is set up to automatically report errors to Canonical and its "
658+"partners, the makers of the operating system."
659+msgstr ""
660+"Cyfluniwyd eich ffôn i adrodd gwallau i Canonical a'i bartneriaid, "
661+"gwneuthurwyr y system weithredu."
662+
663+#: qml/Wizard/Pages/60-reporting.qml:43
664+msgid ""
665+"This can be disabled in <b>System Settings</b> under <b>Security &amp; "
666+"Privacy</b>"
667+msgstr ""
668+"Gellir analluogi hwn yn <b>Gosodiadau System</b> dan <b>Diogelwch a "
669+"Phreifatwydd</b>"
670+
671+#: qml/Wizard/Pages/80-finished.qml:24
672+msgid "All done"
673+msgstr "Wedi gorffen"
674+
675+#: qml/Wizard/Pages/80-finished.qml:39
676+msgid "Nice work!"
677+msgstr "Da iawn!"
678+
679+#: qml/Wizard/Pages/80-finished.qml:46
680+msgid "Your phone is now ready to use."
681+msgstr "Mae eich ffôn yn barod i'w ddefnyddio"
682+
683+#: qml/Wizard/Pages/80-finished.qml:53
684+msgid "Finish"
685+msgstr "Gorffen"
686+
687+#: qml/Wizard/Pages/here-terms.qml:27
688+msgid "Terms & Conditions"
689+msgstr "Amodau a Thelerau"
690+
691+#: qml/Wizard/Pages/passwd-confirm.qml:49
692+msgid "Confirm passphrase"
693+msgstr "Cadarnhau cyfrinair"
694+
695+#: qml/Wizard/Pages/passwd-confirm.qml:50
696+msgid "Confirm passcode"
697+msgstr "Cadarnhau cod cyfrinachol"
698+
699+#: qml/Wizard/Pages/passwd-confirm.qml:53
700+#: qml/Wizard/Pages/passwd-confirm.qml:54
701+msgid "Please try again."
702+msgstr "Rhowch gynnig arall arni"
703+
704+#: qml/Wizard/Pages/passwd-set.qml:61
705+msgid "Choose your passcode"
706+msgstr "Dewiswch eich cod cyfrinachol"
707+
708+#: qml/Wizard/Pages/passwd-set.qml:68
709+msgid "Passphrase must be 4 characters long"
710+msgstr "Dylai cyfrinair fod yn bedwar nod"
711+
712+#. Translators: This is the arrow for "Back" buttons
713+#: qml/Wizard/StackButton.qml:39
714+#, qt-format
715+msgid "〈 %1"
716+msgstr "〈 %1"
717+
718+#. Translators: This is the arrow for "Forward" buttons
719+#: qml/Wizard/StackButton.qml:42
720+#, qt-format
721+msgid "%1 〉"
722+msgstr "%1 〉"
po/gl.po+4−4
@@ -8,15 +8,15 @@ msgstr ""
88"Project-Id-Version: unity\n"
99"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
1010"POT-Creation-Date: 2015-07-17 10:10+0200\n"
11−"PO-Revision-Date: 2015-07-27 02:51+0000\n"
11+"PO-Revision-Date: 2015-08-01 03:25+0000\n"
1212"Last-Translator: Marcos Lans <Unknown>\n"
1313"Language-Team: Galician <gl@li.org>\n"
1414"MIME-Version: 1.0\n"
1515"Content-Type: text/plain; charset=UTF-8\n"
1616"Content-Transfer-Encoding: 8bit\n"
1717"Plural-Forms: nplurals=2; plural=n != 1;\n"
18−"X-Launchpad-Export-Date: 2015-07-28 05:40+0000\n"
19−"X-Generator: Launchpad (build 17628)\n"
18+"X-Launchpad-Export-Date: 2015-08-04 05:41+0000\n"
19+"X-Generator: Launchpad (build 17656)\n"
2020
2121#: plugins/LightDM/Greeter.cpp:112
2222msgid "Password: "
@@ -380,7 +380,7 @@ msgstr "Mostrar contrasinal"
380380
381381#: qml/Panel/ActiveCallHint.qml:79
382382msgid "Tap to return to call..."
383−msgstr "Toque para volver a chamar..."
383+msgstr "Toque para volver á chamada..."
384384
385385#: qml/Panel/ActiveCallHint.qml:92
386386msgid "Conference"
po/hu.po+4−4
@@ -8,15 +8,15 @@ msgstr ""
88"Project-Id-Version: unity\n"
99"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
1010"POT-Creation-Date: 2015-07-17 10:10+0200\n"
11−"PO-Revision-Date: 2015-07-04 11:42+0000\n"
11+"PO-Revision-Date: 2015-08-01 06:47+0000\n"
1212"Last-Translator: Richard Somlói <ricsipontaz@gmail.com>\n"
1313"Language-Team: Hungarian <hu@li.org>\n"
1414"MIME-Version: 1.0\n"
1515"Content-Type: text/plain; charset=UTF-8\n"
1616"Content-Transfer-Encoding: 8bit\n"
1717"Plural-Forms: nplurals=2; plural=n != 1;\n"
18−"X-Launchpad-Export-Date: 2015-07-25 05:39+0000\n"
19−"X-Generator: Launchpad (build 17628)\n"
18+"X-Launchpad-Export-Date: 2015-08-04 05:41+0000\n"
19+"X-Generator: Launchpad (build 17656)\n"
2020
2121#: plugins/LightDM/Greeter.cpp:112
2222msgid "Password: "
@@ -370,7 +370,7 @@ msgstr "Elnézést, helytelen jelmondat"
370370#: qml/Greeter/NarrowView.qml:121
371371msgctxt "passphrase"
372372msgid "Please re-enter"
373−msgstr ""
373+msgstr "Próbálja újra"
374374
375375#: qml/Greeter/NarrowView.qml:122
376376msgid "Sorry, incorrect passcode"
po/pt.po+17−17
@@ -8,15 +8,15 @@ msgstr ""
88"Project-Id-Version: unity8\n"
99"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
1010"POT-Creation-Date: 2015-07-17 10:10+0200\n"
11−"PO-Revision-Date: 2015-07-16 12:23+0000\n"
12−"Last-Translator: João Fernandes <Unknown>\n"
11+"PO-Revision-Date: 2015-07-29 10:01+0000\n"
12+"Last-Translator: Ivo Xavier <ivofernandes12@gmail.com>\n"
1313"Language-Team: Portuguese <pt@li.org>\n"
1414"MIME-Version: 1.0\n"
1515"Content-Type: text/plain; charset=UTF-8\n"
1616"Content-Transfer-Encoding: 8bit\n"
1717"Plural-Forms: nplurals=2; plural=n != 1;\n"
18−"X-Launchpad-Export-Date: 2015-07-25 05:39+0000\n"
19−"X-Generator: Launchpad (build 17628)\n"
18+"X-Launchpad-Export-Date: 2015-08-04 05:41+0000\n"
19+"X-Generator: Launchpad (build 17656)\n"
2020
2121#: plugins/LightDM/Greeter.cpp:112
2222msgid "Password: "
@@ -44,7 +44,7 @@ msgstr "Ontem %l:%M %p"
4444#. en_US example: "%l:%M %p" --> "1:00 PM"
4545#: plugins/Utils/relativetimeformatter.cpp:164
4646msgid "%l:%M %p"
47−msgstr ""
47+msgstr "%l:%M %p"
4848
4949#. Translators, please edit/rearrange these strftime(3) tokens to suit your locale!
5050#. This format string is used for showing, on a 12-hour clock, events/appointments that happen tomorrow.
@@ -60,7 +60,7 @@ msgstr "Amanhã %l:%M %p"
6060#. en_US example: "%a\u2003%l:%M %p" --> "Fri 1:00 PM"
6161#: plugins/Utils/relativetimeformatter.cpp:181
6262msgid "%a %l:%M %p"
63−msgstr ""
63+msgstr "%a %l:%M %p"
6464
6565#. Translators, please edit/rearrange these strftime(3) tokens to suit your locale!
6666#. This format string is used for showing, on a 12-hour clock, times that happened before a week from now.
@@ -69,7 +69,7 @@ msgstr ""
6969#. en_GB example: "%a %b %d\u2003%l:%M %p" --> "Fri 31 Oct 1:00 PM"
7070#: plugins/Utils/relativetimeformatter.cpp:190
7171msgid "%a %d %b %l:%M %p"
72−msgstr ""
72+msgstr "%a %b %d %l:%M %p"
7373
7474#. Translators, please edit/rearrange these strftime(3) tokens to suit your locale!
7575#. This format string is used for showing, on a 24-hour clock, times that happen yesterday.
@@ -84,7 +84,7 @@ msgstr "Ontem %H:%M"
8484#. en_US example: "%H:%M" --> "13:00"
8585#: plugins/Utils/relativetimeformatter.cpp:208
8686msgid "%H:%M"
87−msgstr ""
87+msgstr "%H:%M"
8888
8989#. Translators, please edit/rearrange these strftime(3) tokens to suit your locale!
9090#. This format string is used for showing, on a 24-hour clock, events/appointments that happen tomorrow.
@@ -100,7 +100,7 @@ msgstr "Amanhã %H:%M"
100100#. en_US example: "%a\u2003%H:%M" --> "Fri 13:00"
101101#: plugins/Utils/relativetimeformatter.cpp:225
102102msgid "%a %H:%M"
103−msgstr ""
103+msgstr "%a %H:%M"
104104
105105#. Translators, please edit/rearrange these strftime(3) tokens to suit your locale!
106106#. This format string is used for showing, on a 24-hour clock, times that happened before a week from now.
@@ -109,7 +109,7 @@ msgstr ""
109109#. en_GB example: "%a %b %d\u2003%H:%M" --> "Fri 31 Oct 13:00"
110110#: plugins/Utils/relativetimeformatter.cpp:234
111111msgid "%a %d %b %H:%M"
112−msgstr ""
112+msgstr "%a %b %d %H:%M"
113113
114114#: qml/Components/DelayedLockscreen.qml:49
115115msgid "Device Locked"
@@ -374,7 +374,7 @@ msgstr "Palavra-passe incorreta"
374374#: qml/Greeter/NarrowView.qml:121
375375msgctxt "passphrase"
376376msgid "Please re-enter"
377−msgstr ""
377+msgstr "Por favor, volte a introduzir"
378378
379379#: qml/Greeter/NarrowView.qml:122
380380msgid "Sorry, incorrect passcode"
@@ -407,25 +407,25 @@ msgstr[1] "%1 horas"
407407#, qt-format
408408msgid "%1 minute"
409409msgid_plural "%1 minutes"
410−msgstr[0] ""
411−msgstr[1] ""
410+msgstr[0] "%1 minuto"
411+msgstr[1] "%1 minutos"
412412
413413#: qml/Panel/Indicators/MenuItemFactory.qml:777
414414#, qt-format
415415msgid "%1 second"
416416msgid_plural "%1 seconds"
417−msgstr[0] ""
418−msgstr[1] ""
417+msgstr[0] "%1 segundo"
418+msgstr[1] "%1 segundos"
419419
420420#: qml/Panel/Indicators/MenuItemFactory.qml:780
421421msgid "0 seconds"
422−msgstr ""
422+msgstr "0 segundos"
423423
424424#. Translators: String like "1 hour, 2 minutes, 3 seconds remaining"
425425#: qml/Panel/Indicators/MenuItemFactory.qml:782
426426#, qt-format
427427msgid "%1 remaining"
428−msgstr ""
428+msgstr "Falta %1"
429429
430430#: qml/Panel/Indicators/MenuItemFactory.qml:788
431431msgid "In queue…"
po/pt_BR.po+12−12
@@ -8,15 +8,15 @@ msgstr ""
88"Project-Id-Version: unity\n"
99"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
1010"POT-Creation-Date: 2015-07-17 10:10+0200\n"
11−"PO-Revision-Date: 2015-05-04 21:50+0000\n"
12−"Last-Translator: Tiago Hillebrandt <tiagohillebrandt@gmail.com>\n"
11+"PO-Revision-Date: 2015-08-05 04:40+0000\n"
12+"Last-Translator: Julio Alexander Sieg <julio.sieg@gmail.com>\n"
1313"Language-Team: Brazilian Portuguese <pt_BR@li.org>\n"
1414"MIME-Version: 1.0\n"
1515"Content-Type: text/plain; charset=UTF-8\n"
1616"Content-Transfer-Encoding: 8bit\n"
1717"Plural-Forms: nplurals=2; plural=n > 1;\n"
18−"X-Launchpad-Export-Date: 2015-07-25 05:39+0000\n"
19−"X-Generator: Launchpad (build 17628)\n"
18+"X-Launchpad-Export-Date: 2015-08-06 05:46+0000\n"
19+"X-Generator: Launchpad (build 17656)\n"
2020
2121#: plugins/LightDM/Greeter.cpp:112
2222msgid "Password: "
@@ -398,32 +398,32 @@ msgstr "Nada está tocando"
398398#, qt-format
399399msgid "%1 hour"
400400msgid_plural "%1 hours"
401−msgstr[0] ""
402−msgstr[1] ""
401+msgstr[0] "%1 hora"
402+msgstr[1] "%1 horas"
403403
404404#: qml/Panel/Indicators/MenuItemFactory.qml:772
405405#, qt-format
406406msgid "%1 minute"
407407msgid_plural "%1 minutes"
408−msgstr[0] ""
409−msgstr[1] ""
408+msgstr[0] "%1 minuto"
409+msgstr[1] "%1 minutos"
410410
411411#: qml/Panel/Indicators/MenuItemFactory.qml:777
412412#, qt-format
413413msgid "%1 second"
414414msgid_plural "%1 seconds"
415−msgstr[0] ""
416−msgstr[1] ""
415+msgstr[0] "%1 segundo"
416+msgstr[1] "%1 segundos"
417417
418418#: qml/Panel/Indicators/MenuItemFactory.qml:780
419419msgid "0 seconds"
420−msgstr ""
420+msgstr "0 segundo"
421421
422422#. Translators: String like "1 hour, 2 minutes, 3 seconds remaining"
423423#: qml/Panel/Indicators/MenuItemFactory.qml:782
424424#, qt-format
425425msgid "%1 remaining"
426−msgstr ""
426+msgstr "%1 restantes"
427427
428428#: qml/Panel/Indicators/MenuItemFactory.qml:788
429429msgid "In queue…"
po/zh_TW.po+17−20
@@ -8,15 +8,15 @@ msgstr ""
88"Project-Id-Version: unity\n"
99"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
1010"POT-Creation-Date: 2015-07-17 10:10+0200\n"
11−"PO-Revision-Date: 2015-03-15 06:06+0000\n"
12−"Last-Translator: Cheng-Chia Tseng <pswo10680@gmail.com>\n"
11+"PO-Revision-Date: 2015-07-31 09:29+0000\n"
12+"Last-Translator: Po-Hsu Lin <po-hsu.lin@canonical.com>\n"
1313"Language-Team: Chinese (Traditional) <zh_TW@li.org>\n"
1414"MIME-Version: 1.0\n"
1515"Content-Type: text/plain; charset=UTF-8\n"
1616"Content-Transfer-Encoding: 8bit\n"
1717"Plural-Forms: nplurals=1; plural=0;\n"
18−"X-Launchpad-Export-Date: 2015-07-25 05:39+0000\n"
19−"X-Generator: Launchpad (build 17628)\n"
18+"X-Launchpad-Export-Date: 2015-08-04 05:41+0000\n"
19+"X-Generator: Launchpad (build 17656)\n"
2020"Language: zh_TW\n"
2121
2222#: plugins/LightDM/Greeter.cpp:112
@@ -38,14 +38,14 @@ msgstr "取消捷徑釘選"
3838#. en_US example: "Yesterday\u2003%l:%M %p" --> "Yesterday 1:00 PM"
3939#: plugins/Utils/relativetimeformatter.cpp:157
4040msgid "Yesterday %l:%M %p"
41−msgstr ""
41+msgstr "昨日 %l:%M %p"
4242
4343#. Translators, please edit/rearrange these strftime(3) tokens to suit your locale!
4444#. This format string is used for showing, on a 12-hour clock, times that happened today.
4545#. en_US example: "%l:%M %p" --> "1:00 PM"
4646#: plugins/Utils/relativetimeformatter.cpp:164
4747msgid "%l:%M %p"
48−msgstr ""
48+msgstr "%l:%M %p"
4949
5050#. Translators, please edit/rearrange these strftime(3) tokens to suit your locale!
5151#. This format string is used for showing, on a 12-hour clock, events/appointments that happen tomorrow.
@@ -53,7 +53,7 @@ msgstr ""
5353#. en_US example: "Tomorrow\u2003%l:%M %p" --> "Tomorrow 1:00 PM"
5454#: plugins/Utils/relativetimeformatter.cpp:172
5555msgid "Tomorrow %l:%M %p"
56−msgstr ""
56+msgstr "明日 %l:%M %p"
5757
5858#. Translators, please edit/rearrange these strftime(3) tokens to suit your locale!
5959#. This format string is used for showing, on a 12-hour clock, times that happened in the last week.
@@ -61,7 +61,7 @@ msgstr ""
6161#. en_US example: "%a\u2003%l:%M %p" --> "Fri 1:00 PM"
6262#: plugins/Utils/relativetimeformatter.cpp:181
6363msgid "%a %l:%M %p"
64−msgstr ""
64+msgstr "%a %l:%M %p"
6565
6666#. Translators, please edit/rearrange these strftime(3) tokens to suit your locale!
6767#. This format string is used for showing, on a 12-hour clock, times that happened before a week from now.
@@ -78,14 +78,14 @@ msgstr ""
7878#. en_US example: "Yesterday\u2003%l:%M %p" --> "Yesterday 13:00"
7979#: plugins/Utils/relativetimeformatter.cpp:201
8080msgid "Yesterday %H:%M"
81−msgstr ""
81+msgstr "昨日 %H:%M"
8282
8383#. Translators, please edit/rearrange these strftime(3) tokens to suit your locale!
8484#. This format string is used for showing, on a 24-hour clock, times that happened today.
8585#. en_US example: "%H:%M" --> "13:00"
8686#: plugins/Utils/relativetimeformatter.cpp:208
8787msgid "%H:%M"
88−msgstr ""
88+msgstr "%H:%M"
8989
9090#. Translators, please edit/rearrange these strftime(3) tokens to suit your locale!
9191#. This format string is used for showing, on a 24-hour clock, events/appointments that happen tomorrow.
@@ -93,7 +93,7 @@ msgstr ""
9393#. en_US example: "Tomorrow\u2003%l:%M %p" --> "Tomorrow 13:00"
9494#: plugins/Utils/relativetimeformatter.cpp:216
9595msgid "Tomorrow %H:%M"
96−msgstr ""
96+msgstr "明日 %H:%M"
9797
9898#. Translators, please edit/rearrange these strftime(3) tokens to suit your locale!
9999#. This format string is used for showing, on a 24-hour clock, times that happened in the last week.
@@ -364,7 +364,7 @@ msgstr "抱歉,密語不對"
364364#: qml/Greeter/NarrowView.qml:121
365365msgctxt "passphrase"
366366msgid "Please re-enter"
367−msgstr ""
367+msgstr "請再輸入"
368368
369369#: qml/Greeter/NarrowView.qml:122
370370msgid "Sorry, incorrect passcode"
@@ -390,32 +390,29 @@ msgstr "無影音播放中"
390390#, qt-format
391391msgid "%1 hour"
392392msgid_plural "%1 hours"
393−msgstr[0] ""
394−msgstr[1] ""
393+msgstr[0] "%1 小時"
395394
396395#: qml/Panel/Indicators/MenuItemFactory.qml:772
397396#, qt-format
398397msgid "%1 minute"
399398msgid_plural "%1 minutes"
400−msgstr[0] ""
401−msgstr[1] ""
399+msgstr[0] "%1 分鐘"
402400
403401#: qml/Panel/Indicators/MenuItemFactory.qml:777
404402#, qt-format
405403msgid "%1 second"
406404msgid_plural "%1 seconds"
407−msgstr[0] ""
408−msgstr[1] ""
405+msgstr[0] "%1 秒鐘"
409406
410407#: qml/Panel/Indicators/MenuItemFactory.qml:780
411408msgid "0 seconds"
412−msgstr ""
409+msgstr "0 秒鐘"
413410
414411#. Translators: String like "1 hour, 2 minutes, 3 seconds remaining"
415412#: qml/Panel/Indicators/MenuItemFactory.qml:782
416413#, qt-format
417414msgid "%1 remaining"
418−msgstr ""
415+msgstr "還剩下 %1"
419416
420417#: qml/Panel/Indicators/MenuItemFactory.qml:788
421418msgid "In queue…"
qml/Components/PhysicalKeysMapper.qml+24
@@ -41,14 +41,23 @@ Item {
4141 signal volumeUpTriggered;
4242 signal screenshotTriggered;
4343
44+ readonly property bool altTabPressed: d.altTabPressed
45+
4446 property int powerKeyLongPressTime: 2000
4547
48+ // For testing. If running windowed (e.g. tryShell), Alt+Tab is taken by the
49+ // running desktop, set this to true to use Ctrl+Tab instead.
50+ property bool controlInsteadOfAlt: false
51+
4652 QtObject {
4753 id: d
4854
4955 property bool volumeDownKeyPressed: false
5056 property bool volumeUpKeyPressed: false
5157 property bool ignoreVolumeEvents: false
58+
59+ property bool altPressed: false
60+ property bool altTabPressed: false
5261 }
5362
5463 Timer {
@@ -93,6 +102,15 @@ Item {
93102 }
94103 d.volumeUpKeyPressed = true;
95104 }
105+ } else if (event.key == Qt.Key_Alt || (root.controlInsteadOfAlt && event.key == Qt.Key_Control)) {
106+ d.altPressed = true;
107+ } else if (event.key == Qt.Key_Tab) {
108+ if (d.altPressed && !d.altTabPressed) {
109+ d.altTabPressed = true;
110+ event.accepted = true;
111+ }
112+ } else if (event.key == Qt.Key_Print) {
113+ root.screenshotTriggered();
96114 }
97115 }
98116
@@ -108,6 +126,12 @@ Item {
108126 if (!d.ignoreVolumeEvents) root.volumeUpTriggered();
109127 d.volumeUpKeyPressed = false;
110128 if (!d.volumeDownKeyPressed) d.ignoreVolumeEvents = false;
129+ } else if (event.key == Qt.Key_Alt || (root.controlInsteadOfAlt && event.key == Qt.Key_Control)) {
130+ d.altPressed = false;
131+ if (d.altTabPressed) {
132+ d.altTabPressed = false;
133+ event.accepted = true;
134+ }
111135 }
112136 }
113137}
qml/Components/ScreenGrabber.qml+1−5
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2014 Canonical, Ltd.
2+ * Copyright (C) 2014-2015 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -20,7 +20,6 @@ import ScreenGrabber 0.1
2020
2121Rectangle {
2222 id: root
23− enabled: true
2423 visible: false
2524 color: "white"
2625 anchors.fill: parent
@@ -38,9 +37,6 @@ Rectangle {
3837 }
3938
4039 function capture() {
41− if (!enabled)
42− return;
43−
4440 visible = true;
4541 shutterSound.stop();
4642 shutterSound.play();
qml/Components/Unity8Settings.qmlnew file+50
@@ -0,0 +1,50 @@
1+/*
2+ * Copyright (C) 2015 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.0
18+import GSettings 1.0
19+
20+QtObject {
21+ id: root
22+
23+ property string usageMode: "Staged"
24+
25+ // FIXME: Works around a bug where if we change a Loader's source in response to a GSettings
26+ // property change in the same event loop, the Loader's previously loaded component does not
27+ // get destroyed and its bindings continue to operate!
28+ //
29+ // Shouldn't be needed after
30+ // https://code.launchpad.net/~lukas-kde/gsettings-qt/queued-processing/+merge/259883 gets
31+ // merged.
32+ property var timer: Timer {
33+ interval: 1
34+ onTriggered: { root.usageMode = root.wrapped.usageMode; }
35+ }
36+ property var wrappedConnections: Connections {
37+ target: root.wrapped
38+ ignoreUnknownSignals: true // don't spam us
39+ onUsageModeChanged: { root.timer.start(); }
40+ }
41+ property var wrapped: GSettings {
42+ schema.id: "com.canonical.Unity8"
43+ Component.onCompleted: {
44+ // init the value. it's a dynamic prop, so we have to check first
45+ if (root.usageMode) {
46+ root.usageMode = root.wrapped.usageMode;
47+ }
48+ }
49+ }
50+}
qml/Dash/Dash.qml+1−1
@@ -41,7 +41,7 @@ Showable {
4141 onSetCurrentScopeRequested: {
4242 if (!isSwipe || !window.active || bottomEdgeController.progress != 0 || scopeItem.scope || dashContent.subPageShown) {
4343 if (bottomEdgeController.progress != 0 && window.active) animate = false;
44− dashContent.setCurrentScopeAtIndex(index, animate, isSwipe)
44+ dashContent.setCurrentScopeAtIndex(index, animate, true)
4545 backToDashContent()
4646 }
4747 }
qml/Dash/DashContent.qml+1
@@ -84,6 +84,7 @@ Item {
8484
8585 if (reset) {
8686 dashContentList.currentItem.item.positionAtBeginning()
87+ dashContentList.currentItem.item.resetSearch()
8788 }
8889 }
8990
qml/Dash/GenericScopeView.qml+5
@@ -67,6 +67,11 @@ FocusScope {
6767 subPageLoader.closeSubPage()
6868 }
6969
70+ function resetSearch() {
71+ if(pageHeaderLoader.item && showPageHeader)
72+ pageHeaderLoader.item.resetSearch()
73+ }
74+
7075 function itemClicked(index, result, item, itemModel, resultsModel, limitedCategoryItemCount, categoryId) {
7176 if (itemModel.uri.indexOf("scope://") === 0 || scope.id === "clickscope" || (scope.id === "videoaggregator" && categoryId === "myvideos-getstarted")) {
7277 // TODO Technically it is possible that calling activate() will make the scope emit
qml/Launcher/LauncherDelegate.qml+100−14
@@ -27,6 +27,8 @@ Item {
2727 property bool itemFocused: false
2828 property real maxAngle: 0
2929 property bool inverted: false
30+ property bool alerting: false
31+ readonly property alias wiggling: wiggleAnim.running
3032
3133 readonly property int effectiveHeight: Math.cos(angle * Math.PI / 180) * itemHeight
3234 readonly property real foldedHeight: Math.cos(maxAngle * Math.PI / 180) * itemHeight
@@ -39,6 +41,82 @@ Item {
3941 property real offset: 0
4042 property real itemOpacity: 1
4143 property real brightness: 0
44+ property double maxWiggleAngle: 5.0
45+
46+ QtObject {
47+ id: priv
48+
49+ readonly property int wiggleDuration: UbuntuAnimation.SnapDuration
50+ property real wiggleAngle: 0
51+ }
52+
53+ SequentialAnimation {
54+ id: wiggleAnim
55+
56+ running: alerting
57+ loops: 1
58+ alwaysRunToEnd: true
59+
60+ NumberAnimation {
61+ target: priv
62+ property: "wiggleAngle"
63+ from: 0
64+ to: maxWiggleAngle
65+ duration: priv.wiggleDuration
66+ easing.type: Easing.InQuad
67+ }
68+
69+ NumberAnimation {
70+ target: priv
71+ property: "wiggleAngle"
72+ from: maxWiggleAngle
73+ to: -maxWiggleAngle
74+ duration: priv.wiggleDuration
75+ easing.type: Easing.InOutQuad
76+ }
77+
78+ NumberAnimation {
79+ target: priv
80+ property: "wiggleAngle"
81+ from: -maxWiggleAngle
82+ to: maxWiggleAngle
83+ duration: priv.wiggleDuration
84+ easing.type: Easing.InOutQuad
85+ }
86+
87+ NumberAnimation {
88+ target: priv
89+ property: "wiggleAngle"
90+ from: maxWiggleAngle
91+ to: -maxWiggleAngle
92+ duration: priv.wiggleDuration
93+ easing.type: Easing.InOutQuad
94+ }
95+
96+ NumberAnimation {
97+ target: priv
98+ property: "wiggleAngle"
99+ from: -maxWiggleAngle
100+ to: maxWiggleAngle
101+ duration: priv.wiggleDuration
102+ easing.type: Easing.InOutQuad
103+ }
104+
105+ NumberAnimation {
106+ target: priv
107+ property: "wiggleAngle"
108+ from: maxWiggleAngle
109+ to: 0
110+ duration: priv.wiggleDuration
111+ easing.type: Easing.OutQuad
112+ }
113+
114+ UbuntuNumberAnimation {
115+ target: root
116+ property: "alerting"
117+ to: 0
118+ }
119+ }
42120
43121 Item {
44122 id: iconItem
@@ -74,7 +152,7 @@ Item {
74152 objectName: "countEmblem"
75153 anchors {
76154 right: parent.right
77− top: parent.top
155+ bottom: parent.bottom
78156 margins: units.dp(3)
79157 }
80158 width: Math.min(root.itemWidth, Math.max(units.gu(2), countLabel.implicitWidth + units.gu(1)))
@@ -99,23 +177,21 @@ Item {
99177 }
100178 }
101179
102− BorderImage {
180+ UbuntuShape {
103181 id: progressOverlay
104182 objectName: "progressOverlay"
183+
105184 anchors {
106185 left: iconItem.left
107186 right: iconItem.right
108− bottom: iconItem.bottom
109− leftMargin: units.gu(1)
110− rightMargin: units.gu(1)
111− bottomMargin: units.gu(1)
187+ verticalCenter: parent.verticalCenter
188+ leftMargin: units.gu(1.5)
189+ rightMargin: units.gu(1.5)
112190 }
113− height: units.gu(1.5)
191+ height: units.gu(1)
114192 visible: root.progress > -1
115− source: "graphics/progressbar-trough.sci"
116−
117− // For fill calculation we need to remove the 2 units of border defined in .sci file
118− property int adjustedWidth: width - units.gu(2)
193+ color: UbuntuColors.darkGrey
194+ borderSource: "none"
119195
120196 Item {
121197 anchors {
@@ -123,20 +199,22 @@ Item {
123199 top: parent.top
124200 bottom: parent.bottom
125201 }
126− width: Math.min(100, root.progress) / 100 * parent.adjustedWidth + units.gu(1)
202+ width: Math.min(100, root.progress) / 100 * parent.width
127203 clip: true
128204
129− BorderImage {
205+ UbuntuShape {
130206 anchors {
131207 left: parent.left
132208 top: parent.top
133209 bottom: parent.bottom
134210 }
211+ color: "white"
212+ borderSource: "none"
135213 width: progressOverlay.width
136− source: "graphics/progressbar-fill.sci"
137214 }
138215 }
139216 }
217+
140218 Image {
141219 objectName: "focusedHighlight"
142220 anchors {
@@ -166,6 +244,14 @@ Item {
166244 }
167245
168246 transform: [
247+ // The rotation about the icon's center/z-axis for the wiggle
248+ // needs to happen here too, because there's no other way to
249+ // align the wiggle with the icon-folding otherwise
250+ Rotation {
251+ axis { x: 0; y: 0; z: 1 }
252+ origin { x: iconItem.width / 2; y: iconItem.height / 2; z: 0 }
253+ angle: priv.wiggleAngle
254+ },
169255 // Rotating 3 times at top/bottom because that increases the perspective.
170256 // This is a hack, but as QML does not support real 3D coordinates
171257 // getting a higher perspective can only be done by a hack. This is the most
qml/Launcher/LauncherPanel.qml+93−13
@@ -86,6 +86,7 @@ Rectangle {
8686 height: parent.height - dashItem.height - parent.spacing*2
8787
8888 Item {
89+ id: launcherListViewItem
8990 anchors.fill: parent
9091 clip: true
9192
@@ -109,6 +110,9 @@ Rectangle {
109110 preferredHighlightBegin: (height - itemHeight) / 2
110111 preferredHighlightEnd: (height + itemHeight) / 2
111112
113+ // for the single peeking icon, when alert-state is set on delegate
114+ property int peekingIndex: -1
115+
112116 // The size of the area the ListView is extended to make sure items are not
113117 // destroyed when dragging them outside the list. This needs to be at least
114118 // itemHeight to prevent folded items from disappearing and DragArea limits
@@ -159,6 +163,17 @@ Rectangle {
159163 to: launcherListView.contentHeight - launcherListView.height + launcherListView.originY - launcherListView.topMargin
160164 }
161165
166+ UbuntuNumberAnimation {
167+ id: moveAnimation
168+ target: launcherListView
169+ property: "contentY"
170+ function moveTo(contentY) {
171+ from = launcherListView.contentY;
172+ to = contentY;
173+ start();
174+ }
175+ }
176+
162177 displaced: Transition {
163178 NumberAnimation { properties: "x,y"; duration: UbuntuAnimation.FastDuration; easing: UbuntuAnimation.StandardEasing }
164179 }
@@ -180,10 +195,67 @@ Rectangle {
180195 progress: model.progress
181196 itemFocused: model.focused
182197 inverted: root.inverted
198+ alerting: model.alerting
183199 z: -Math.abs(offset)
184200 maxAngle: 55
185201 property bool dragging: false
186202
203+ SequentialAnimation {
204+ id: peekingAnimation
205+
206+ // revealing
207+ PropertyAction { target: root; property: "visible"; value: (launcher.visibleWidth === 0) ? 1 : 0 }
208+ PropertyAction { target: launcherListViewItem; property: "clip"; value: 0 }
209+
210+ UbuntuNumberAnimation {
211+ target: launcherDelegate
212+ alwaysRunToEnd: true
213+ loops: 1
214+ properties: "x"
215+ to: (units.gu(.5) + launcherListView.width * .5) * (root.inverted ? -1 : 1)
216+ duration: UbuntuAnimation.BriskDuration
217+ }
218+
219+ // hiding
220+ UbuntuNumberAnimation {
221+ target: launcherDelegate
222+ alwaysRunToEnd: true
223+ loops: 1
224+ properties: "x"
225+ to: 0
226+ duration: UbuntuAnimation.BriskDuration
227+ }
228+
229+ PropertyAction { target: launcherListViewItem; property: "clip"; value: 1 }
230+ PropertyAction { target: root; property: "visible"; value: (launcher.visibleWidth === 0) ? 0 : 1 }
231+ }
232+
233+ onAlertingChanged: {
234+ if(alerting) {
235+ if (!dragging && (launcherListView.peekingIndex === -1 || launcher.visibleWidth > 0)) {
236+ var itemPosition = index * launcherListView.itemHeight;
237+ var height = launcherListView.height - launcherListView.topMargin - launcherListView.bottomMargin
238+ var distanceToEnd = index == 0 || index == launcherListView.count - 1 ? 0 : launcherListView.itemHeight
239+ if (itemPosition + launcherListView.itemHeight + distanceToEnd > launcherListView.contentY + launcherListView.topMargin + height) {
240+ moveAnimation.moveTo(itemPosition + launcherListView.itemHeight - launcherListView.topMargin - height + distanceToEnd);
241+ } else if (itemPosition - distanceToEnd < launcherListView.contentY + launcherListView.topMargin) {
242+ moveAnimation.moveTo(itemPosition - distanceToEnd - launcherListView.topMargin);
243+ }
244+ if (!dragging && launcher.state !== "visible") {
245+ peekingAnimation.start()
246+ }
247+ }
248+
249+ if (launcherListView.peekingIndex === -1) {
250+ launcherListView.peekingIndex = index
251+ }
252+ } else {
253+ if (launcherListView.peekingIndex === index) {
254+ launcherListView.peekingIndex = -1
255+ }
256+ }
257+ }
258+
187259 ThinDivider {
188260 id: dropIndicator
189261 objectName: "dropIndicator"
@@ -286,6 +358,7 @@ Rectangle {
286358 MouseArea {
287359 id: dndArea
288360 objectName: "dndArea"
361+ acceptedButtons: Qt.LeftButton | Qt.RightButton
289362 anchors {
290363 fill: parent
291364 topMargin: launcherListView.topMargin
@@ -297,7 +370,7 @@ Rectangle {
297370 property int draggedIndex: -1
298371 property var selectedItem
299372 property bool preDragging: false
300− property bool dragging: selectedItem !== undefined && selectedItem !== null && selectedItem.dragging
373+ property bool dragging: !!selectedItem && selectedItem.dragging
301374 property bool postDragging: false
302375 property int startX
303376 property int startY
@@ -315,6 +388,15 @@ Rectangle {
315388 return;
316389 }
317390
391+ if (mouse.button & Qt.RightButton) { // context menu
392+ // Opening QuickList
393+ quickList.item = clickedItem;
394+ quickList.model = launcherListView.model.get(index).quickList;
395+ quickList.appId = launcherListView.model.get(index).appId;
396+ quickList.state = "open";
397+ return
398+ }
399+
318400 // First/last item do the scrolling at more than 12 degrees
319401 if (index == 0 || index == launcherListView.count - 1) {
320402 if (clickedItem.angle > 12) {
@@ -508,7 +590,7 @@ Rectangle {
508590 id: quickListShape
509591 objectName: "quickListShape"
510592 anchors.fill: quickList
511− opacity: quickList.state === "open" ? 0.96 : 0
593+ opacity: quickList.state === "open" ? 0.8 : 0
512594 visible: opacity > 0
513595 rotation: root.rotation
514596
@@ -520,15 +602,15 @@ Rectangle {
520602
521603 Image {
522604 anchors {
523− left: parent.left
524− leftMargin: quickList.item ? (quickList.item.width - units.gu(1)) / 2 - width / 2 : 0
605+ right: parent.left
606+ rightMargin: -units.dp(4)
525607 verticalCenter: parent.verticalCenter
526− verticalCenterOffset: (parent.height / 2 + units.dp(3)) * (quickList.offset > 0 ? 1 : -1) * (root.inverted ? 1 : -1)
608+ verticalCenterOffset: -quickList.offset * (root.inverted ? -1 : 1)
527609 }
528610 height: units.gu(1)
529611 width: units.gu(2)
530612 source: "graphics/quicklist_tooltip.png"
531− rotation: (quickList.offset > 0 ? 0 : 180) + (root.inverted ? 0 : 180)
613+ rotation: 90
532614 }
533615
534616 InverseMouseArea {
@@ -551,11 +633,11 @@ Rectangle {
551633 height: quickListColumn.height
552634 visible: quickListShape.visible
553635 anchors {
554− left: root.inverted ? undefined : parent.left
555− right: root.inverted ? parent.right : undefined
636+ left: root.inverted ? undefined : parent.right
637+ right: root.inverted ? parent.left : undefined
556638 margins: units.gu(1)
557639 }
558− y: itemCenter + offset
640+ y: itemCenter - (height / 2) + offset
559641 rotation: root.rotation
560642
561643 property var model
@@ -564,10 +646,8 @@ Rectangle {
564646
565647 // internal
566648 property int itemCenter: item ? root.mapFromItem(quickList.item).y + (item.height / 2) : units.gu(1)
567− property int offset: item ? itemCenter + (item.height/2) + height + units.gu(1) > parent.height ?
568− -(item.height/2) - height - units.gu(.5) :
569− (item.height/2) + units.gu(.5) :
570− 0
649+ property int offset: itemCenter + (height/2) + units.gu(1) > parent.height ? -itemCenter - (height/2) - units.gu(1) + parent.height :
650+ itemCenter - (height/2) < units.gu(1) ? (height/2) - itemCenter + units.gu(1) : 0
571651
572652 Column {
573653 id: quickListColumn
qml/Launcher/graphics/progressbar-fill@30.pngdeleted

Binary file not shown.

qml/Launcher/graphics/progressbar-fill@30.scideleted−5
@@ -1,5 +0,0 @@
1−border.left: 36
2−border.top: 16
3−border.bottom: 28
4−border.right: 36
5−source: progressbar-fill@30.png
qml/Launcher/graphics/progressbar-trough@30.pngdeleted

Binary file not shown.

qml/Launcher/graphics/progressbar-trough@30.scideleted−5
@@ -1,5 +0,0 @@
1−border.left: 36
2−border.top: 16
3−border.bottom: 26
4−border.right: 36
5−source: progressbar-trough@30.png
qml/OrientedShell.qml+3−3
@@ -43,7 +43,7 @@ Rectangle {
4343
4444
4545 // to be overwritten by tests
46− property var usageModeSettings: GSettings { schema.id: "com.canonical.Unity8" }
46+ property var unity8Settings: Unity8Settings {}
4747 property var oskSettings: GSettings { schema.id: "com.canonical.keyboard.maliit" }
4848
4949 property int physicalOrientation: Screen.orientation
@@ -160,9 +160,9 @@ Rectangle {
160160 // TODO: Factor in the connected input devices (eg: physical keyboard, mouse, touchscreen),
161161 // what's the output device (eg: big TV, desktop monitor, phone display), etc.
162162 usageScenario: {
163− if (root.usageModeSettings.usageMode === "Windowed") {
163+ if (root.unity8Settings.usageMode === "Windowed") {
164164 return "desktop";
165− } else if (root.usageModeSettings.usageMode === "Staged") {
165+ } else if (root.unity8Settings.usageMode === "Staged") {
166166 if (deviceConfiguration.category === "phone") {
167167 return "phone";
168168 } else {
qml/Panel/IndicatorsMenu.qml+3−1
@@ -36,6 +36,7 @@ Showable {
3636 readonly property bool fullyClosed: unitProgress == 0
3737 property bool enableHint: true
3838 property bool contentEnabled: true
39+ property bool showOnClick: true
3940 property color panelColor: "black"
4041
4142 signal showTapped(point position)
@@ -68,6 +69,7 @@ Showable {
6869 // eater
6970 MouseArea {
7071 anchors.fill: parent
72+ hoverEnabled: true
7173 }
7274
7375 MenuContent {
@@ -168,7 +170,7 @@ Showable {
168170 anchors.left: parent.left
169171 anchors.right: parent.right
170172 height: minimizedPanelHeight
171− enabled: __showDragHandle.enabled
173+ enabled: __showDragHandle.enabled && showOnClick
172174 onClicked: {
173175 bar.selectItemAt(mouseX)
174176 root.show()
qml/Panel/Panel.qml+1
@@ -147,6 +147,7 @@ Item {
147147 return root.width
148148 }
149149 enableHint: !callHint.active && !fullscreenMode
150+ showOnClick: !callHint.visible
150151 panelColor: indicatorAreaBackground.color
151152
152153 onShowTapped: {
qml/Shell.qml+16−13
@@ -178,6 +178,7 @@ Item {
178178
179179 PhysicalKeysMapper {
180180 id: physicalKeysMapper
181+ objectName: "physicalKeysMapper"
181182
182183 onPowerKeyLongPressed: dialogs.showPowerDialog()
183184 onVolumeDownTriggered: volumeControl.volumeDown();
@@ -188,13 +189,6 @@ Item {
188189 ScreenGrabber {
189190 id: screenGrabber
190191 z: dialogs.z + 10
191− enabled: Powerd.status === Powerd.On
192− }
193−
194− Binding {
195− target: ApplicationManager
196− property: "forceDashActive"
197− value: launcher.shown || launcher.dashSwipe
198192 }
199193
200194 WindowKeysFilter {
@@ -352,6 +346,21 @@ Item {
352346 property: "beingResized"
353347 value: shell.beingResized
354348 }
349+ Binding {
350+ target: applicationsDisplayLoader.item
351+ property: "keepDashRunning"
352+ value: launcher.shown || launcher.dashSwipe
353+ }
354+ Binding {
355+ target: applicationsDisplayLoader.item
356+ property: "suspended"
357+ value: greeter.shown
358+ }
359+ Binding {
360+ target: applicationsDisplayLoader.item
361+ property: "altTabPressed"
362+ value: physicalKeysMapper.altTabPressed
363+ }
355364 }
356365
357366 Tutorial {
@@ -454,12 +463,6 @@ Item {
454463 }
455464
456465 onEmergencyCall: startLockedApp("dialer-app")
457−
458− Binding {
459− target: ApplicationManager
460− property: "suspended"
461− value: greeter.shown
462− }
463466 }
464467 }
465468
qml/Stages/ApplicationWindow.qml+3
@@ -135,6 +135,9 @@ FocusScope {
135135 onSurfaceChanged: {
136136 if (sessionContainer.surface) {
137137 surfaceInitTimer.start();
138+ if (ApplicationManager.focusedApplicationId == application.appId) {
139+ root.forceActiveFocus();
140+ }
138141 } else {
139142 d.surfaceInitialized = false;
140143 }
qml/Stages/DecoratedWindow.qml+72−8
@@ -23,21 +23,67 @@ import Unity.Application 0.1
2323FocusScope {
2424 id: root
2525
26+ property alias window: applicationWindow
2627 property alias application: applicationWindow.application
2728 property alias active: decoration.active
2829
30+ property bool decorationShown: true
31+ property bool highlightShown: false
32+ property real shadowOpacity: 1
33+
34+ property int windowWidth: width
35+ property int windowHeight: height
36+
2937 signal close();
3038 signal maximize();
3139 signal minimize();
3240
41+ state: "normal"
42+ states: [
43+ State {
44+ name: "normal"
45+ PropertyChanges {
46+ target: root
47+ width: windowWidth
48+ height: windowHeight
49+ }
50+ },
51+ State {
52+ name: "transformed"
53+ PropertyChanges {
54+ target: applicationWindow
55+ itemScale: Math.max(root.width / root.windowWidth, root.height / root.windowHeight)
56+ interactive: false
57+ }
58+ PropertyChanges {
59+ target: clipper
60+ clip: true
61+ }
62+ }
63+ ]
64+
3365 BorderImage {
3466 anchors {
3567 fill: root
3668 margins: -units.gu(2)
3769 }
3870 source: "graphics/dropshadow2gu.sci"
39− opacity: .3
40− Behavior on opacity { UbuntuNumberAnimation {} }
71+ opacity: root.shadowOpacity * .3
72+ }
73+
74+ Rectangle {
75+ id: selectionHighlight
76+ anchors.fill: parent
77+ anchors.margins: -units.gu(1)
78+ color: "white"
79+ opacity: highlightShown ? 0.15 : 0
80+ }
81+
82+ Rectangle {
83+ anchors { left: selectionHighlight.left; right: selectionHighlight.right; bottom: selectionHighlight.bottom; }
84+ height: units.dp(2)
85+ color: UbuntuColors.orange
86+ visible: root.highlightShown
4187 }
4288
4389 WindowDecoration {
@@ -49,14 +95,32 @@ FocusScope {
4995 onClose: root.close();
5096 onMaximize: root.maximize();
5197 onMinimize: root.minimize();
98+ visible: decorationShown
5299 }
53100
54− ApplicationWindow {
55− id: applicationWindow
56− objectName: application ? "appWindow_" + application.appId : "appWindow_null"
101+ Item {
102+ id: clipper
57103 anchors.fill: parent
58− anchors.topMargin: units.gu(3)
59− interactive: true
60− focus: true
104+
105+ ApplicationWindow {
106+ id: applicationWindow
107+ objectName: application ? "appWindow_" + application.appId : "appWindow_null"
108+ anchors.top: parent.top
109+ anchors.topMargin: root.decorationShown ? decoration.height : 0
110+ anchors.left: parent.left
111+ width: root.windowWidth
112+ height: root.windowHeight - (root.decorationShown ? decoration.height : 0)
113+ interactive: true
114+ focus: true
115+
116+ property real itemScale: 1
117+ transform: [
118+ Scale {
119+ origin.x: 0; origin.y: 0
120+ xScale: applicationWindow.itemScale
121+ yScale: applicationWindow.itemScale
122+ }
123+ ]
124+ }
61125 }
62126}
qml/Stages/DesktopStage.qml+511−57
@@ -17,13 +17,15 @@
1717 */
1818
1919import QtQuick 2.3
20+import QtQuick.Layouts 1.1
2021import Ubuntu.Components 1.3
2122import Unity.Application 0.1
2223import "../Components/PanelState"
24+import Utils 0.1
25+import Ubuntu.Gestures 0.1
2326
2427Rectangle {
2528 id: root
26−
2729 anchors.fill: parent
2830
2931 // Controls to be set from outside
@@ -37,6 +39,8 @@ Rectangle {
3739 property int shellPrimaryOrientation
3840 property int nativeOrientation
3941 property bool beingResized: false
42+ property bool keepDashRunning: true
43+ property bool suspended: false
4044
4145 // functions to be called from outside
4246 function updateFocusedAppOrientation() { /* TODO */ }
@@ -50,6 +54,7 @@ Rectangle {
5054 readonly property bool orientationChangesEnabled: false
5155
5256 property alias background: wallpaper.source
57+ property bool altTabPressed: false
5358
5459 CrossFadeImage {
5560 id: wallpaper
@@ -61,15 +66,18 @@ Rectangle {
6166 Connections {
6267 target: ApplicationManager
6368 onApplicationAdded: {
64− ApplicationManager.requestFocusApplication(ApplicationManager.get(ApplicationManager.count-1).appId)
69+ if (root.state == "altTab") {
70+ root.state = "";
71+ }
72+
73+ ApplicationManager.requestFocusApplication(appId)
6574 }
6675
6776 onFocusRequested: {
6877 var appIndex = priv.indexOf(appId);
6978 var appDelegate = appRepeater.itemAt(appIndex);
70− if (appDelegate.state === "minimized") {
71− appDelegate.state = "normal"
72− }
79+ appDelegate.minimized = false;
80+ appDelegate.focus = true;
7381 ApplicationManager.focusApplication(appId);
7482 }
7583 }
@@ -104,8 +112,8 @@ Rectangle {
104112 onClose: {
105113 ApplicationManager.stopApplication(ApplicationManager.focusedApplicationId)
106114 }
107− onMinimize: appRepeater.itemAt(0).state = "minimized"
108− onMaximize: appRepeater.itemAt(0).state = "normal"
115+ onMinimize: appRepeater.itemAt(0).minimize();
116+ onMaximize: appRepeater.itemAt(0).unmaximize();
109117 }
110118
111119 Binding {
@@ -114,67 +122,513 @@ Rectangle {
114122 value: priv.focusedAppDelegate !== null && priv.focusedAppDelegate.state === "maximized"
115123 }
116124
117− Repeater {
118− id: appRepeater
119− model: ApplicationManager
125+ Rectangle {
126+ id: spreadBackground
127+ anchors.fill: parent
128+ color: "#55000000"
129+ visible: false
130+ }
120131
121− delegate: FocusScope {
122− id: appDelegate
123− z: ApplicationManager.count - index
124− y: units.gu(3)
125− width: units.gu(60)
126− height: units.gu(50)
132+ FocusScope {
133+ id: appContainer
134+ objectName: "appContainer"
135+ anchors.fill: parent
127136
128− onFocusChanged: {
129− if (focus) {
130− ApplicationManager.requestFocusApplication(model.appId);
131− }
137+ Keys.onPressed: {
138+ switch (event.key) {
139+ case Qt.Key_Left:
140+ case Qt.Key_Backtab:
141+ selectPrevious(event.isAutoRepeat)
142+ break;
143+ case Qt.Key_Right:
144+ case Qt.Key_Tab:
145+ selectNext(event.isAutoRepeat)
146+ break;
147+ case Qt.Key_Escape:
148+ appRepeater.highlightedIndex = -1
149+ case Qt.Key_Enter:
150+ case Qt.Key_Return:
151+ case Qt.Key_Space:
152+ root.state = ""
153+ }
154+ }
155+
156+ function selectNext(isAutoRepeat) {
157+ if (isAutoRepeat && appRepeater.highlightedIndex >= ApplicationManager.count -1) {
158+ return; // AutoRepeat is not allowed to wrap around
159+ }
160+
161+ appRepeater.highlightedIndex = (appRepeater.highlightedIndex + 1) % ApplicationManager.count;
162+ var newContentX = ((spreadFlickable.contentWidth) / (ApplicationManager.count + 1)) * Math.max(0, Math.min(ApplicationManager.count - 5, appRepeater.highlightedIndex - 3));
163+ if (spreadFlickable.contentX < newContentX || appRepeater.highlightedIndex == 0) {
164+ spreadFlickable.snapTo(newContentX)
165+ }
166+ }
167+
168+ function selectPrevious(isAutoRepeat) {
169+ if (isAutoRepeat && appRepeater.highlightedIndex == 0) {
170+ return; // AutoRepeat is not allowed to wrap around
171+ }
172+
173+ var newIndex = appRepeater.highlightedIndex - 1 >= 0 ? appRepeater.highlightedIndex - 1 : ApplicationManager.count - 1;
174+ appRepeater.highlightedIndex = newIndex;
175+ var newContentX = ((spreadFlickable.contentWidth) / (ApplicationManager.count + 1)) * Math.max(0, Math.min(ApplicationManager.count - 5, appRepeater.highlightedIndex - 1));
176+ if (spreadFlickable.contentX > newContentX || newIndex == ApplicationManager.count -1) {
177+ spreadFlickable.snapTo(newContentX)
178+ }
179+ }
180+
181+ function focusSelected() {
182+ if (appRepeater.highlightedIndex != -1) {
183+ appRepeater.itemAt(appRepeater.highlightedIndex).focus = true;
132184 }
185+ }
186+
187+ Repeater {
188+ id: appRepeater
189+ model: ApplicationManager
190+ objectName: "appRepeater"
191+
192+ property int highlightedIndex: -1
193+ property int closingIndex: -1
194+
195+ delegate: FocusScope {
196+ id: appDelegate
197+ z: ApplicationManager.count - index
198+ y: units.gu(3)
199+ width: units.gu(60)
200+ height: units.gu(50)
201+
202+ readonly property int minWidth: units.gu(10)
203+ readonly property int minHeight: units.gu(10)
204+
205+ property bool maximized: false
206+ property bool minimized: false
207+
208+ onFocusChanged: {
209+ if (focus && ApplicationManager.focusedApplicationId !== model.appId) {
210+ ApplicationManager.requestFocusApplication(model.appId);
211+ decoratedWindow.forceActiveFocus();
212+ }
213+ }
214+ Component.onCompleted: {
215+ if (ApplicationManager.focusedApplicationId == model.appId) {
216+ decoratedWindow.forceActiveFocus();
217+ }
218+ }
219+
220+ Binding {
221+ target: ApplicationManager.get(index)
222+ property: "requestedState"
223+ // TODO: figure out some lifecycle policy, like suspending minimized apps
224+ // if running on a tablet or something.
225+ // TODO: If the device has a dozen suspended apps because it was running
226+ // in staged mode, when it switches to Windowed mode it will suddenly
227+ // resume all those apps at once. We might want to avoid that.
228+ value: ApplicationInfoInterface.RequestedRunning // Always running for now
229+ }
230+
231+ function maximize() {
232+ minimized = false;
233+ maximized = true;
234+ }
235+ function minimize() {
236+ maximized = false;
237+ minimized = true;
238+ }
239+ function unmaximize() {
240+ minimized = false;
241+ maximized = false;
242+ }
243+
244+ Behavior on x {
245+ id: closeBehavior
246+ enabled: appRepeater.closingIndex >= 0
247+ UbuntuNumberAnimation {
248+ onRunningChanged: if (!running) appRepeater.closingIndex = -1
249+ }
250+ }
251+
252+ states: [
253+ State {
254+ name: "normal"; when: !appDelegate.maximized && !appDelegate.minimized && root.state !== "altTab"
255+ },
256+ State {
257+ name: "maximized"; when: appDelegate.maximized && (root.state !== "altTab" || (root.state == "altTab" && !root.workspacesUpdated))
258+ PropertyChanges { target: appDelegate; x: 0; y: 0; width: root.width; height: root.height }
259+ },
260+ State {
261+ name: "minimized"; when: appDelegate.minimized && (root.state !== "altTab" || (root.state == "altTab" && !root.workspacesUpdated))
262+ PropertyChanges { target: appDelegate; x: -appDelegate.width / 2; scale: units.gu(5) / appDelegate.width; opacity: 0 }
263+ },
264+ State {
265+ name: "altTab"; when: root.state == "altTab" && root.workspacesUpdated
266+ PropertyChanges {
267+ target: appDelegate
268+ x: spreadMaths.animatedX
269+ y: spreadMaths.animatedY + (appDelegate.height - decoratedWindow.height)
270+ angle: spreadMaths.animatedAngle
271+ itemScale: spreadMaths.scale
272+ itemScaleOriginY: decoratedWindow.height / 2;
273+ z: index
274+ visible: spreadMaths.itemVisible
275+ }
276+ PropertyChanges {
277+ target: decoratedWindow
278+ decorationShown: false
279+ highlightShown: index == appRepeater.highlightedIndex
280+ state: "transformed"
281+ width: spreadMaths.spreadHeight
282+ height: spreadMaths.spreadHeight
283+ shadowOpacity: spreadMaths.shadowOpacity
284+ }
285+ PropertyChanges {
286+ target: tileInfo
287+ visible: true
288+ opacity: spreadMaths.tileInfoOpacity
289+ }
290+ PropertyChanges {
291+ target: spreadSelectArea
292+ enabled: true
293+ }
294+ PropertyChanges {
295+ target: windowMoveResizeArea
296+ enabled: false
297+ }
298+ }
299+ ]
300+ transitions: [
301+ Transition {
302+ from: "maximized,minimized,normal,"
303+ to: "maximized,minimized,normal,"
304+ PropertyAnimation { target: appDelegate; properties: "x,y,opacity,width,height,scale" }
305+ }
306+ ]
307+ property real angle: 0
308+ property real itemScale: 1
309+ property int itemScaleOriginX: 0
310+ property int itemScaleOriginY: 0
311+
312+ SpreadMaths {
313+ id: spreadMaths
314+ flickable: spreadFlickable
315+ itemIndex: index
316+ totalItems: Math.max(6, ApplicationManager.count)
317+ sceneHeight: root.height
318+ itemHeight: appDelegate.height
319+ }
320+
321+ WindowMoveResizeArea {
322+ id: windowMoveResizeArea
323+ target: appDelegate
324+ minWidth: appDelegate.minWidth
325+ minHeight: appDelegate.minHeight
326+ resizeHandleWidth: units.gu(2)
327+ windowId: model.appId // FIXME: Change this to point to windowId once we have such a thing
133328
134− readonly property int minWidth: units.gu(10)
135− readonly property int minHeight: units.gu(10)
136−
137− states: [
138− State {
139− name: "normal"
140− },
141− State {
142− name: "maximized"
143− PropertyChanges { target: appDelegate; x: 0; y: 0; width: root.width; height: root.height }
144− },
145− State {
146− name: "minimized"
147− PropertyChanges { target: appDelegate; x: -appDelegate.width / 2; scale: units.gu(5) / appDelegate.width; opacity: 0 }
329+ onPressed: appDelegate.focus = true;
148330 }
149− ]
150− transitions: [
151− Transition {
152− PropertyAnimation { target: appDelegate; properties: "x,y,opacity,width,height,scale" }
331+
332+ DecoratedWindow {
333+ id: decoratedWindow
334+ objectName: "decoratedWindow"
335+ anchors.left: appDelegate.left
336+ anchors.top: appDelegate.top
337+ windowWidth: appDelegate.width
338+ windowHeight: appDelegate.height
339+ application: ApplicationManager.get(index)
340+ active: ApplicationManager.focusedApplicationId === model.appId
341+ focus: false
342+
343+ onClose: ApplicationManager.stopApplication(model.appId)
344+ onMaximize: appDelegate.maximize()
345+ onMinimize: appDelegate.minimize()
346+
347+ transform: [
348+ Scale {
349+ origin.x: itemScaleOriginX
350+ origin.y: itemScaleOriginY
351+ xScale: itemScale
352+ yScale: itemScale
353+ },
354+ Rotation {
355+ origin { x: 0; y: (decoratedWindow.height - (decoratedWindow.height * itemScale / 2)) }
356+ axis { x: 0; y: 1; z: 0 }
357+ angle: appDelegate.angle
358+ }
359+ ]
360+
361+ MouseArea {
362+ id: spreadSelectArea
363+ anchors.fill: parent
364+ anchors.margins: -units.gu(2)
365+ enabled: false
366+ hoverEnabled: enabled
367+
368+ // There is a bug in MouseArea where containsMouse doesn't
369+ // return to false if the MouseArea is disabled while
370+ // containing the mouse. Let's manage the property our own.
371+ property bool upperThirdContainsMouse: false
372+ onContainsMouseChanged: evaluateContainsMouse()
373+ onMouseYChanged: evaluateContainsMouse()
374+ function evaluateContainsMouse() {
375+ if (containsMouse) {
376+ appRepeater.highlightedIndex = index
377+ }
378+
379+ if (containsMouse && mouseY < height / 3) {
380+ spreadSelectArea.upperThirdContainsMouse = true
381+ } else {
382+ spreadSelectArea.upperThirdContainsMouse = false;
383+ }
384+ }
385+ onEnabledChanged: {
386+ if (!enabled) {
387+ spreadSelectArea.upperThirdContainsMouse = false
388+ }
389+ }
390+
391+ onClicked: {
392+ root.state = ""
393+ }
394+ }
395+ }
396+
397+ Image {
398+ id: closeImage
399+ anchors { left: parent.left; top: parent.top; leftMargin: -height / 2; topMargin: -height / 2 + spreadMaths.closeIconOffset }
400+ source: "graphics/window-close.svg"
401+ visible: spreadSelectArea.upperThirdContainsMouse
402+ height: units.gu(1.5)
403+ width: height
404+ sourceSize.width: width
405+ sourceSize.height: height
406+ }
407+
408+ MouseArea {
409+ id: closeMouseArea
410+ objectName: "closeMouseArea"
411+ anchors.fill: closeImage
412+ anchors.margins: -units.gu(2)
413+ enabled: spreadSelectArea.upperThirdContainsMouse
414+ onClicked: {
415+ appRepeater.closingIndex = index;
416+ ApplicationManager.stopApplication(model.appId)
417+ }
153418 }
154− ]
155419
156− WindowMoveResizeArea {
157− target: appDelegate
158− minWidth: appDelegate.minWidth
159− minHeight: appDelegate.minHeight
160− resizeHandleWidth: units.gu(0.5)
161− windowId: model.appId // FIXME: Change this to point to windowId once we have such a thing
420+ MouseArea {
421+ id: tileInfo
422+ objectName: "tileInfo"
423+ anchors { left: parent.left; top: decoratedWindow.bottom; topMargin: units.gu(5) }
424+ width: units.gu(30)
425+ height: titleInfoColumn.height
426+ visible: false
427+ hoverEnabled: true
428+
429+ onContainsMouseChanged: {
430+ if (containsMouse) {
431+ appRepeater.highlightedIndex = index
432+ }
433+ }
434+
435+ onClicked: {
436+ root.state = ""
437+ }
438+
439+ ColumnLayout {
440+ id: titleInfoColumn
441+ anchors { left: parent.left; top: parent.top; right: parent.right }
442+ spacing: units.gu(1)
443+
444+ UbuntuShape {
445+ Layout.preferredHeight: Math.min(units.gu(6), root.height * .05)
446+ Layout.preferredWidth: height * 8 / 7.6
447+ image: Image {
448+ anchors.fill: parent
449+ source: model.icon
450+ }
451+ }
452+ Label {
453+ Layout.fillWidth: true
454+ Layout.preferredHeight: units.gu(6)
455+ text: model.name
456+ wrapMode: Text.WordWrap
457+ maximumLineCount: 2
458+ }
459+ }
460+ }
461+ }
462+ }
463+ }
464+
465+ FloatingFlickable {
466+ id: spreadFlickable
467+ anchors.fill: parent
468+ contentWidth: Math.max(6, ApplicationManager.count) * Math.min(height / 4, width / 5)
469+ enabled: false
470+
471+ function snapTo(contentX) {
472+ snapAnimation.stop();
473+ snapAnimation.to = contentX
474+ snapAnimation.start();
475+ }
476+
477+ UbuntuNumberAnimation {
478+ id: snapAnimation
479+ target: spreadFlickable
480+ property: "contentX"
481+ }
482+ }
483+
484+ Item {
485+ id: workspaceSelector
486+ anchors {
487+ left: parent.left
488+ top: parent.top
489+ right: parent.right
490+ topMargin: units.gu(3.5) // TODO: should be root.panelHeight
491+ }
492+ height: root.height * 0.25
493+ visible: false
494+
495+ RowLayout {
496+ anchors.fill: parent
497+ spacing: units.gu(1)
498+ Item { Layout.fillWidth: true }
499+ Repeater {
500+ model: 1 // TODO: will be a workspacemodel in the future
501+ Item {
502+ Layout.fillHeight: true
503+ Layout.preferredWidth: ((height - units.gu(6)) * root.width / root.height)
504+ Image {
505+ source: root.background
506+ anchors {
507+ left: parent.left
508+ right: parent.right
509+ verticalCenter: parent.verticalCenter
510+ }
511+ height: parent.height * 0.75
512+
513+ // FIXME: This is temporary until we can have multiple Items per surface
514+ ShaderEffect {
515+ anchors.fill: parent
516+
517+ property var source: ShaderEffectSource {
518+ id: shaderEffectSource
519+ live: false
520+ sourceItem: appContainer
521+ Connections { target: root; onUpdateWorkspaces: shaderEffectSource.scheduleUpdate() }
522+ }
523+
524+ fragmentShader: "
525+ varying highp vec2 qt_TexCoord0;
526+ uniform sampler2D source;
527+ void main(void)
528+ {
529+ highp vec4 sourceColor = texture2D(source, qt_TexCoord0);
530+ gl_FragColor = sourceColor;
531+ }"
532+ }
533+ }
534+
535+ // TODO: This is the bar for the currently selected workspace
536+ // Enable this once the workspace stuff is implemented
537+// Rectangle {
538+// anchors { left: parent.left; right: parent.right; bottom: parent.bottom }
539+// height: units.dp(2)
540+// color: UbuntuColors.orange
541+// visible: index == 0 // TODO: should be active workspace index
542+// }
543+ }
162544
163− onPressed: appDelegate.focus = true;
164545 }
546+ // TODO: This is the "new workspace" button. Enable this once workspaces are implemented
547+// Item {
548+// Layout.fillHeight: true
549+// Layout.preferredWidth: ((height - units.gu(6)) * root.width / root.height)
550+// Rectangle {
551+// anchors {
552+// left: parent.left
553+// right: parent.right
554+// verticalCenter: parent.verticalCenter
555+// }
556+// height: parent.height * 0.75
557+// color: "#22ffffff"
558+
559+// Label {
560+// anchors.centerIn: parent
561+// font.pixelSize: parent.height / 2
562+// text: "+"
563+// }
564+// }
565+// }
566+ Item { Layout.fillWidth: true }
567+ }
568+ }
165569
166− DecoratedWindow {
167− id: decoratedWindow
168− objectName: "decoratedWindow_" + appId
169− anchors.fill: parent
170− application: ApplicationManager.get(index)
171− active: ApplicationManager.focusedApplicationId === model.appId
172− focus: true
173−
174− onClose: ApplicationManager.stopApplication(model.appId)
175− onMaximize: appDelegate.state = (appDelegate.state == "maximized" ? "normal" : "maximized")
176− onMinimize: appDelegate.state = "minimized"
570+ Label {
571+ id: currentSelectedLabel
572+ anchors { bottom: parent.bottom; bottomMargin: root.height * 0.625; horizontalCenter: parent.horizontalCenter }
573+ text: appRepeater.highlightedIndex >= 0 ? ApplicationManager.get(appRepeater.highlightedIndex).name : ""
574+ visible: false
575+ fontSize: "large"
576+ }
577+
578+ states: [
579+ State {
580+ name: "altTab"; when: root.altTabPressed
581+ PropertyChanges { target: workspaceSelector; visible: true }
582+ PropertyChanges { target: spreadFlickable; enabled: true }
583+ PropertyChanges { target: currentSelectedLabel; visible: true }
584+ PropertyChanges { target: spreadBackground; visible: true }
585+ PropertyChanges { target: appContainer; focus: true }
586+ }
587+ ]
588+ signal updateWorkspaces();
589+ property bool workspacesUpdated: false
590+ transitions: [
591+ Transition {
592+ from: "*"
593+ to: "altTab"
594+ SequentialAnimation {
595+ PropertyAction { target: appRepeater; property: "highlightedIndex"; value: Math.min(ApplicationManager.count - 1, 1) }
596+ PauseAnimation { duration: 50 }
597+ PropertyAction { target: workspaceSelector; property: "visible" }
598+ ScriptAction { script: root.updateWorkspaces() }
599+ // FIXME: Updating of shaderEffectSource take a bit of time. This is temporary until we can paint multiple items per surface
600+ PauseAnimation { duration: 10 }
601+ PropertyAction { target: root; property: "workspacesUpdated"; value: true }
602+ PropertyAction { target: spreadFlickable; property: "visible" }
603+ PropertyAction { targets: [currentSelectedLabel,spreadBackground]; property: "visible" }
604+ PropertyAction { target: spreadFlickable; property: "contentX"; value: 0 }
177605 }
606+ },
607+ Transition {
608+ from: "*"
609+ to: "*"
610+ PropertyAnimation { property: "opacity" }
611+ PropertyAction { target: root; property: "workspacesUpdated"; value: false }
612+ ScriptAction { script: { appContainer.focusSelected() } }
613+ PropertyAction { target: appRepeater; property: "highlightedIndex"; value: -1 }
178614 }
615+
616+ ]
617+
618+ MouseArea {
619+ anchors {
620+ top: parent.top
621+ right: parent.right
622+ bottom: parent.bottom
623+ }
624+ // TODO: Make this a push to edge thing like the launcher when we can,
625+ // for now, yes, we want 1 pixel, regardless of the scaling
626+ width: 1
627+ hoverEnabled: true
628+ onContainsMouseChanged: {
629+ if (containsMouse) {
630+ root.state = "altTab"
631+ }
632+ }
179633 }
180634}
qml/Stages/PhoneStage.qml+10
@@ -36,6 +36,8 @@ Rectangle {
3636 property bool altTabEnabled: true
3737 property real startScale: 1.1
3838 property real endScale: 0.7
39+ property bool keepDashRunning: true
40+ property bool suspended: false
3941 property int shellOrientationAngle: 0
4042 property int shellOrientation
4143 property int shellPrimaryOrientation
@@ -428,6 +430,14 @@ Rectangle {
428430 dropShadow: spreadView.active || priv.focusedAppDelegateIsDislocated
429431 focusFirstApp: root.focusFirstApp
430432
433+ Binding {
434+ target: appDelegate.application
435+ property: "requestedState"
436+ value: (isDash && root.keepDashRunning) || (!root.suspended && appDelegate.focus)
437+ ? ApplicationInfoInterface.RequestedRunning
438+ : ApplicationInfoInterface.RequestedSuspended
439+ }
440+
431441 readonly property bool isDash: model.appId == "unity8-dash"
432442
433443 z: isDash && !spreadView.active ? -1 : behavioredIndex
qml/Stages/SpreadMaths.qmlnew file+164
@@ -0,0 +1,164 @@
1+/*
2+ * Copyright (C) 2015 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.2
18+import Ubuntu.Components 1.3
19+import Utils 0.1
20+
21+Item {
22+ id: root
23+ anchors { left: parent.left; top: parent.top; margins: units.gu(1) }
24+
25+ // Information about the transformed item
26+ property int itemIndex: 0
27+ property real itemHeight: units.gu(10)
28+
29+ // Information about the environment
30+ property int totalItems: 0
31+ property Item flickable: null
32+ property int sceneHeight: units.gu(20)
33+
34+ // Spread properties
35+ property real spreadHeight: sceneHeight * 0.4
36+ property int spreadBottomOffset: sceneHeight * 0.18
37+ property int foldingAreaWidth: flickableWidth * 0.2
38+ property int maxVisibleItems: 7
39+ property int margins: flickableWidth * 0.05
40+ property real stackScale: 0.1
41+ property int leftEndFoldedAngle: 70
42+ property int rightEndFoldedAngle: 65
43+ property int unfoldedAngle: 30
44+ property int stackWidth: flickableWidth * 0.01
45+
46+
47+ // Internal
48+ readonly property int flickableWidth: flickable ? flickable.width : 0
49+ readonly property int flickableContentWidth: flickable ? flickable.contentWidth: 0
50+ readonly property real flickableProgress: flickable ? flickable.contentX / (flickable.contentWidth - flickableWidth) : 0
51+
52+ readonly property int contentWidth: flickableWidth - root.margins * 2
53+
54+ readonly property int distance: (flickableContentWidth - (margins * 2) - (foldingAreaWidth * 2)) / (totalItems - 2)
55+ readonly property int startPos: margins + foldingAreaWidth + (itemIndex - 1) * distance
56+ readonly property int linearX: startPos - flickableProgress * (flickableContentWidth - flickableWidth)
57+
58+ readonly property int leftFoldingAreaX: margins + foldingAreaWidth
59+ readonly property int rightFoldingAreaX: flickableWidth - foldingAreaWidth - margins
60+
61+ readonly property real leftFoldingAreaProgress: linearAnimation(leftFoldingAreaX, margins, 0, 1, linearX)
62+ readonly property real rightFoldingAreaProgress: linearAnimation(rightFoldingAreaX, flickableWidth - margins, 0, 1, linearX)
63+
64+ readonly property real limitedLeftProgress: Math.min(2, leftFoldingAreaProgress)
65+ readonly property real limitedRightProgress: Math.min(2, rightFoldingAreaProgress)
66+
67+ readonly property real middleSectionProgress: (linearX - margins - foldingAreaWidth) / (flickableWidth - (margins + foldingAreaWidth) * 2)
68+
69+ // Output
70+ readonly property int animatedX: {
71+ if (leftFoldingAreaProgress > 4) { // Stop it at the edge
72+ return margins;
73+ }
74+ if (leftFoldingAreaProgress > 2) { // move it slowly through the stack
75+ return linearAnimation(2, 4, margins + stackWidth, margins, leftFoldingAreaProgress)
76+ }
77+ if (leftFoldingAreaProgress > 1 && itemIndex == 0) {
78+ // The leftmost runs faster... make it stop before the stack and wait for others
79+ return margins + stackWidth;
80+ }
81+
82+ if (leftFoldingAreaProgress > 0) { // slow it down in a curve
83+ if (itemIndex == 0) { // except if it's the leftmost. that one goes straigt
84+ return linearAnimation(0, 1, leftFoldingAreaX, margins + stackWidth, leftFoldingAreaProgress)
85+ }
86+ return linearAnimation(0, 1, leftFoldingAreaX, margins + stackWidth, leftEasing.value)
87+ }
88+ // same for the right side stack... mostly... don't need to treat the rightmost special...
89+ if (rightFoldingAreaProgress > 4) {
90+ return flickableWidth - margins
91+ }
92+ if (rightFoldingAreaProgress > 2) {
93+ return linearAnimation(2, 4, flickableWidth - margins - stackWidth, flickableWidth - margins, rightFoldingAreaProgress)
94+ }
95+
96+ if (rightFoldingAreaProgress > 0) {
97+ return linearAnimation(0, 1, rightFoldingAreaX, flickableWidth - margins - stackWidth, rightEasing.value);
98+ }
99+
100+ return linearX
101+ }
102+
103+ readonly property int animatedY: sceneHeight - itemHeight - spreadBottomOffset
104+
105+ readonly property real animatedAngle: {
106+ if (limitedLeftProgress > 0) {
107+ // Leftmost is special...
108+ if (index == 0) {
109+ if (limitedLeftProgress < 1) {
110+ return unfoldedAngle;
111+ } else {
112+ return linearAnimation(1, 2, unfoldedAngle, leftEndFoldedAngle, limitedLeftProgress)
113+ }
114+ }
115+ return linearAnimation(0, 2, unfoldedAngle, leftEndFoldedAngle, limitedLeftProgress)
116+ } else if (limitedRightProgress > 0) {
117+ return linearAnimation(0, 1, unfoldedAngle, rightEndFoldedAngle, rightEasing.value)
118+ } else {
119+ return unfoldedAngle
120+ }
121+ }
122+
123+ readonly property real scale: limitedLeftProgress > 0 ?
124+ linearAnimation(0, 1, 1, 1 + stackScale, leftEasing.value)
125+ : limitedRightProgress > 0 ?
126+ linearAnimation(0, 1, 1, 1 + stackScale, rightEasing.value)
127+ : 0.95 + Math.abs(middleSectionProgress - 0.5) * 0.1
128+
129+ readonly property real closeIconOffset: (scale - 1) * (-root.spreadHeight / 2)
130+
131+ readonly property real tileInfoOpacity: leftFoldingAreaProgress > 0 ?
132+ linearAnimation(1, 1.5, 1, 0, leftFoldingAreaProgress)
133+ : rightFoldingAreaProgress > 0 ?
134+ linearAnimation(1, 1.5, 1, 0, rightFoldingAreaProgress)
135+ : 1
136+
137+ readonly property bool itemVisible: itemIndex == totalItems - 1 ? true : leftFoldingAreaProgress < 5 && rightFoldingAreaProgress < 5
138+ readonly property real shadowOpacity: itemIndex == totalItems -1 ?
139+ 1
140+ : leftFoldingAreaProgress > 3 ?
141+ linearAnimation(3, 3.5, 1, 0, leftFoldingAreaProgress)
142+ : rightFoldingAreaProgress > 3 ?
143+ linearAnimation(3, 3.5, 1, 0, rightFoldingAreaProgress)
144+ : 1
145+
146+
147+ // Helpers
148+ function linearAnimation(startProgress, endProgress, startValue, endValue, progress) {
149+ // progress : progressDiff = value : valueDiff => value = progress * valueDiff / progressDiff
150+ return (progress - startProgress) * (endValue - startValue) / (endProgress - startProgress) + startValue;
151+ }
152+
153+ EasingCurve {
154+ id: leftEasing
155+ type: EasingCurve.OutSine
156+ progress: limitedLeftProgress / 2 // OutSine starts with twice the speed. slow it down.
157+ }
158+
159+ EasingCurve {
160+ id: rightEasing
161+ type: EasingCurve.OutSine
162+ progress: limitedRightProgress / 2 // OutSine starts with twice the speed. slow it down.
163+ }
164+}
qml/Stages/TabletStage.qml+14
@@ -36,6 +36,8 @@ Rectangle {
3636 property bool spreadEnabled: true // If false, animations and right edge will be disabled
3737
3838 property real inverseProgress: 0 // This is the progress for left edge drags, in pixels.
39+ property bool keepDashRunning: true
40+ property bool suspended: false
3941 property int shellOrientationAngle: 0
4042 property int shellOrientation
4143 property int shellPrimaryOrientation
@@ -117,6 +119,7 @@ Rectangle {
117119
118120 QtObject {
119121 id: priv
122+ objectName: "stagesPriv"
120123
121124 property string focusedAppId: ApplicationManager.focusedApplicationId
122125 readonly property var focusedAppDelegate: {
@@ -626,6 +629,17 @@ Rectangle {
626629
627630 readonly property bool isDash: model.appId == "unity8-dash"
628631
632+ Binding {
633+ target: spreadTile.application
634+ property: "requestedState"
635+ value: (spreadTile.isDash && root.keepDashRunning)
636+ ||
637+ (!root.suspended && (model.appId == priv.mainStageAppId
638+ || model.appId == priv.sideStageAppId))
639+ ? ApplicationInfoInterface.RequestedRunning
640+ : ApplicationInfoInterface.RequestedSuspended
641+ }
642+
629643 // FIXME: A regular binding doesn't update any more after closing an app.
630644 // Using a Binding for now.
631645 Binding {
qml/Stages/graphics/window-close.svgnew file+64
@@ -0,0 +1,64 @@
1+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2+<!-- Created with Inkscape (http://www.inkscape.org/) -->
3+
4+<svg
5+ xmlns:dc="http://purl.org/dc/elements/1.1/"
6+ xmlns:cc="http://creativecommons.org/ns#"
7+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
8+ xmlns:svg="http://www.w3.org/2000/svg"
9+ xmlns="http://www.w3.org/2000/svg"
10+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
11+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
12+ width="14.81667mm"
13+ height="14.816668mm"
14+ viewBox="0 0 14.81667 14.816668"
15+ id="svg4171"
16+ version="1.1"
17+ inkscape:version="0.91+devel r"
18+ sodipodi:docname="window-close01.svg">
19+ <defs
20+ id="defs4173" />
21+ <sodipodi:namedview
22+ id="base"
23+ pagecolor="#ffffff"
24+ bordercolor="#666666"
25+ borderopacity="1.0"
26+ inkscape:pageopacity="0.0"
27+ inkscape:pageshadow="2"
28+ inkscape:zoom="11.957142"
29+ inkscape:cx="37.299889"
30+ inkscape:cy="25.219237"
31+ inkscape:document-units="mm"
32+ inkscape:current-layer="layer1"
33+ showgrid="false"
34+ fit-margin-top="0"
35+ fit-margin-left="0"
36+ fit-margin-right="0"
37+ fit-margin-bottom="0" />
38+ <metadata
39+ id="metadata4176">
40+ <rdf:RDF>
41+ <cc:Work
42+ rdf:about="">
43+ <dc:format>image/svg+xml</dc:format>
44+ <dc:type
45+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
46+ <dc:title></dc:title>
47+ </cc:Work>
48+ </rdf:RDF>
49+ </metadata>
50+ <g
51+ inkscape:label="Layer 1"
52+ inkscape:groupmode="layer"
53+ id="layer1"
54+ transform="translate(-134.71071,-3.8416665)">
55+ <path
56+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#dd4814;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:16.93333244;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
57+ d="M 142.11905,3.8416665 A 7.4083333,7.4083333 0 0 0 134.71071,11.25 7.4083333,7.4083333 0 0 0 142.11905,18.658334 7.4083333,7.4083333 0 0 0 149.52738,11.25 7.4083333,7.4083333 0 0 0 142.11905,3.8416665 Z"
58+ id="path4141" />
59+ <path
60+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:16.93333244;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
61+ d="m 139.31818,7.7008631 2.80087,2.8008629 2.80086,-2.8008629 0.74827,0.7482734 -2.80086,2.8008635 2.80086,2.800864 -0.74827,0.748273 -2.80086,-2.800864 -2.80087,2.800864 -0.74827,-0.748273 2.80086,-2.800864 -2.80086,-2.8008635 0.74827,-0.7482734 z"
62+ id="path4170" />
63+ </g>
64+</svg>
tests/libs/UbuntuGestures/tst_TouchRegistry.cpp+47
@@ -67,6 +67,7 @@ private Q_SLOTS:
6767 void rejectingTouchfterItsEnd();
6868 void removeOldUndecidedCandidates();
6969 void interimOwnerWontGetUnownedTouchEvents();
70+ void candidateVanishes();
7071
7172private:
7273 TouchRegistry *touchRegistry;
@@ -817,6 +818,52 @@ void tst_TouchRegistry::interimOwnerWontGetUnownedTouchEvents()
817818 QCOMPARE(interimOwner.unownedTouchEvents.count(), 0);
818819}
819820
821+/*
822+ Regression test for https://bugs.launchpad.net/ubuntu/+source/unity8/+bug/1473492
823+
824+ Bug caused by candidates getting deleted before ownership resolution
825+ */
826+void tst_TouchRegistry::candidateVanishes()
827+{
828+ DummyCandidate mainCandidate;
829+ DummyCandidate *missingCandidate = new DummyCandidate;
830+
831+ {
832+ QList<QTouchEvent::TouchPoint> touchPoints;
833+ touchPoints.append(QTouchEvent::TouchPoint(0));
834+ touchPoints[0].setState(Qt::TouchPointPressed);
835+ QTouchEvent touchEvent(QEvent::TouchBegin,
836+ 0 /* device */,
837+ Qt::NoModifier,
838+ Qt::TouchPointPressed,
839+ touchPoints);
840+ touchRegistry->update(&touchEvent);
841+ }
842+
843+ touchRegistry->addCandidateOwnerForTouch(0, &mainCandidate);
844+ touchRegistry->addCandidateOwnerForTouch(0, missingCandidate);
845+
846+ {
847+ QList<QTouchEvent::TouchPoint> touchPoints;
848+ touchPoints.append(QTouchEvent::TouchPoint(0));
849+ touchPoints[0].setState(Qt::TouchPointMoved);
850+ QTouchEvent touchEvent(QEvent::TouchUpdate,
851+ 0 /* device */,
852+ Qt::NoModifier,
853+ Qt::TouchPointMoved,
854+ touchPoints);
855+ touchRegistry->update(&touchEvent);
856+ }
857+
858+ // This candidate suddenly disappears without having removed its candidacy.
859+ delete missingCandidate;
860+ missingCandidate = nullptr;
861+
862+ touchRegistry->requestTouchOwnership(0, &mainCandidate);
863+
864+ QVERIFY(mainCandidate.ownedTouches.contains(0));
865+}
866+
820867////////////// TouchMemento //////////
821868
822869TouchMemento::TouchMemento(const QTouchEvent *touchEvent)
tests/mocks/Unity/Application/ApplicationInfo.cpp+40−6
@@ -30,7 +30,7 @@ ApplicationInfo::ApplicationInfo(const QString &appId, QObject *parent)
3030 : ApplicationInfoInterface(appId, parent)
3131 , m_appId(appId)
3232 , m_stage(MainStage)
33− , m_state(Starting)
33+ , m_state(Stopped)
3434 , m_focused(false)
3535 , m_fullscreen(false)
3636 , m_session(0)
@@ -39,6 +39,7 @@ ApplicationInfo::ApplicationInfo(const QString &appId, QObject *parent)
3939 Qt::InvertedPortraitOrientation |
4040 Qt::InvertedLandscapeOrientation)
4141 , m_rotatesWindowContents(false)
42+ , m_requestedState(RequestedRunning)
4243 , m_manualSurfaceCreation(false)
4344{
4445}
@@ -46,7 +47,7 @@ ApplicationInfo::ApplicationInfo(const QString &appId, QObject *parent)
4647ApplicationInfo::ApplicationInfo(QObject *parent)
4748 : ApplicationInfoInterface(QString(), parent)
4849 , m_stage(MainStage)
49− , m_state(Starting)
50+ , m_state(Stopped)
5051 , m_focused(false)
5152 , m_fullscreen(false)
5253 , m_session(0)
@@ -55,6 +56,7 @@ ApplicationInfo::ApplicationInfo(QObject *parent)
5556 Qt::InvertedPortraitOrientation |
5657 Qt::InvertedLandscapeOrientation)
5758 , m_rotatesWindowContents(false)
59+ , m_requestedState(RequestedRunning)
5860 , m_manualSurfaceCreation(false)
5961{
6062}
@@ -74,7 +76,6 @@ void ApplicationInfo::createSession()
7476
7577void ApplicationInfo::setSession(Session* session)
7678{
77− qDebug() << "Application::setSession - appId=" << appId() << "session=" << session;
7879 if (m_session == session)
7980 return;
8081
@@ -91,6 +92,8 @@ void ApplicationInfo::setSession(Session* session)
9192 m_session->setApplication(this);
9293 m_session->setParent(this);
9394 SessionManager::singleton()->registerSession(m_session);
95+ connect(m_session, &Session::surfaceChanged,
96+ this, &ApplicationInfo::onSessionSurfaceChanged);
9497
9598 if (!m_manualSurfaceCreation) {
9699 QTimer::singleShot(500, m_session, SLOT(createSurface()));
@@ -161,11 +164,12 @@ void ApplicationInfo::setState(State value)
161164 m_state = value;
162165 Q_EMIT stateChanged(value);
163166
164− if (!m_manualSurfaceCreation && m_state == ApplicationInfo::Running) {
167+ if (!m_manualSurfaceCreation && !m_session && m_state == ApplicationInfo::Starting) {
165168 QTimer::singleShot(500, this, SLOT(createSession()));
166169 } else if (m_state == ApplicationInfo::Stopped) {
167− delete m_session;
168− m_session = nullptr;
170+ Session *session = m_session;
171+ setSession(nullptr);
172+ delete session;
169173 }
170174 }
171175}
@@ -213,3 +217,33 @@ void ApplicationInfo::setRotatesWindowContents(bool value)
213217{
214218 m_rotatesWindowContents = value;
215219}
220+
221+ApplicationInfo::RequestedState ApplicationInfo::requestedState() const
222+{
223+ return m_requestedState;
224+}
225+
226+void ApplicationInfo::setRequestedState(RequestedState value)
227+{
228+ if (m_requestedState != value) {
229+ m_requestedState = value;
230+ Q_EMIT requestedStateChanged(m_requestedState);
231+
232+ if (m_requestedState == RequestedRunning && m_state == Suspended) {
233+ setState(Running);
234+ } else if (m_requestedState == RequestedSuspended && m_state == Running) {
235+ setState(Suspended);
236+ }
237+ }
238+}
239+
240+void ApplicationInfo::onSessionSurfaceChanged(MirSurfaceItem* surface)
241+{
242+ if (surface != nullptr && m_state == Starting) {
243+ if (m_requestedState == RequestedRunning) {
244+ setState(Running);
245+ } else {
246+ setState(Suspended);
247+ }
248+ }
249+}
tests/mocks/Unity/Application/ApplicationInfo.h+7
@@ -46,6 +46,9 @@ public:
4646 ApplicationInfo(const QString &appId, QObject *parent = nullptr);
4747 ~ApplicationInfo();
4848
49+ RequestedState requestedState() const override;
50+ void setRequestedState(RequestedState) override;
51+
4952 void setIconId(const QString &iconId);
5053 void setScreenshotId(const QString &screenshotId);
5154
@@ -101,6 +104,9 @@ Q_SIGNALS:
101104public Q_SLOTS:
102105 Q_INVOKABLE void createSession();
103106
107+private Q_SLOTS:
108+ void onSessionSurfaceChanged(MirSurfaceItem*);
109+
104110private:
105111 void setIcon(const QUrl &value);
106112
@@ -116,6 +122,7 @@ private:
116122 Session* m_session;
117123 Qt::ScreenOrientations m_supportedOrientations;
118124 bool m_rotatesWindowContents;
125+ RequestedState m_requestedState;
119126
120127 bool m_manualSurfaceCreation;
121128};
tests/mocks/Unity/Application/ApplicationManager.cpp+2−49
@@ -49,8 +49,6 @@ ApplicationManager *ApplicationManager::singleton()
4949
5050ApplicationManager::ApplicationManager(QObject *parent)
5151 : ApplicationManagerInterface(parent)
52− , m_suspended(false)
53− , m_forceDashActive(false)
5452{
5553 m_roleNames.insert(RoleSession, "session");
5654 m_roleNames.insert(RoleFullscreen, "fullscreen");
@@ -217,7 +215,7 @@ ApplicationInfo* ApplicationManager::startApplication(const QString &appId,
217215 && application->stage() == ApplicationInfo::SideStage) {
218216 application->setStage(ApplicationInfo::MainStage);
219217 }
220− application->setState(ApplicationInfo::Running);
218+ application->setState(ApplicationInfo::Starting);
221219
222220 return application;
223221}
@@ -262,50 +260,6 @@ QString ApplicationManager::focusedApplicationId() const {
262260 return QString();
263261}
264262
265−bool ApplicationManager::suspended() const
266−{
267− return m_suspended;
268−}
269−
270−void ApplicationManager::setSuspended(bool suspended)
271−{
272− ApplicationInfo *focusedApp = findApplication(focusedApplicationId());
273− if (focusedApp) {
274− if (suspended) {
275− focusedApp->setState(ApplicationInfo::Suspended);
276− } else {
277− focusedApp->setState(ApplicationInfo::Running);
278− }
279− }
280− m_suspended = suspended;
281− Q_EMIT suspendedChanged();
282−}
283−
284−bool ApplicationManager::forceDashActive() const
285−{
286− return m_forceDashActive;
287−}
288−
289−void ApplicationManager::setForceDashActive(bool forceDashActive)
290−{
291− if (m_forceDashActive == forceDashActive) {
292− return;
293− }
294−
295− ApplicationInfo *dash = findApplication("unity8-dash");
296− if (dash) {
297− if (forceDashActive) {
298− dash->setState(ApplicationInfo::Running);
299− } else {
300− if (!dash->focused()) {
301− dash->setState(ApplicationInfo::Suspended);
302− }
303− }
304− }
305− m_forceDashActive = forceDashActive;
306− Q_EMIT forceDashActiveChanged();
307−}
308−
309263bool ApplicationManager::focusApplication(const QString &appId)
310264{
311265 ApplicationInfo *application = findApplication(appId);
@@ -316,13 +270,11 @@ bool ApplicationManager::focusApplication(const QString &appId)
316270 for (ApplicationInfo *app : m_runningApplications) {
317271 if (app->focused()) {
318272 app->setFocused(false);
319− app->setState(ApplicationInfo::Suspended);
320273 }
321274 }
322275
323276 // focus this app
324277 application->setFocused(true);
325− application->setState(ApplicationInfo::Running);
326278
327279 // move app to top of stack
328280 move(m_runningApplications.indexOf(application), 0);
@@ -394,6 +346,7 @@ void ApplicationManager::buildListOfAvailableApplications()
394346 application->setScreenshotId("gallery");
395347 application->setIconId("gallery");
396348 application->setFullscreen(true);
349+ application->setStage(ApplicationInfo::MainStage);
397350 m_availableApplications.append(application);
398351
399352 application = new ApplicationInfo(this);
tests/mocks/Unity/Application/ApplicationManager.h−7
@@ -69,11 +69,6 @@ class ApplicationManager : public ApplicationManagerInterface {
6969 Q_INVOKABLE bool stopApplication(const QString &appId) override;
7070
7171 QString focusedApplicationId() const override;
72− bool suspended() const override;
73− void setSuspended(bool suspended) override;
74−
75− bool forceDashActive() const override;
76− void setForceDashActive(bool forceDashActive) override;
7772
7873 // Only for testing
7974 QStringList availableApplications();
@@ -96,8 +91,6 @@ class ApplicationManager : public ApplicationManagerInterface {
9691 void remove(ApplicationInfo* application);
9792 void buildListOfAvailableApplications();
9893 void onWindowCreated();
99− bool m_suspended;
100− bool m_forceDashActive;
10194 QList<ApplicationInfo*> m_runningApplications;
10295 QList<ApplicationInfo*> m_availableApplications;
10396 QTimer m_windowCreatedTimer;
tests/mocks/Unity/Launcher/CMakeLists.txt+1−1
@@ -1,4 +1,4 @@
1−pkg_check_modules(LAUNCHER_API REQUIRED unity-shell-launcher=6)
1+pkg_check_modules(LAUNCHER_API REQUIRED unity-shell-launcher=7)
22
33include_directories(
44 ${CMAKE_CURRENT_SOURCE_DIR}
tests/mocks/Unity/Launcher/MockLauncherItem.cpp+22
@@ -37,9 +37,13 @@ MockLauncherItem::MockLauncherItem(const QString &appId, const QString& desktopF
3737 m_count(0),
3838 m_countVisible(false),
3939 m_focused(false),
40+ m_alerting(false),
4041 m_quickList(new MockQuickListModel(this))
4142{
43+}
4244
45+MockLauncherItem::~MockLauncherItem()
46+{
4347}
4448
4549QString MockLauncherItem::appId() const
@@ -129,6 +133,9 @@ void MockLauncherItem::setCount(int count)
129133 {
130134 m_count = count;
131135 Q_EMIT countChanged(count);
136+ if (m_countVisible) {
137+ setAlerting(true);
138+ }
132139 }
133140}
134141
@@ -142,6 +149,9 @@ void MockLauncherItem::setCountVisible(bool countVisible)
142149 if (m_countVisible != countVisible) {
143150 m_countVisible = countVisible;
144151 Q_EMIT countVisibleChanged(countVisible);
152+ if (countVisible) {
153+ setAlerting(true);
154+ }
145155 }
146156}
147157
@@ -159,6 +169,18 @@ void MockLauncherItem::setFocused(bool focused)
159169 }
160170}
161171
172+bool MockLauncherItem::alerting() const
173+{
174+ return m_alerting;
175+}
176+
177+void MockLauncherItem::setAlerting(bool alerting)
178+{
179+ if (m_alerting != alerting) {
180+ m_alerting = alerting;
181+ Q_EMIT alertingChanged(alerting);
182+ }
183+}
162184
163185unity::shell::launcher::QuickListModelInterface *MockLauncherItem::quickList() const
164186{
tests/mocks/Unity/Launcher/MockLauncherItem.h+4
@@ -31,6 +31,7 @@ class MockLauncherItem: public LauncherItemInterface
3131 Q_OBJECT
3232public:
3333 MockLauncherItem(const QString &appId, const QString& desktopFile, const QString& name, const QString& icon, QObject* parent = 0);
34+ ~MockLauncherItem();
3435
3536 QString appId() const override;
3637 QString desktopFile() const;
@@ -44,6 +45,7 @@ public:
4445 int count() const override;
4546 bool countVisible() const override;
4647 bool focused() const override;
48+ bool alerting() const override;
4749
4850 unity::shell::launcher::QuickListModelInterface *quickList() const override;
4951
@@ -55,6 +57,7 @@ private:
5557 void setCount(int count);
5658 void setCountVisible(bool countVisible);
5759 void setFocused(bool focused);
60+ void setAlerting(bool alerting);
5861
5962 QString m_appId;
6063 QString m_desktopFile;
@@ -67,6 +70,7 @@ private:
6770 int m_count;
6871 bool m_countVisible;
6972 bool m_focused;
73+ bool m_alerting;
7074 MockQuickListModel *m_quickList;
7175
7276 friend class MockLauncherModel;
tests/mocks/Unity/Launcher/MockLauncherModel.cpp+54−1
@@ -34,6 +34,7 @@ MockLauncherModel::MockLauncherModel(QObject* parent): LauncherModelInterface(pa
3434 item = new MockLauncherItem("gallery-app", "/usr/share/applications/gallery-app.desktop", "Gallery", "gallery", this);
3535 item->setProgress(50);
3636 item->setCountVisible(true);
37+ item->setAlerting(false);
3738 m_list.append(item);
3839 item = new MockLauncherItem("music-app", "/usr/share/applications/music-app.desktop", "Music", "soundcloud", this);
3940 m_list.append(item);
@@ -43,25 +44,31 @@ MockLauncherModel::MockLauncherModel(QObject* parent): LauncherModelInterface(pa
4344 item = new MockLauncherItem("webbrowser-app", "/usr/share/applications/webbrowser-app.desktop", "Browser", "browser", this);
4445 item->setCount(1);
4546 item->setCountVisible(true);
47+ item->setRunning(true);
48+ item->setAlerting(false);
4649 m_list.append(item);
4750 item = new MockLauncherItem("twitter-webapp", "/usr/share/applications/twitter-webapp.desktop", "Twitter", "twitter", this);
4851 item->setCount(12);
4952 item->setCountVisible(true);
53+ item->setAlerting(false);
5054 item->setPinned(true);
5155 m_list.append(item);
5256 item = new MockLauncherItem("gmail-webapp", "/usr/share/applications/gmail-webapp.desktop", "GMail", "gmail", this);
5357 item->setCount(123);
5458 item->setCountVisible(true);
59+ item->setAlerting(false);
5560 m_list.append(item);
5661 item = new MockLauncherItem("ubuntu-weather-app", "/usr/share/applications/ubuntu-weather-app.desktop", "Weather", "weather", this);
5762 item->setCount(1234567890);
5863 item->setCountVisible(true);
64+ item->setAlerting(false);
5965 item->setPinned(true);
6066 m_list.append(item);
6167 item = new MockLauncherItem("notes-app", "/usr/share/applications/notes-app.desktop", "Notepad", "notepad", this);
6268 item->setProgress(50);
6369 item->setCount(5);
6470 item->setCountVisible(true);
71+ item->setAlerting(false);
6572 item->setFocused(true);
6673 item->setPinned(true);
6774 m_list.append(item);
@@ -105,6 +112,8 @@ QVariant MockLauncherModel::data(const QModelIndex& index, int role) const
105112 return item->countVisible();
106113 case RoleFocused:
107114 return item->focused();
115+ case RoleAlerting:
116+ return item->alerting();
108117 }
109118
110119 return QVariant();
@@ -119,6 +128,18 @@ unity::shell::launcher::LauncherItemInterface *MockLauncherModel::get(int index)
119128 return m_list.at(index);
120129}
121130
131+void MockLauncherModel::setAlerting(const QString &appId, bool alerting) {
132+ int index = findApp(appId);
133+ if (index >= 0) {
134+ QModelIndex modelIndex = this->index(index);
135+ MockLauncherItem *item = m_list.at(index);
136+ if (!item->focused()) {
137+ item->setAlerting(alerting);
138+ Q_EMIT dataChanged(modelIndex, modelIndex, QVector<int>() << RoleAlerting);
139+ }
140+ }
141+}
142+
122143void MockLauncherModel::move(int oldIndex, int newIndex)
123144{
124145 // Make sure its not moved outside the lists
@@ -174,7 +195,9 @@ void MockLauncherModel::requestRemove(const QString &appId)
174195 int index = findApp(appId);
175196 if (index >= 0) {
176197 beginRemoveRows(QModelIndex(), index, 0);
177− m_list.takeAt(index)->deleteLater();
198+ MockLauncherItem * item = m_list.takeAt(index);
199+ item->setRunning(false);
200+ item->deleteLater();
178201 endRemoveRows();
179202 }
180203}
@@ -195,6 +218,16 @@ int MockLauncherModel::findApp(const QString &appId)
195218 return -1;
196219}
197220
221+void MockLauncherModel::setProgress(const QString &appId, int progress)
222+{
223+ int index = findApp(appId);
224+ if (index >= 0) {
225+ m_list.at(index)->setProgress(progress);
226+ QModelIndex modelIndex = this->index(index);
227+ Q_EMIT dataChanged(modelIndex, modelIndex, QVector<int>() << RoleProgress);
228+ }
229+}
230+
198231void MockLauncherModel::setUser(const QString &username)
199232{
200233 Q_UNUSED(username)
@@ -226,6 +259,26 @@ void MockLauncherModel::emitHint()
226259 Q_EMIT hint();
227260}
228261
262+void MockLauncherModel::setCount(const QString &appId, int count)
263+{
264+ int index = findApp(appId);
265+ if (index >= 0) {
266+ m_list.at(index)->setCount(count);
267+ QModelIndex modelIndex = this->index(index);
268+ Q_EMIT dataChanged(modelIndex, modelIndex);
269+ }
270+}
271+
272+void MockLauncherModel::setCountVisible(const QString &appId, bool countVisible)
273+{
274+ int index = findApp(appId);
275+ if (index >= 0) {
276+ m_list.at(index)->setCountVisible(countVisible);
277+ QModelIndex modelIndex = this->index(index);
278+ Q_EMIT dataChanged(modelIndex, modelIndex);
279+ }
280+}
281+
229282unity::shell::application::ApplicationManagerInterface *MockLauncherModel::applicationManager() const
230283{
231284 return nullptr;
tests/mocks/Unity/Launcher/MockLauncherModel.h+4
@@ -38,6 +38,7 @@ public:
3838
3939 QVariant data(const QModelIndex& index, int role) const override;
4040
41+ Q_INVOKABLE void setAlerting(const QString &appId, bool alerting) override;
4142 Q_INVOKABLE unity::shell::launcher::LauncherItemInterface *get(int index) const override;
4243 Q_INVOKABLE void move(int oldIndex, int newIndex) override;
4344 Q_INVOKABLE void pin(const QString &appId, int index = -1) override;
@@ -54,6 +55,9 @@ public:
5455
5556 // For testing
5657 Q_INVOKABLE void emitHint();
58+ Q_INVOKABLE void setProgress(const QString &appId, int progress);
59+ Q_INVOKABLE void setCount(const QString &appId, int count);
60+ Q_INVOKABLE void setCountVisible(const QString &appId, bool countVisible);
5761
5862Q_SIGNALS:
5963 void quickListTriggered(const QString &appId, int index);
tests/mocks/Utils/CMakeLists.txt+3−1
@@ -4,12 +4,12 @@ include_directories(
44 ${CMAKE_CURRENT_SOURCE_DIR}
55 ${CMAKE_CURRENT_BINARY_DIR}
66 ${Qt5Gui_PRIVATE_INCLUDE_DIRS}
7+ ${Qt5Quick_PRIVATE_INCLUDE_DIRS}
78 ${GIO_INCLUDE_DIRS}
89 ${CMAKE_SOURCE_DIR}/plugins/Utils
910)
1011
1112set(QMLPLUGIN_SRC
12− ${CMAKE_SOURCE_DIR}/plugins/Utils/inputwatcher.cpp
1313 ${CMAKE_SOURCE_DIR}/plugins/Utils/qlimitproxymodelqml.cpp
1414 ${CMAKE_SOURCE_DIR}/plugins/Utils/unitysortfilterproxymodelqml.cpp
1515 ${CMAKE_SOURCE_DIR}/plugins/Utils/relativetimeformatter.cpp
@@ -18,9 +18,11 @@ set(QMLPLUGIN_SRC
1818 ${CMAKE_SOURCE_DIR}/plugins/Utils/windowkeysfilter.cpp
1919 ${CMAKE_SOURCE_DIR}/plugins/Utils/windowscreenshotprovider.cpp
2020 ${CMAKE_SOURCE_DIR}/plugins/Utils/easingcurve.cpp
21+ ${CMAKE_SOURCE_DIR}/plugins/Utils/inputwatcher.cpp
2122 constants.cpp
2223 plugin.cpp
2324 windowstatestorage.cpp
25+ plugin.cpp
2426 )
2527
2628add_library(FakeUtils-qml SHARED
tests/mocks/Utils/plugin.cpp+9−1
@@ -23,6 +23,14 @@
2323
2424// local
2525#include "plugin.h"
26+#include "inputwatcher.h"
27+#include "qlimitproxymodelqml.h"
28+#include "unitysortfilterproxymodelqml.h"
29+#include "relativetimeformatter.h"
30+#include "timeformatter.h"
31+#include "unitymenumodelpaths.h"
32+#include "windowkeysfilter.h"
33+#include "easingcurve.h"
2634#include "windowstatestorage.h"
2735#include "constants.h"
2836
@@ -53,7 +61,7 @@ static QObject *createConstants(QQmlEngine *engine, QJSEngine *scriptEngine)
5361
5462void FakeUtilsPlugin::registerTypes(const char *uri)
5563{
56− Q_ASSERT(uri == QLatin1String("Utils"));;
64+ Q_ASSERT(uri == QLatin1String("Utils"));
5765 qmlRegisterType<QAbstractItemModel>();
5866 qmlRegisterType<QLimitProxyModelQML>(uri, 0, 1, "LimitProxyModel");
5967 qmlRegisterType<UnitySortFilterProxyModelQML>(uri, 0, 1, "UnitySortFilterProxyModel");
tests/plugins/CMakeLists.txt+1
@@ -2,6 +2,7 @@ add_subdirectory(AccountsService)
22add_subdirectory(Greeter)
33add_subdirectory(LightDM)
44add_subdirectory(Dash)
5+add_subdirectory(ScreenGrabber)
56add_subdirectory(Ubuntu)
67add_subdirectory(Unity)
78add_subdirectory(Utils)
tests/plugins/Greeter/Unity/Launcher/CMakeLists.txt+1−2
@@ -1,5 +1,4 @@
1−pkg_check_modules(LAUNCHER_API REQUIRED unity-shell-launcher=6)
2−pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=6)
1+pkg_check_modules(LAUNCHER_API REQUIRED unity-shell-launcher=7)
32
43include_directories(
54 ${CMAKE_CURRENT_SOURCE_DIR}
tests/plugins/Greeter/Unity/Launcher/launchermodelastest.cpp+3
@@ -43,6 +43,7 @@ private Q_SLOTS:
4343 item.insert("name", "Item 1");
4444 item.insert("icon", "fake.svg");
4545 item.insert("count", 0);
46+ item.insert("progress", 0);
4647 item.insert("countVisible", false);
4748 item.insert("pinned", true);
4849 list.append(item);
@@ -67,6 +68,7 @@ private Q_SLOTS:
6768 inSync &= model->get(i)->name() == list.at(i).value("name").toString();
6869 inSync &= model->get(i)->icon() == list.at(i).value("icon").toString();
6970 inSync &= model->get(i)->count() == list.at(i).value("count").toInt();
71+ inSync &= model->get(i)->progress() == list.at(i).value("progress").toInt();
7072 inSync &= model->get(i)->countVisible() == list.at(i).value("countVisible").toBool();
7173 }
7274 return inSync;
@@ -111,6 +113,7 @@ private Q_SLOTS:
111113 newEntry.insert("name", "New app");
112114 newEntry.insert("icon", "some-icon.svg");
113115 newEntry.insert("count", 0);
116+ newEntry.insert("progress", 42);
114117 newEntry.insert("countVisible", false);
115118 newEntry.insert("pinned", true);
116119 newList.append(newEntry);
tests/plugins/ScreenGrabber/CMakeLists.txtnew file+14
@@ -0,0 +1,14 @@
1+include_directories(
2+ ${CMAKE_SOURCE_DIR}/plugins/ScreenGrabber
3+ ${CMAKE_CURRENT_BINARY_DIR}
4+ )
5+
6+add_definitions(
7+ -DCURRENT_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}"
8+ )
9+add_definitions(-DBUILT_PLUGINS_DIR="${CMAKE_BINARY_DIR}/plugins")
10+
11+add_executable(ScreenGrabberTestExec ScreenGrabberTest.cpp ${CMAKE_SOURCE_DIR}/plugins/ScreenGrabber/screengrabber.cpp)
12+qt5_use_modules(ScreenGrabberTestExec Test Quick)
13+target_link_libraries(ScreenGrabberTestExec ${Qt5Quick_LIBRARIES} ScreenGrabber-qml)
14+add_unity8_uitest(ScreenGrabber ScreenGrabberTestExec DEPENDS ScreenGrabber-qml)
tests/plugins/ScreenGrabber/ScreenGrabberTest.cppnew file+65
@@ -0,0 +1,65 @@
1+/*
2+ * Copyright (C) 2015 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "screengrabber.h"
18+
19+#include <QtTest>
20+#include <QtQuick/QQuickView>
21+#include <QtQuick/QQuickItem>
22+#include <QQmlEngine>
23+
24+class ScreenGrabberTest: public QObject
25+{
26+ Q_OBJECT
27+
28+private Q_SLOTS:
29+
30+ void initTestCase()
31+ {
32+ m_view = new QQuickView();
33+ m_view->engine()->addImportPath(BUILT_PLUGINS_DIR);
34+ m_view->setSource(QUrl::fromLocalFile(CURRENT_SOURCE_DIR "/grabber.qml"));
35+ m_grabber = dynamic_cast<ScreenGrabber*>(m_view->rootObject()->property("grabber").value<QObject*>());
36+ QVERIFY(m_grabber);
37+ m_view->show();
38+ QTest::qWaitForWindowExposed(m_view);
39+ }
40+
41+ void cleanupTestCase()
42+ {
43+ delete m_view;
44+ m_view = nullptr;
45+ QTRY_COMPARE(QThreadPool::globalInstance()->activeThreadCount(), 0);
46+ }
47+
48+ void testGrabScreenshot()
49+ {
50+ QSignalSpy grabberSpy(m_grabber, &ScreenGrabber::screenshotSaved);
51+ m_grabber->captureAndSave();
52+ QTRY_VERIFY(grabberSpy.count() == 1);
53+ const QVariantList args = grabberSpy.takeFirst();
54+ QVERIFY(args.count() == 1); // verify we got a non-empty filename where the screenshot has been saved
55+ QVERIFY(!args.first().toString().isEmpty());
56+ }
57+
58+private:
59+ QQuickView *m_view;
60+ ScreenGrabber *m_grabber = nullptr;
61+};
62+
63+QTEST_MAIN(ScreenGrabberTest)
64+
65+#include "ScreenGrabberTest.moc"
tests/plugins/ScreenGrabber/grabber.qmlnew file+31
@@ -0,0 +1,31 @@
1+/*
2+ * Copyright 2015 Canonical Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import ScreenGrabber 0.1
19+
20+Rectangle {
21+ property var grabber: screenGrabber
22+
23+ width: 100
24+ height: 100
25+ color: "green"
26+
27+ ScreenGrabber {
28+ id: screenGrabber
29+ objectName: "screenGrabber"
30+ }
31+}
tests/plugins/Unity/Launcher/CMakeLists.txt+1−1
@@ -1,5 +1,5 @@
11pkg_check_modules(GSETTINGS_QT REQUIRED gsettings-qt)
2−pkg_check_modules(LAUNCHER_API REQUIRED unity-shell-launcher=6)
2+pkg_check_modules(LAUNCHER_API REQUIRED unity-shell-launcher=7)
33
44include_directories(
55 ${CMAKE_CURRENT_SOURCE_DIR}
tests/plugins/Unity/Launcher/launchermodeltest.cpp+67−11
@@ -39,6 +39,9 @@ class MockApp: public unity::shell::application::ApplicationInfoInterface
3939 Q_OBJECT
4040public:
4141 MockApp(const QString &appId, QObject *parent = 0): ApplicationInfoInterface(appId, parent), m_appId(appId), m_focused(false) { }
42+
43+ RequestedState requestedState() const override { return RequestedRunning; }
44+ void setRequestedState(RequestedState) override {}
4245 QString appId() const override { return m_appId; }
4346 QString name() const override { return "mock"; }
4447 QString comment() const override { return "this is a mock"; }
@@ -68,6 +71,7 @@ class MockAppManager: public unity::shell::application::ApplicationManagerInterf
6871 Q_OBJECT
6972public:
7073 MockAppManager(QObject *parent = 0): ApplicationManagerInterface(parent) {}
74+ ~MockAppManager() {}
7175 int rowCount(const QModelIndex &) const override { return m_list.count(); }
7276 QVariant data(const QModelIndex &, int ) const override { return QVariant(); }
7377 QString focusedApplicationId() const override {
@@ -116,10 +120,6 @@ public:
116120 endRemoveRows();
117121 }
118122 bool requestFocusApplication(const QString &appId) override { Q_UNUSED(appId); return true; }
119− bool suspended() const override { return false; }
120− void setSuspended(bool) override {}
121− bool forceDashActive() const override { return false; }
122− void setForceDashActive(bool) override {}
123123
124124private:
125125 QList<MockApp*> m_list;
@@ -325,6 +325,46 @@ private Q_SLOTS:
325325 QCOMPARE(launcherModel->get(0)->appId(), QLatin1String("abs-icon"));
326326 }
327327
328+ void testQuitMenuItem() {
329+ // we have 2 apps running, both should have the Quit action in its quick list
330+ QCOMPARE(launcherModel->rowCount(), 2);
331+
332+ // stop the second one keeping it pinned so that it doesn't go away
333+ launcherModel->pin("no-icon");
334+ appManager->stopApplication("no-icon");
335+
336+ // find the first Quit item, should be there
337+ QuickListModel *model = qobject_cast<QuickListModel*>(launcherModel->get(0)->quickList());
338+ int quitActionIndex = -1;
339+ for (int i = 0; i < model->rowCount(); ++i) {
340+ if (model->get(i).actionId() == "stop_item") {
341+ quitActionIndex = i;
342+ break;
343+ }
344+ }
345+ QVERIFY(quitActionIndex >= 0);
346+
347+ // find the second Quit item, should NOT be there, the app is stopped
348+ QuickListModel *model2 = qobject_cast<QuickListModel*>(launcherModel->get(1)->quickList());
349+ int quitActionIndex2 = -1;
350+ for (int i = 0; i < model2->rowCount(); ++i) {
351+ if (model2->get(i).actionId() == "stop_item") {
352+ quitActionIndex2 = i;
353+ break;
354+ }
355+ }
356+ QVERIFY(quitActionIndex2 == -1);
357+
358+ // trigger the first quit item quicklist action
359+ launcherModel->quickListActionInvoked(launcherModel->get(0)->appId(), quitActionIndex);
360+ // first app should be gone...
361+ QCOMPARE(launcherModel->rowCount(QModelIndex()), 1);
362+ // ... the second app (now at index 0) should still be there, pinned and stopped
363+ QCOMPARE(launcherModel->get(0)->appId(), QStringLiteral("no-icon"));
364+ QCOMPARE(launcherModel->get(0)->pinned(), true);
365+ QCOMPARE(launcherModel->get(0)->running(), false);
366+ }
367+
328368 void testGetUrlForAppId() {
329369 QCOMPARE(launcherModel->getUrlForAppId(QString()), QString());
330370 QCOMPARE(launcherModel->getUrlForAppId(""), QString());
@@ -368,11 +408,18 @@ private Q_SLOTS:
368408 }
369409
370410 void testCountEmblems() {
411+ QSignalSpy spy(launcherModel, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector<int>)));
412+
371413 // Call GetAll on abs-icon
372414 QDBusInterface interface("com.canonical.Unity.Launcher", "/com/canonical/Unity/Launcher/abs_2Dicon", "org.freedesktop.DBus.Properties");
373415 QDBusReply<QVariantMap> reply = interface.call("GetAll");
374416 QVariantMap map = reply.value();
375417
418+ // Check that the alerting-status is still false, and the item on the upper side of the API
419+ int index = launcherModel->findApplication("abs-icon");
420+ QCOMPARE(index >= 0, true);
421+ QVERIFY(launcherModel->get(index)->alerting() == false);
422+
376423 // Make sure GetAll returns a map with count and countVisible props
377424 QCOMPARE(map.contains("count"), true);
378425 QCOMPARE(map.contains("countVisible"), true);
@@ -393,13 +440,8 @@ private Q_SLOTS:
393440 QCOMPARE(map.value("count").toInt(), 55);
394441 QCOMPARE(map.value("countVisible").toBool(), true);
395442
396− // Now the item on the upper side of the API
397− int index = launcherModel->findApplication("abs-icon");
398− QCOMPARE(index >= 0, true);
399−
400− // And make sure values have changed there as well
401− QCOMPARE(launcherModel->get(index)->countVisible(), true);
402− QCOMPARE(launcherModel->get(index)->count(), 55);
443+ // Finally check, that the change to "count" implicitly also set the alerting-state to true
444+ QVERIFY(launcherModel->get(index)->alerting() == true);
403445 }
404446
405447 void testCountEmblemAddsRemovesItem_data() {
@@ -459,6 +501,20 @@ private Q_SLOTS:
459501 QCOMPARE(index == -1, !isRunning && !isPinned && !startWhenVisible);
460502 }
461503
504+ void testAlert() {
505+ // Check that the alerting-status is still false
506+ int index = launcherModel->findApplication("abs-icon");
507+ QCOMPARE(index >= 0, true);
508+ QVERIFY(launcherModel->get(index)->alerting() == false);
509+
510+ // Call Alert() on "abs-icon"
511+ QDBusInterface interface("com.canonical.Unity.Launcher", "/com/canonical/Unity/Launcher/abs_2Dicon", "com.canonical.Unity.Launcher.Item");
512+ interface.call("Alert");
513+
514+ // Check that the alerting-status is now true
515+ QVERIFY(launcherModel->get(index)->alerting() == true);
516+ }
517+
462518 void testRefreshAfterDeletedDesktopFiles_data() {
463519 QTest::addColumn<bool>("deleted");
464520 QTest::newRow("have .desktop files") << false;
tests/plugins/Unity/Session/sessionbackendtest.cpp+54−8
@@ -24,6 +24,11 @@
2424#include <QDBusInterface>
2525#include <QDBusReply>
2626#include <QDBusVariant>
27+#include <QDebug>
28+#include <QDBusObjectPath>
29+
30+#include <unistd.h>
31+#include <sys/types.h>
2732
2833#include "dbusunitysessionservice.h"
2934
@@ -52,9 +57,10 @@ private Q_SLOTS:
5257 QTest::addColumn<QString>("method");
5358 QTest::addColumn<QString>("signal");
5459
55− QTest::newRow("Logout") << "RequestLogout" << "logoutRequested(bool)";
56− QTest::newRow("Reboot") << "RequestReboot" << "rebootRequested(bool)";
57− QTest::newRow("Shutdown") << "RequestShutdown" << "shutdownRequested(bool)";
60+ QTest::newRow("Logout") << "RequestLogout" << "LogoutRequested(bool)";
61+ QTest::newRow("Reboot") << "RequestReboot" << "RebootRequested(bool)";
62+ QTest::newRow("Shutdown") << "RequestShutdown" << "ShutdownRequested(bool)";
63+ QTest::newRow("PromptLock") << "PromptLock" << "LockRequested()";
5864 }
5965
6066 void testUnitySessionLogoutRequested() {
@@ -65,7 +71,7 @@ private Q_SLOTS:
6571 QCoreApplication::processEvents(); // to let the service register on DBus
6672
6773 // .. because QSignalSpy checks the signal signature like this: "if (((aSignal[0] - '0') & 0x03) != QSIGNAL_CODE)"
68− QSignalSpy spy(&dbusUnitySessionService, signal.prepend(QSIGNAL_CODE).toLocal8Bit().constData());
74+ QSignalSpy spy(&dbusUnitySessionService, qPrintable(signal.prepend(QSIGNAL_CODE)));
6975
7076 QDBusReply<void> reply = dbusUnitySession->call(method);
7177 QCOMPARE(reply.isValid(), true);
@@ -77,9 +83,9 @@ private Q_SLOTS:
7783 QTest::addColumn<uint>("method");
7884 QTest::addColumn<QString>("signal");
7985
80− QTest::newRow("Logout") << (uint)Action::LOGOUT << "logoutRequested(bool)";
81− QTest::newRow("Shutdown") << (unsigned)Action::SHUTDOWN << "shutdownRequested(bool)";
82− QTest::newRow("Reboot") << (unsigned)Action::REBOOT << "rebootRequested(bool)";
86+ QTest::newRow("Logout") << (uint)Action::LOGOUT << "LogoutRequested(bool)";
87+ QTest::newRow("Shutdown") << (uint)Action::SHUTDOWN << "ShutdownRequested(bool)";
88+ QTest::newRow("Reboot") << (uint)Action::REBOOT << "RebootRequested(bool)";
8389 }
8490
8591 void testGnomeSessionWrapper() {
@@ -92,7 +98,7 @@ private Q_SLOTS:
9298 // Spy on the given signal on the /com/canonical/Unity/Session object
9399 // as proof we are actually calling the actual method.
94100 // .. because QSignalSpy checks the signal signature like this: "if (((aSignal[0] - '0') & 0x03) != QSIGNAL_CODE)"
95− QSignalSpy spy(&dbusUnitySessionService, signal.prepend(QSIGNAL_CODE).toLocal8Bit().constData());
101+ QSignalSpy spy(&dbusUnitySessionService, qPrintable(signal.prepend(QSIGNAL_CODE)));
96102
97103 DBusGnomeSessionManagerWrapper dbusGnomeSessionManagerWrapper;
98104 QCoreApplication::processEvents(); // to let the service register on DBus
@@ -116,6 +122,46 @@ private Q_SLOTS:
116122 QCOMPARE(spy.count(), 1);
117123 }
118124
125+ void testUserName() {
126+ DBusUnitySessionService dbusUnitySessionService;
127+ QCoreApplication::processEvents(); // to let the service register on DBus
128+
129+ QProcess * proc = new QProcess(this);
130+ proc->start("id -un", QProcess::ReadOnly);
131+ proc->waitForFinished();
132+ const QByteArray out = proc->readAll().trimmed();
133+
134+ QCOMPARE(dbusUnitySessionService.UserName(), QString::fromUtf8(out));
135+ }
136+
137+ void testRealName() {
138+ DBusUnitySessionService dbusUnitySessionService;
139+ QCoreApplication::processEvents(); // to let the service register on DBus
140+
141+ QDBusInterface accIface("org.freedesktop.Accounts", "/org/freedesktop/Accounts", "org.freedesktop.Accounts", QDBusConnection::systemBus());
142+ if (accIface.isValid()) {
143+ QDBusReply<QDBusObjectPath> userPath = accIface.asyncCall("FindUserById", static_cast<qint64>(geteuid()));
144+ if (userPath.isValid()) {
145+ QDBusInterface userAccIface("org.freedesktop.Accounts", userPath.value().path(), "org.freedesktop.Accounts.User", QDBusConnection::systemBus());
146+ QCOMPARE(dbusUnitySessionService.RealName(), userAccIface.property("RealName").toString());
147+ }
148+ }
149+ }
150+
151+ void testLogin1Capabilities() {
152+ DBusUnitySessionService dbusUnitySessionService;
153+ QDBusInterface login1face("org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager", QDBusConnection::systemBus());
154+ QCoreApplication::processEvents(); // to let the services register on DBus
155+
156+ if (login1face.isValid()) {
157+ QCOMPARE(dbusUnitySessionService.CanHibernate(), (login1face.call("CanHibernate").arguments().first().toString() != "no"));
158+ QCOMPARE(dbusUnitySessionService.CanSuspend(), (login1face.call("CanSuspend").arguments().first().toString() != "no"));
159+ QCOMPARE(dbusUnitySessionService.CanReboot(), (login1face.call("CanReboot").arguments().first().toString() != "no"));
160+ QCOMPARE(dbusUnitySessionService.CanShutdown(), (login1face.call("CanPowerOff").arguments().first().toString() != "no"));
161+ QCOMPARE(dbusUnitySessionService.CanHybridSleep(), (login1face.call("CanHybridSleep").arguments().first().toString() != "no"));
162+ }
163+ }
164+
119165private:
120166 QDBusInterface *dbusUnitySession;
121167};
tests/qmltests/Components/tst_PhysicalKeysMapper.qml+5
@@ -89,6 +89,11 @@ TestCase {
8989 compare(powerSpy.count, 1);
9090 }
9191
92+ function test_screenshotPrtScr() {
93+ physicalKeysMapper.onKeyPressed({ key: Qt.Key_Print })
94+ tryCompare(screenshotSpy, "count", 1) // verify the screenshotTriggered signal has been called, exactly once
95+ }
96+
9297 function test_screenshotButtons_data() {
9398 return [
9499 { tag: "UpFirst", first: Qt.Key_VolumeUp, second: Qt.Key_VolumeDown },
tests/qmltests/Launcher/tst_Launcher.qml+210−7
@@ -93,6 +93,57 @@ Item {
9393 onClicked: launcherLoader.item.inverted = !launcherLoader.item.inverted
9494 Layout.fillWidth: true
9595 }
96+
97+ Row {
98+ spacing: units.gu(1)
99+
100+ Button {
101+ text: "35% bar"
102+ onClicked: LauncherModel.setProgress(LauncherModel.get(parseInt(appIdEntryBar.displayText)).appId, 35)
103+ Layout.fillWidth: true
104+ }
105+
106+ TextArea {
107+ id: appIdEntryBar
108+ anchors.verticalCenter: parent.verticalCenter
109+ width: units.gu(4)
110+ height: units.gu(4)
111+ autoSize: true
112+ text: "2"
113+ maximumLineCount: 1
114+ }
115+
116+ Button {
117+ text: "no bar"
118+ onClicked: LauncherModel.setProgress(LauncherModel.get(parseInt(appIdEntryBar.displayText)).appId, -1)
119+ Layout.fillWidth: true
120+ }
121+ }
122+
123+ Row {
124+ spacing: units.gu(1)
125+
126+ Button {
127+ text: "set alert"
128+ onClicked: LauncherModel.setAlerting(LauncherModel.get(parseInt(appIdEntryAlert.displayText)).appId, true)
129+ }
130+
131+ TextArea {
132+ id: appIdEntryAlert
133+ anchors.verticalCenter: parent.verticalCenter
134+ width: units.gu(5)
135+ height: units.gu(4)
136+ autoSize: true
137+ text: "2"
138+ maximumLineCount: 1
139+ Layout.fillWidth: true
140+ }
141+
142+ Button {
143+ text: "unset alert"
144+ onClicked: LauncherModel.setAlerting(LauncherModel.get(parseInt(appIdEntryAlert.displayText)).appId, false)
145+ }
146+ }
96147 }
97148
98149 SignalSpy {
@@ -146,7 +197,7 @@ Item {
146197 startX+units.gu(8), startY);
147198
148199 var panel = findChild(launcher, "launcherPanel");
149− verify(panel != undefined);
200+ verify(!!panel);
150201
151202 // wait until it gets fully extended
152203 tryCompare(panel, "x", 0);
@@ -157,7 +208,7 @@ Item {
157208 mouseMove(root, 1, root.height / 2);
158209
159210 var panel = findChild(launcher, "launcherPanel");
160− verify(panel != undefined);
211+ verify(!!panel);
161212
162213 // wait until it gets fully extended
163214 tryCompare(panel, "x", 0);
@@ -169,6 +220,16 @@ Item {
169220 tryCompare(panel, "x", -panel.width, 1000);
170221 }
171222
223+ function waitForWiggleToStart(appIcon) {
224+ verify(appIcon != undefined)
225+ tryCompare(appIcon, "wiggling", true, 1000, "wiggle-anim should not be in stopped state")
226+ }
227+
228+ function waitForWiggleToStop(appIcon) {
229+ verify(appIcon != undefined)
230+ tryCompare(appIcon, "wiggling", false, 1000, "wiggle-anim should not be in running state")
231+ }
232+
172233 function positionLauncherListAtBeginning() {
173234 var listView = testCase.findChild(launcherLoader.item, "launcherListView");
174235 listView.contentY = -listView.topMargin;
@@ -187,7 +248,7 @@ Item {
187248 waitUntilLauncherDisappears();
188249
189250 var panel = findChild(launcher, "launcherPanel")
190− verify(panel != undefined)
251+ verify(!!panel)
191252
192253 // it starts out hidden just left of the left launcher edge
193254 compare(panel.x, -panel.width)
@@ -225,7 +286,7 @@ Item {
225286
226287 var appIcon = findChild(launcher, "launcherDelegate0");
227288
228− verify(appIcon != undefined);
289+ verify(!!appIcon);
229290
230291 if (data.mouse) {
231292 mouseClick(appIcon);
@@ -248,7 +309,7 @@ Item {
248309 dragLauncherIntoView()
249310
250311 var dashIcon = findChild(launcher, "dashItem")
251− verify(dashIcon != undefined)
312+ verify(!!dashIcon)
252313
253314 mouseClick(dashIcon)
254315
@@ -538,7 +599,7 @@ Item {
538599
539600 // Doing longpress
540601 mousePress(draggedItem);
541− tryCompare(quickListShape, "opacity", 0.96);
602+ tryCompare(quickListShape, "opacity", 0.8);
542603 mouseRelease(draggedItem);
543604
544605 verify(quickList.y >= units.gu(1));
@@ -623,9 +684,31 @@ Item {
623684 tryCompare(quickList, "state", "");
624685 }
625686
687+ function test_quickListMenuOnRMB() {
688+ dragLauncherIntoView();
689+ var clickedItem = findChild(launcher, "launcherDelegate5")
690+ var quickList = findChild(launcher, "quickList")
691+ var quickListShape = findChild(launcher, "quickListShape")
692+ var dndArea = findChild(launcher, "dndArea");
693+
694+ // Initial state
695+ tryCompare(quickListShape, "visible", false)
696+
697+ // Doing RMB click
698+ mouseClick(clickedItem, clickedItem.width / 2, clickedItem.height / 2, Qt.RightButton)
699+ tryCompare(quickListShape, "visible", true)
700+ verify(quickList, "state", "open")
701+ verify(dndArea, "dragging", false)
702+
703+ // Click somewhere in the empty space to dismiss the quicklist
704+ mouseClick(launcher, launcher.width - units.gu(1), units.gu(1));
705+ tryCompare(quickListShape, "visible", false);
706+ verify(quickList, "state", "")
707+ }
708+
626709 function test_revealByHover() {
627710 var panel = findChild(launcher, "launcherPanel");
628− verify(panel != undefined);
711+ verify(!!panel);
629712
630713 revealByHover();
631714 tryCompare(launcher, "state", "visibleTemporary");
@@ -636,5 +719,125 @@ Item {
636719 tryCompare(launcher, "state", "", 1000, "Launcher didn't hide after moving mouse away from it");
637720 waitUntilLauncherDisappears();
638721 }
722+
723+ function test_progressChangeViaModel() {
724+ dragLauncherIntoView();
725+ var item = findChild(launcher, "launcherDelegate0")
726+ verify(item != undefined)
727+ LauncherModel.setProgress(LauncherModel.get(0).appId, -1)
728+ compare(findChild(item, "progressOverlay").visible, false)
729+ LauncherModel.setProgress(LauncherModel.get(0).appId, 20)
730+ compare(findChild(item, "progressOverlay").visible, true)
731+ LauncherModel.setProgress(LauncherModel.get(0).appId, 0)
732+ }
733+
734+ function test_alertPeekingIcon() {
735+ var listView = findChild(launcher, "launcherListView")
736+ verify(listView != undefined)
737+ LauncherModel.setAlerting(LauncherModel.get(5).appId, true)
738+ tryCompare(listView, "peekingIndex", 5, 1000, "Wrong appId set as peeking-index")
739+ LauncherModel.setAlerting(LauncherModel.get(5).appId, false)
740+ tryCompare(listView, "peekingIndex", -1, 1000, "peeking-index should be -1")
741+ }
742+
743+ function test_alertHidingIcon() {
744+ var listView = findChild(launcher, "launcherListView")
745+ verify(listView != undefined)
746+ var appIcon6 = findChild(launcher, "launcherDelegate6")
747+ verify(appIcon6 != undefined)
748+ LauncherModel.setAlerting(LauncherModel.get(6).appId, true)
749+ waitForWiggleToStart(appIcon6)
750+ LauncherModel.setAlerting(LauncherModel.get(6).appId, false)
751+ waitForWiggleToStop(appIcon6)
752+ tryCompare(appIcon6, "x", 0, 1000, "x-value of appId #6 should not be non-zero")
753+ waitForRendering(listView)
754+ }
755+
756+ function test_alertIgnoreFocusedApp() {
757+ LauncherModel.setAlerting(LauncherModel.get(0).appId, true)
758+ compare(LauncherModel.get(0).alerting, false, "Focused app should not have the alert-state set")
759+ }
760+
761+ function test_alertOnlyOnePeekingIcon() {
762+ var listView = findChild(launcher, "launcherListView")
763+ verify(listView != undefined)
764+ LauncherModel.setAlerting(LauncherModel.get(3).appId, true)
765+ LauncherModel.setAlerting(LauncherModel.get(1).appId, true)
766+ LauncherModel.setAlerting(LauncherModel.get(5).appId, true)
767+ tryCompare(listView, "peekingIndex", 3, 1000, "Wrong appId set as peeking-index")
768+ LauncherModel.setAlerting(LauncherModel.get(1).appId, false)
769+ LauncherModel.setAlerting(LauncherModel.get(3).appId, false)
770+ LauncherModel.setAlerting(LauncherModel.get(5).appId, false)
771+ tryCompare(listView, "peekingIndex", -1, 1000, "peeking-index should be -1")
772+ waitForRendering(listView)
773+ }
774+
775+ function test_alertMultipleApps() {
776+ LauncherModel.setAlerting(LauncherModel.get(1).appId, true)
777+ LauncherModel.setAlerting(LauncherModel.get(3).appId, true)
778+ LauncherModel.setAlerting(LauncherModel.get(5).appId, true)
779+ LauncherModel.setAlerting(LauncherModel.get(7).appId, true)
780+ compare(LauncherModel.get(1).alerting, true, "Alert-state of appId #1 should not be false")
781+ compare(LauncherModel.get(3).alerting, true, "Alert-state of appId #3 should not be false")
782+ compare(LauncherModel.get(5).alerting, true, "Alert-state of appId #5 should not be false")
783+ compare(LauncherModel.get(7).alerting, true, "Alert-state of appId #7 should not be false")
784+ LauncherModel.setAlerting(LauncherModel.get(1).appId, false)
785+ LauncherModel.setAlerting(LauncherModel.get(3).appId, false)
786+ LauncherModel.setAlerting(LauncherModel.get(5).appId, false)
787+ LauncherModel.setAlerting(LauncherModel.get(7).appId, false)
788+ compare(LauncherModel.get(1).alerting, false, "Alert-state of appId #1 should not be true")
789+ compare(LauncherModel.get(3).alerting, false, "Alert-state of appId #1 should not be true")
790+ compare(LauncherModel.get(5).alerting, false, "Alert-state of appId #1 should not be true")
791+ compare(LauncherModel.get(7).alerting, false, "Alert-state of appId #1 should not be true")
792+ }
793+
794+ function test_alertMoveIconIntoView() {
795+ dragLauncherIntoView();
796+ var appIcon1 = findChild(launcher, "launcherDelegate1");
797+ var appIcon7 = findChild(launcher, "launcherDelegate7");
798+ LauncherModel.setAlerting(LauncherModel.get(1).appId, true)
799+ tryCompare(appIcon1, "angle", 0, 1000, "angle of appId #1 should not be non-zero")
800+ waitForWiggleToStart(appIcon1)
801+ LauncherModel.setAlerting(LauncherModel.get(7).appId, true)
802+ tryCompare(appIcon7, "angle", 0, 1000, "angle of appId #7 should not be non-zero")
803+ waitForWiggleToStart(appIcon7)
804+ LauncherModel.setAlerting(LauncherModel.get(1).appId, false)
805+ waitForWiggleToStop(appIcon1)
806+ LauncherModel.setAlerting(LauncherModel.get(7).appId, false)
807+ waitForWiggleToStop(appIcon7)
808+ }
809+
810+ function test_alertWigglePeekDrag() {
811+ var appIcon5 = findChild(launcher, "launcherDelegate5");
812+ var listView = findChild(launcher, "launcherListView")
813+ verify(listView != undefined)
814+ LauncherModel.setAlerting(LauncherModel.get(5).appId, true)
815+ tryCompare(listView, "peekingIndex", 5, 1000, "Wrong appId set as peeking-index")
816+ waitForWiggleToStart(appIcon5)
817+ tryCompare(appIcon5, "wiggling", true, 1000, "appId #6 should not be still")
818+ dragLauncherIntoView();
819+ tryCompare(listView, "peekingIndex", -1, 1000, "peeking-index should be -1")
820+ LauncherModel.setAlerting(LauncherModel.get(5).appId, false)
821+ waitForWiggleToStop(appIcon5)
822+ tryCompare(appIcon5, "wiggling", false, 1000, "appId #1 should not be wiggling")
823+ }
824+
825+ function test_alertViaCountAndCountVisible() {
826+ dragLauncherIntoView();
827+ var appIcon1 = findChild(launcher, "launcherDelegate1")
828+ var oldCount = LauncherModel.get(1).count
829+ LauncherModel.setCount(LauncherModel.get(1).appId, 42)
830+ tryCompare(appIcon1, "wiggling", false, 1000, "appId #1 should be still")
831+ LauncherModel.setCountVisible(LauncherModel.get(1).appId, 1)
832+ tryCompare(appIcon1, "wiggling", true, 1000, "appId #1 should not be still")
833+ LauncherModel.setAlerting(LauncherModel.get(1).appId, false)
834+ waitForWiggleToStop(appIcon1)
835+ LauncherModel.setCount(LauncherModel.get(1).appId, 4711)
836+ tryCompare(appIcon1, "wiggling", true, 1000, "appId #1 should not be still")
837+ LauncherModel.setAlerting(LauncherModel.get(1).appId, false)
838+ waitForWiggleToStop(appIcon1)
839+ LauncherModel.setCountVisible(LauncherModel.get(1).appId, 0)
840+ LauncherModel.setCount(LauncherModel.get(1).appId, oldCount)
841+ }
639842 }
640843}
tests/qmltests/Panel/tst_Panel.qml+18
@@ -19,6 +19,7 @@ import QtQuick.Layouts 1.1
1919import QtTest 1.0
2020import Unity.Test 0.1
2121import Ubuntu.Components 1.3
22+import Unity.Application 0.1
2223import Unity.Indicators 0.1 as Indicators
2324import Ubuntu.Telephony 0.1 as Telephony
2425import "../../../qml/Panel"
@@ -421,6 +422,23 @@ IndicatorTest {
421422 verify(panel.indicators.fullyClosed);
422423 }
423424
425+ function test_tapToReturnCallDoesntExpandIndicators() {
426+ compare(panel.indicators.shown, false);
427+ verify(panel.indicators.fullyClosed);
428+
429+ callManager.foregroundCall = phoneCall;
430+
431+ ApplicationManager.focusApplication("unity8-dash");
432+ tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
433+
434+ mouseClick(panel.indicators,
435+ panel.indicators.width / 2,
436+ panel.indicators.minimizedPanelHeight / 2);
437+
438+ compare(panel.indicators.shown, false);
439+ verify(panel.indicators.fullyClosed);
440+ }
441+
424442 function test_openAndClosePanelWithMouseClicks() {
425443 compare(panel.indicators.shown, false);
426444 verify(panel.indicators.fullyClosed);
tests/qmltests/Stages/ApplicationCheckBox.qml+21
@@ -86,8 +86,29 @@ RowLayout {
8686 }
8787 }
8888 Label {
89+ id: appIdLabel
8990 text: root.appId
9091 color: "white"
9192 anchors.verticalCenter: parent.verticalCenter
9293 }
94+ Rectangle {
95+ color: {
96+ if (d.application) {
97+ if (d.application.state === ApplicationInfoInterface.Starting) {
98+ return "yellow";
99+ } else if (d.application.state === ApplicationInfoInterface.Running) {
100+ return "green";
101+ } else if (d.application.state === ApplicationInfoInterface.Suspended) {
102+ return "blue";
103+ } else {
104+ return "darkred";
105+ }
106+ } else {
107+ return "darkred";
108+ }
109+ }
110+ width: height
111+ height: appIdLabel.height * 0.7
112+ anchors.verticalCenter: parent.verticalCenter
113+ }
93114}
tests/qmltests/Stages/tst_DesktopStage.qml+2−2
@@ -24,9 +24,8 @@ import Utils 0.1
2424
2525import "../../../qml/Stages"
2626
27−Rectangle {
27+Item {
2828 id: root
29− color: "darkblue"
3029 width: desktopStageLoader.width + controls.width
3130 height: desktopStageLoader.height
3231
@@ -57,6 +56,7 @@ Rectangle {
5756 property bool itemDestroyed: false
5857 sourceComponent: Component {
5958 DesktopStage {
59+ color: "darkblue"
6060 anchors.fill: parent
6161 Component.onDestruction: {
6262 desktopStageLoader.itemDestroyed = true;
tests/qmltests/Stages/tst_PhoneStage.qml+75−3
@@ -301,16 +301,16 @@ Item {
301301 addApps(2);
302302
303303 var secondApp = ApplicationManager.get(0);
304− tryCompare(secondApp, "state", ApplicationInfoInterface.Running);
304+ tryCompare(secondApp, "requestedState", ApplicationInfoInterface.RequestedRunning);
305305 tryCompare(secondApp, "focused", true);
306306
307307 var firstApp = ApplicationManager.get(1);
308− tryCompare(firstApp, "state", ApplicationInfoInterface.Suspended);
308+ tryCompare(firstApp, "requestedState", ApplicationInfoInterface.RequestedSuspended);
309309 tryCompare(firstApp, "focused", false);
310310
311311 ApplicationManager.stopApplication(secondApp.appId);
312312
313− tryCompare(firstApp, "state", ApplicationInfoInterface.Running);
313+ tryCompare(firstApp, "requestedState", ApplicationInfoInterface.RequestedRunning);
314314 tryCompare(firstApp, "focused", true);
315315 }
316316
@@ -388,5 +388,77 @@ Item {
388388
389389 tryCompare(focusedDelegate, "x", 0);
390390 }
391+
392+ function test_focusedAppIsTheOnlyRunningApp() {
393+ addApps(2);
394+
395+ var delegateA = findChild(phoneStage, "appDelegate0");
396+ verify(delegateA);
397+ var delegateB = findChild(phoneStage, "appDelegate1");
398+ verify(delegateB);
399+
400+ // A is focused and running, B is unfocused and suspended
401+ compare(delegateA.focus, true);
402+ compare(delegateA.application.requestedState, ApplicationInfoInterface.RequestedRunning);
403+ compare(delegateB.focus, false);
404+ compare(delegateB.application.requestedState, ApplicationInfoInterface.RequestedSuspended);
405+
406+ // Switch foreground/focused appp from A to B
407+ goToSpread();
408+ phoneStage.select(delegateB.application.appId);
409+
410+ // Now it's the other way round
411+ // A is unfocused and suspended, B is focused and running
412+ tryCompare(delegateA, "focus", false);
413+ tryCompare(delegateA.application, "requestedState", ApplicationInfoInterface.RequestedSuspended);
414+ tryCompare(delegateB, "focus", true);
415+ tryCompare(delegateB.application, "requestedState", ApplicationInfoInterface.RequestedRunning);
416+ }
417+
418+ function test_dashRemainsRunningIfStageIsToldSo() {
419+ addApps(1);
420+
421+ var delegateDash = findChild(phoneStage, "appDelegate1");
422+ verify(delegateDash);
423+ compare(delegateDash.application.appId, "unity8-dash");
424+
425+ var delegateOther = findChild(phoneStage, "appDelegate0");
426+ verify(delegateOther);
427+
428+ goToSpread();
429+ phoneStage.select("unity8-dash");
430+
431+ tryCompare(delegateDash, "focus", true);
432+ tryCompare(delegateDash.application, "requestedState", ApplicationInfoInterface.RequestedRunning);
433+ compare(delegateOther.focus, false);
434+ compare(delegateOther.application.requestedState, ApplicationInfoInterface.RequestedSuspended);
435+
436+ goToSpread();
437+ phoneStage.select(delegateOther.application.appId);
438+
439+ // The other app gets focused and running but dash is kept running despite being unfocused
440+ tryCompare(delegateDash, "focus", false);
441+ tryCompare(delegateDash.application, "requestedState", ApplicationInfoInterface.RequestedRunning);
442+ compare(delegateOther.focus, true);
443+ compare(delegateOther.application.requestedState, ApplicationInfoInterface.RequestedRunning);
444+ }
445+
446+ function test_foregroundAppIsSuspendedWhenStageIsSuspended() {
447+ addApps(1);
448+
449+ var delegate = findChild(phoneStage, "appDelegate0");
450+ verify(delegate);
451+
452+ compare(delegate.focus, true);
453+ compare(delegate.application.requestedState, ApplicationInfoInterface.RequestedRunning);
454+
455+ phoneStage.suspended = true;
456+
457+ tryCompare(delegate.application, "requestedState", ApplicationInfoInterface.RequestedSuspended);
458+
459+ phoneStage.suspended = false;
460+
461+ tryCompare(delegate.application, "requestedState", ApplicationInfoInterface.RequestedRunning);
462+ }
391463 }
392464}
tests/qmltests/Stages/tst_TabletStage.qml+161−1
@@ -76,10 +76,18 @@ Rectangle {
7676 id: webbrowserCheckBox
7777 appId: "webbrowser-app"
7878 }
79+ ApplicationCheckBox {
80+ id: galleryCheckBox
81+ appId: "gallery-app"
82+ }
7983 ApplicationCheckBox {
8084 id: dialerCheckBox
8185 appId: "dialer-app"
8286 }
87+ ApplicationCheckBox {
88+ id: facebookCheckBox
89+ appId: "facebook-webapp"
90+ }
8391 }
8492 }
8593
@@ -93,6 +101,19 @@ Rectangle {
93101 function init() {
94102 tabletStageLoader.active = true;
95103 tryCompare(tabletStageLoader, "status", Loader.Ready);
104+
105+ // this is very strange, but sometimes the test starts without
106+ // TabletStage components having finished loading themselves
107+ var appWindow = null;
108+ while (!appWindow) {
109+ appWindow = findChild(tabletStage, "appWindow_unity8-dash");
110+ if (!appWindow) {
111+ console.log("didn't find appWindow_unity8-dash in " + tabletStage + ". Trying again...");
112+ wait(50);
113+ }
114+ }
115+
116+ waitUntilAppSurfaceShowsUp("unity8-dash");
96117 }
97118
98119 function cleanup() {
@@ -109,7 +130,9 @@ Rectangle {
109130
110131 // kill all (fake) running apps
111132 webbrowserCheckBox.checked = false;
133+ galleryCheckBox.checked = false;
112134 dialerCheckBox.checked = false;
135+ facebookCheckBox.checked = false;
113136 }
114137
115138 function waitUntilAppSurfaceShowsUp(appId) {
@@ -120,6 +143,34 @@ Rectangle {
120143 tryCompare(appWindowStates, "state", "surface");
121144 }
122145
146+ function switchToApp(targetAppId) {
147+ touchFlick(tabletStage,
148+ tabletStage.width - (tabletStage.dragAreaWidth / 2), tabletStage.height / 2,
149+ tabletStage.x + 1, tabletStage.height / 2);
150+
151+ var spreadView = findChild(tabletStage, "spreadView");
152+ verify(spreadView);
153+ tryCompare(spreadView, "phase", 2);
154+ tryCompare(spreadView, "flicking", false);
155+ tryCompare(spreadView, "moving", false);
156+
157+ waitUntilAppDelegateStopsMoving(targetAppId);
158+
159+ var targetAppWindow = findChild(tabletStage, "appWindow_" + targetAppId);
160+ tap(targetAppWindow, 10, 10);
161+ }
162+
163+ function waitUntilAppDelegateStopsMoving(targetAppId)
164+ {
165+ var targetAppDelegate = findChild(tabletStage, "tabletSpreadDelegate_" + targetAppId);
166+ verify(targetAppDelegate);
167+ var lastValue = undefined;
168+ do {
169+ lastValue = targetAppDelegate.animatedProgress;
170+ wait(30);
171+ } while (lastValue != targetAppDelegate.animatedProgress);
172+ }
173+
123174 function test_tappingSwitchesFocusBetweenStages() {
124175 webbrowserCheckBox.checked = true;
125176 waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId);
@@ -155,7 +206,6 @@ Rectangle {
155206 tryCompare(webbrowserApp.session.surface, "activeFocus", false);
156207 }
157208
158−
159209 function test_closeAppInSideStage() {
160210 dialerCheckBox.checked = true;
161211 waitUntilAppSurfaceShowsUp(dialerCheckBox.appId);
@@ -200,6 +250,116 @@ Rectangle {
200250 appWindow.width / 2, appWindow.height / 2,
201251 appWindow.width / 2, -appWindow.height / 2);
202252 }
253+
254+ function test_suspendsAndResumesAppsInMainStage() {
255+ webbrowserCheckBox.checked = true;
256+ var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
257+ compare(webbrowserApp.stage, ApplicationInfoInterface.MainStage);
258+
259+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Running);
260+
261+ galleryCheckBox.checked = true;
262+ var galleryApp = ApplicationManager.findApplication(galleryCheckBox.appId);
263+ compare(galleryApp.stage, ApplicationInfoInterface.MainStage);
264+
265+ tryCompare(galleryApp, "state", ApplicationInfoInterface.Running);
266+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
267+
268+ switchToApp(webbrowserApp.appId);
269+
270+ tryCompare(galleryApp, "state", ApplicationInfoInterface.Suspended);
271+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Running);
272+
273+ switchToApp(galleryApp.appId);
274+
275+ tryCompare(galleryApp, "state", ApplicationInfoInterface.Running);
276+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
277+ }
278+
279+
280+ function test_foregroundMainAndSideStageAppsAreKeptRunning() {
281+
282+ var stagesPriv = findInvisibleChild(tabletStage, "stagesPriv");
283+ verify(stagesPriv);
284+
285+ // launch two main stage apps
286+ // gallery will be on foreground and webbrowser on background
287+
288+ webbrowserCheckBox.checked = true;
289+ waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId)
290+ var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
291+ compare(webbrowserApp.stage, ApplicationInfoInterface.MainStage);
292+
293+ galleryCheckBox.checked = true;
294+ waitUntilAppSurfaceShowsUp(galleryCheckBox.appId)
295+ var galleryApp = ApplicationManager.findApplication(galleryCheckBox.appId);
296+ compare(galleryApp.stage, ApplicationInfoInterface.MainStage);
297+
298+ compare(stagesPriv.mainStageAppId, galleryCheckBox.appId);
299+
300+ // then launch two side stage apps
301+ // facebook will be on foreground and dialer on background
302+
303+ dialerCheckBox.checked = true;
304+ waitUntilAppSurfaceShowsUp(dialerCheckBox.appId)
305+ var dialerApp = ApplicationManager.findApplication(dialerCheckBox.appId);
306+ compare(dialerApp.stage, ApplicationInfoInterface.SideStage);
307+
308+ facebookCheckBox.checked = true;
309+ waitUntilAppSurfaceShowsUp(facebookCheckBox.appId)
310+ var facebookApp = ApplicationManager.findApplication(facebookCheckBox.appId);
311+ compare(facebookApp.stage, ApplicationInfoInterface.SideStage);
312+
313+ compare(stagesPriv.sideStageAppId, facebookCheckBox.appId);
314+
315+ // Now check that the foreground apps are running and that the background ones
316+ // are suspended
317+
318+ tryCompare(galleryApp, "state", ApplicationInfoInterface.Running);
319+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
320+ tryCompare(facebookApp, "state", ApplicationInfoInterface.Running);
321+ tryCompare(dialerApp, "state", ApplicationInfoInterface.Suspended);
322+
323+ switchToApp(dialerCheckBox.appId);
324+
325+ tryCompare(galleryApp, "state", ApplicationInfoInterface.Running);
326+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
327+ tryCompare(facebookApp, "state", ApplicationInfoInterface.Suspended);
328+ tryCompare(dialerApp, "state", ApplicationInfoInterface.Running);
329+
330+ switchToApp(webbrowserCheckBox.appId);
331+
332+ tryCompare(galleryApp, "state", ApplicationInfoInterface.Suspended);
333+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Running);
334+ tryCompare(facebookApp, "state", ApplicationInfoInterface.Suspended);
335+ tryCompare(dialerApp, "state", ApplicationInfoInterface.Running);
336+ }
337+
338+ function test_foregroundAppsAreSuspendedWhenStageIsSuspended() {
339+ webbrowserCheckBox.checked = true;
340+ waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId)
341+ var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
342+ compare(webbrowserApp.stage, ApplicationInfoInterface.MainStage);
343+
344+ dialerCheckBox.checked = true;
345+ waitUntilAppSurfaceShowsUp(dialerCheckBox.appId)
346+ var dialerApp = ApplicationManager.findApplication(dialerCheckBox.appId);
347+ compare(dialerApp.stage, ApplicationInfoInterface.SideStage);
348+
349+
350+ compare(webbrowserApp.requestedState, ApplicationInfoInterface.RequestedRunning);
351+ compare(dialerApp.requestedState, ApplicationInfoInterface.RequestedRunning);
352+
353+ tabletStage.suspended = true;
354+
355+ tryCompare(webbrowserApp, "requestedState", ApplicationInfoInterface.RequestedSuspended);
356+ tryCompare(dialerApp, "requestedState", ApplicationInfoInterface.RequestedSuspended);
357+
358+ tabletStage.suspended = false;
359+
360+ tryCompare(webbrowserApp, "requestedState", ApplicationInfoInterface.RequestedRunning);
361+ tryCompare(dialerApp, "requestedState", ApplicationInfoInterface.RequestedRunning);
362+ }
203363 }
204364
205365}
tests/qmltests/tst_OrientedShell.qml+3−3
@@ -46,7 +46,7 @@ Rectangle {
4646 }
4747
4848 QtObject {
49− id: mockUsageModeSettings
49+ id: mockUnity8Settings
5050 property string usageMode: usageModeSelector.model[usageModeSelector.selectedIndex]
5151 }
5252
@@ -125,7 +125,7 @@ Rectangle {
125125 sourceComponent: Component {
126126 OrientedShell {
127127 anchors.fill: parent
128− usageModeSettings: mockUsageModeSettings
128+ unity8Settings: mockUnity8Settings
129129 oskSettings: mockOskSettings
130130 physicalOrientation: root.physicalOrientation0
131131 orientationLocked: orientationLockedCheckBox.checked
@@ -926,7 +926,7 @@ Rectangle {
926926
927927 function test_attachRemoveInputDevices() {
928928 usageModeSelector.selectedIndex = 2;
929− tryCompare(mockUsageModeSettings, "usageMode", "Automatic")
929+ tryCompare(mockUnity8Settings, "usageMode", "Automatic")
930930
931931 loadShell("mako")
932932 var shell = findChild(orientedShell, "shell");
tests/qmltests/tst_Shell.qml+390−23
@@ -47,6 +47,7 @@ Rectangle {
4747 }
4848
4949 Item {
50+ id: shellContainer
5051 anchors.left: root.left
5152 anchors.right: controls.left
5253 anchors.top: root.top
@@ -74,6 +75,18 @@ Rectangle {
7475 width: units.gu(100)
7576 height: units.gu(71)
7677 }
78+ },
79+ State {
80+ name: "desktop"
81+ PropertyChanges {
82+ target: shellLoader
83+ width: shellContainer.width
84+ height: shellContainer.height
85+ }
86+ PropertyChanges {
87+ target: mouseEmulation
88+ checked: false
89+ }
7790 }
7891 ]
7992
@@ -81,6 +94,7 @@ Rectangle {
8194 property bool itemDestroyed: false
8295 sourceComponent: Component {
8396 Shell {
97+ id: __shell
8498 usageScenario: usageScenarioSelector.model[usageScenarioSelector.selectedIndex]
8599 orientation: Qt.PortraitOrientation
86100 primaryOrientation: Qt.PortraitOrientation
@@ -88,6 +102,10 @@ Rectangle {
88102 Component.onDestruction: {
89103 shellLoader.itemDestroyed = true;
90104 }
105+ Component.onCompleted: {
106+ var keyMapper = testCase.findChild(__shell, "physicalKeysMapper");
107+ keyMapper.controlInsteadOfAlt = true;
108+ }
91109 }
92110 }
93111 }
@@ -112,16 +130,32 @@ Rectangle {
112130 anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
113131 spacing: units.gu(1)
114132
115− Button {
116− text: "Show Greeter"
117− activeFocusOnPress: false
118− onClicked: {
119− if (shellLoader.status !== Loader.Ready)
120− return;
121−
122− var greeter = testCase.findChild(shellLoader.item, "greeter");
123− if (!greeter.shown) {
124− LightDM.Greeter.showGreeter();
133+ Row {
134+ spacing: units.gu(1)
135+ Button {
136+ text: "Show Greeter"
137+ activeFocusOnPress: false
138+ onClicked: {
139+ if (shellLoader.status !== Loader.Ready)
140+ return;
141+
142+ var greeter = testCase.findChild(shellLoader.item, "greeter");
143+ if (!greeter.shown) {
144+ LightDM.Greeter.showGreeter();
145+ }
146+ }
147+ }
148+ Button {
149+ text: "Hide Greeter"
150+ activeFocusOnPress: false
151+ onClicked: {
152+ if (shellLoader.status !== Loader.Ready)
153+ return;
154+
155+ var greeter = testCase.findChild(shellLoader.item, "greeter");
156+ if (greeter.shown) {
157+ greeter.hide()
158+ }
125159 }
126160 }
127161 }
@@ -141,7 +175,7 @@ Rectangle {
141175 anchors { left: parent.left; right: parent.right }
142176 activeFocusOnPress: false
143177 text: "Size"
144− model: ["phone", "tablet"]
178+ model: ["phone", "tablet", "desktop"]
145179 onSelectedIndexChanged: {
146180 shellLoader.active = false;
147181 shellLoader.state = model[selectedIndex];
@@ -163,6 +197,17 @@ Rectangle {
163197
164198 Label { text: "Applications"; font.bold: true }
165199
200+ Button {
201+ text: "Start all apps"
202+ width: parent.width
203+ onClicked: {
204+ for (var i = 0; i < ApplicationManager.availableApplications.length; i++) {
205+ var appId = ApplicationManager.availableApplications[i];
206+ ApplicationManager.startApplication(appId)
207+ }
208+ }
209+ }
210+
166211 Repeater {
167212 id: appRepeater
168213 model: ApplicationManager.availableApplications
@@ -226,6 +271,12 @@ Rectangle {
226271 signalName: "actionInvoked"
227272 }
228273
274+ SignalSpy {
275+ id: appRemovedSpy
276+ target: ApplicationManager
277+ signalName: "applicationRemoved"
278+ }
279+
229280 Telephony.CallEntry {
230281 id: phoneCall
231282 phoneNumber: "+447812221111"
@@ -248,9 +299,9 @@ Rectangle {
248299 }
249300
250301 function cleanup() {
302+ mouseEmulation.checked = true;
251303 tryCompare(shell, "enabled", true); // make sure greeter didn't leave us in disabled state
252304 tearDown();
253− GSettingsController.setUsageMode("Staged");
254305 }
255306
256307 function loadShell(formFactor) {
@@ -269,6 +320,20 @@ Rectangle {
269320 waitForGreeterToStabilize();
270321 }
271322
323+ function loadDesktopShellWithApps() {
324+ loadShell("desktop");
325+ shell.usageScenario = "desktop"
326+ waitForRendering(root)
327+ var app1 = ApplicationManager.startApplication("dialer-app")
328+ var app2 = ApplicationManager.startApplication("webbrowser-app")
329+ var app3 = ApplicationManager.startApplication("camera-app")
330+ var app4 = ApplicationManager.startApplication("facebook-webapp")
331+ var app5 = ApplicationManager.startApplication("calendar-app")
332+ var app6 = ApplicationManager.startApplication("gallery-app")
333+ var app7 = ApplicationManager.startApplication("camera-app")
334+ waitUntilAppWindowIsFullyLoaded(app7);
335+ }
336+
272337 function waitForGreeterToStabilize() {
273338 var greeter = findChild(shell, "greeter");
274339 verify(greeter);
@@ -472,6 +537,41 @@ Rectangle {
472537 confirmLoggedIn(data.loggedIn)
473538 }
474539
540+ function test_longLeftEdgeSwipeTakesToAppsAndResetSearchString() {
541+ loadShell("phone");
542+ swipeAwayGreeter();
543+ dragLauncherIntoView();
544+ dashCommunicatorSpy.clear();
545+
546+ tapOnAppIconInLauncher();
547+ waitUntilApplicationWindowIsFullyVisible();
548+
549+ verify(ApplicationManager.focusedApplicationId !== "unity8-dash")
550+
551+ //Long left swipe
552+ swipeFromLeftEdge(units.gu(30));
553+
554+ tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
555+
556+ compare(dashCommunicatorSpy.count, 1);
557+ }
558+
559+ function test_ClickUbuntuIconInLauncherTakesToAppsAndResetSearchString() {
560+ loadShell("phone");
561+ swipeAwayGreeter();
562+ dragLauncherIntoView();
563+ dashCommunicatorSpy.clear();
564+
565+ var launcher = findChild(shell, "launcher");
566+ var dashIcon = findChild(launcher, "dashItem");
567+ verify(dashIcon != undefined);
568+ mouseClick(dashIcon);
569+
570+ tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
571+
572+ compare(dashCommunicatorSpy.count, 1);
573+ }
574+
475575 function test_suspend() {
476576 loadShell("phone");
477577 swipeAwayGreeter();
@@ -486,9 +586,9 @@ Rectangle {
486586 verify(mainAppId != "");
487587 var mainApp = ApplicationManager.findApplication(mainAppId);
488588 verify(mainApp);
489− tryCompare(mainApp, "state", ApplicationInfoInterface.Running);
589+ tryCompare(mainApp, "requestedState", ApplicationInfoInterface.RequestedRunning);
490590
491− // Suspend while call is active...
591+ // Display off while call is active...
492592 callManager.foregroundCall = phoneCall;
493593 Powerd.status = Powerd.Off;
494594 tryCompare(greeter, "shown", false);
@@ -499,8 +599,7 @@ Rectangle {
499599 Powerd.status = Powerd.Off;
500600 tryCompare(greeter, "fullyShown", true);
501601
502− tryCompare(ApplicationManager, "suspended", true);
503− compare(mainApp.state, ApplicationInfoInterface.Suspended);
602+ compare(mainApp.requestedState, ApplicationInfoInterface.RequestedSuspended);
504603
505604 // And wake up
506605 Powerd.status = Powerd.On;
@@ -516,8 +615,7 @@ Rectangle {
516615 removeTimeConstraintsFromDirectionalDragAreas(greeter);
517616 swipeAwayGreeter();
518617
519− tryCompare(ApplicationManager, "suspended", false);
520− compare(mainApp.state, ApplicationInfoInterface.Running);
618+ compare(mainApp.requestedState, ApplicationInfoInterface.RequestedRunning);
521619 tryCompare(ApplicationManager, "focusedApplicationId", mainAppId);
522620 }
523621
@@ -688,15 +786,15 @@ Rectangle {
688786
689787 function test_launchedAppHasActiveFocus_data() {
690788 return [
691− {tag: "phone", formFactor: "phone", usageMode: "Staged"},
692− {tag: "tablet", formFactor: "tablet", usageMode: "Staged"},
693− {tag: "desktop", formFactor: "tablet", usageMode: "Windowed"}
789+ {tag: "phone", formFactor: "phone", usageScenario: "phone"},
790+ {tag: "tablet", formFactor: "tablet", usageScenario: "tablet"},
791+ {tag: "desktop", formFactor: "tablet", usageScenario: "desktop"}
694792 ]
695793 }
696794
697795 function test_launchedAppHasActiveFocus(data) {
698− GSettingsController.setUsageMode(data.usageMode);
699796 loadShell(data.formFactor);
797+ shell.usageScenario = data.usageScenario;
700798 swipeAwayGreeter();
701799
702800 var webApp = ApplicationManager.startApplication("webbrowser-app");
@@ -724,7 +822,7 @@ Rectangle {
724822
725823 // check that the desktop stage and window have been loaded
726824 {
727− var desktopWindow = findChild(shell, "decoratedWindow_webbrowser-app");
825+ var desktopWindow = findChild(shell, "appWindow_webbrowser-app");
728826 verify(desktopWindow);
729827 }
730828
@@ -1082,6 +1180,8 @@ Rectangle {
10821180 }
10831181
10841182 function test_tapUbuntuIconInLauncherOverAppSpread() {
1183+ launcherShowDashHomeSpy.clear();
1184+
10851185 loadShell("phone");
10861186 swipeAwayGreeter();
10871187
@@ -1228,5 +1328,272 @@ Rectangle {
12281328 var launcher = findChild(shell, "launcher");
12291329 compare(launcher.inverted, data.launcherInverted);
12301330 }
1331+
1332+ function test_unfocusedAppsGetSuspendedAfterEnteringStagedMode() {
1333+ loadShell("tablet");
1334+ shell.usageScenario = "desktop";
1335+
1336+ var webBrowserApp = ApplicationManager.startApplication("webbrowser-app");
1337+ waitUntilAppWindowIsFullyLoaded(webBrowserApp);
1338+
1339+ var galleryApp = ApplicationManager.startApplication("gallery-app");
1340+ waitUntilAppWindowIsFullyLoaded(galleryApp);
1341+
1342+ ApplicationManager.requestFocusApplication("unity8-dash");
1343+ tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
1344+
1345+ compare(webBrowserApp.requestedState, ApplicationInfoInterface.RequestedRunning);
1346+ compare(galleryApp.requestedState, ApplicationInfoInterface.RequestedRunning);
1347+
1348+ shell.usageScenario = "tablet";
1349+
1350+ tryCompare(webBrowserApp, "requestedState", ApplicationInfoInterface.RequestedSuspended);
1351+ tryCompare(galleryApp, "requestedState", ApplicationInfoInterface.RequestedSuspended);
1352+ }
1353+
1354+ function test_unfocusedAppsAreResumedWhenEnteringWindowedMode() {
1355+ loadShell("tablet");
1356+ shell.usageScenario = "tablet";
1357+
1358+ var webBrowserApp = ApplicationManager.startApplication("webbrowser-app");
1359+ waitUntilAppWindowIsFullyLoaded(webBrowserApp);
1360+
1361+ var galleryApp = ApplicationManager.startApplication("gallery-app");
1362+ waitUntilAppWindowIsFullyLoaded(galleryApp);
1363+
1364+ ApplicationManager.requestFocusApplication("unity8-dash");
1365+ tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
1366+
1367+ compare(webBrowserApp.requestedState, ApplicationInfoInterface.RequestedSuspended);
1368+ compare(galleryApp.requestedState, ApplicationInfoInterface.RequestedSuspended);
1369+
1370+ shell.usageScenario = "desktop";
1371+
1372+ tryCompare(webBrowserApp, "requestedState", ApplicationInfoInterface.RequestedRunning);
1373+ tryCompare(galleryApp, "requestedState", ApplicationInfoInterface.RequestedRunning);
1374+ }
1375+
1376+ function test_altTabSwitchesFocus() {
1377+ loadShell("desktop");
1378+ shell.usageScenario = "desktop"
1379+ waitForRendering(root)
1380+
1381+ var desktopStage = findChild(shell, "stage");
1382+ verify(desktopStage != null)
1383+
1384+ var app1 = ApplicationManager.startApplication("dialer-app")
1385+ waitUntilAppWindowIsFullyLoaded(app1);
1386+ var app2 = ApplicationManager.startApplication("webbrowser-app")
1387+ waitUntilAppWindowIsFullyLoaded(app2);
1388+ var app3 = ApplicationManager.startApplication("camera-app")
1389+ waitUntilAppWindowIsFullyLoaded(app3);
1390+
1391+ // Do a quick alt-tab and see if focus changes
1392+ tryCompare(app3.session.surface, "activeFocus", true)
1393+ keyClick(Qt.Key_Tab, Qt.ControlModifier)
1394+ tryCompare(app2.session.surface, "activeFocus", true)
1395+
1396+
1397+ tryCompare(desktopStage, "state", "")
1398+
1399+ // Just press Alt, make sure the spread comes up
1400+ keyPress(Qt.Key_Control);
1401+ keyClick(Qt.Key_Tab);
1402+ tryCompare(desktopStage, "state", "altTab")
1403+
1404+ // Release control, check if spread disappears
1405+ keyRelease(Qt.Key_Control)
1406+ tryCompare(desktopStage, "state", "")
1407+
1408+ // Focus should have switched back now
1409+ tryCompare(app3.session.surface, "activeFocus", true)
1410+ }
1411+
1412+ function test_altTabWrapAround() {
1413+ loadDesktopShellWithApps();
1414+
1415+ var desktopStage = findChild(shell, "stage");
1416+ verify(desktopStage !== null)
1417+
1418+ var appContainer = findInvisibleChild(shell, "appContainer")
1419+ verify(appContainer !== null)
1420+
1421+ var appRepeater = findInvisibleChild(shell, "appRepeater")
1422+ verify(appRepeater !== null)
1423+
1424+ // remember the focused appId
1425+ var focused = ApplicationManager.get(ApplicationManager.findApplication(ApplicationManager.focusedApplicationId));
1426+
1427+ tryCompare(desktopStage, "state", "")
1428+
1429+ // Just press Alt, make sure the spread comes up
1430+ keyPress(Qt.Key_Control);
1431+ keyClick(Qt.Key_Tab);
1432+ tryCompare(desktopStage, "state", "altTab")
1433+ tryCompare(appRepeater, "highlightedIndex", 1)
1434+ waitForRendering(shell)
1435+
1436+ // Now press and hold Tab, make sure the highlight moves all the way but stops at the last one
1437+ // We can't simulate a pressed key with keyPress() currently, so let's inject the events
1438+ // at API level. Jump for 10 times, verify that it's still at the last one and didn't wrap around.
1439+ for (var i = 0; i < 10; i++) {
1440+ appContainer.selectNext(true); // true == isAutoRepeat
1441+ wait(0); // Trigger the event loop to make sure all the things happen
1442+ }
1443+ tryCompare(appRepeater, "highlightedIndex", 6)
1444+
1445+ // Now release it once, and verify that it does wrap around with an additional Tab press
1446+ keyRelease(Qt.Key_Tab);
1447+ keyClick(Qt.Key_Tab);
1448+ tryCompare(appRepeater, "highlightedIndex", 0)
1449+
1450+ // Release control, check if spread disappears
1451+ keyRelease(Qt.Key_Control)
1452+ tryCompare(desktopStage, "state", "")
1453+
1454+ // Make sure that after wrapping around once, we have the same one focused as at the beginning
1455+ tryCompare(focused.session.surface, "activeFocus", true)
1456+ }
1457+
1458+ function test_altBackTabNavigation() {
1459+ loadDesktopShellWithApps();
1460+
1461+ var appRepeater = findInvisibleChild(shell, "appRepeater");
1462+ verify(appRepeater !== null);
1463+
1464+ keyPress(Qt.Key_Control)
1465+ keyClick(Qt.Key_Tab);
1466+ tryCompare(appRepeater, "highlightedIndex", 1);
1467+
1468+ keyClick(Qt.Key_Tab);
1469+ tryCompare(appRepeater, "highlightedIndex", 2);
1470+
1471+ keyClick(Qt.Key_Tab);
1472+ tryCompare(appRepeater, "highlightedIndex", 3);
1473+
1474+ keyClick(Qt.Key_Tab);
1475+ tryCompare(appRepeater, "highlightedIndex", 4);
1476+
1477+ keyClick(Qt.Key_Backtab);
1478+ tryCompare(appRepeater, "highlightedIndex", 3);
1479+
1480+ keyClick(Qt.Key_Backtab);
1481+ tryCompare(appRepeater, "highlightedIndex", 2);
1482+
1483+ keyClick(Qt.Key_Backtab);
1484+ tryCompare(appRepeater, "highlightedIndex", 1);
1485+
1486+ keyRelease(Qt.Key_Control);
1487+ }
1488+
1489+ function test_highlightFollowsMouse() {
1490+ loadDesktopShellWithApps()
1491+
1492+ var appRepeater = findInvisibleChild(shell, "appRepeater");
1493+ verify(appRepeater !== null);
1494+
1495+ keyPress(Qt.Key_Control)
1496+ keyClick(Qt.Key_Tab);
1497+
1498+ tryCompare(appRepeater, "highlightedIndex", 1);
1499+
1500+ var x = 0;
1501+ var y = shell.height * .75;
1502+ mouseMove(shell, x, y)
1503+
1504+ for (var i = 0; i < 7; i++) {
1505+ while (appRepeater.highlightedIndex != i && x <= 4000) {
1506+ x+=10;
1507+ mouseMove(shell, x, y)
1508+ waitForRendering(shell)
1509+ }
1510+ }
1511+
1512+ verify(y < 4000);
1513+
1514+ keyRelease(Qt.Key_Control);
1515+ }
1516+
1517+ function test_closeFromSpread() {
1518+ loadDesktopShellWithApps()
1519+
1520+ var appRepeater = findInvisibleChild(shell, "appRepeater");
1521+ verify(appRepeater !== null);
1522+
1523+ keyPress(Qt.Key_Control)
1524+ keyClick(Qt.Key_Tab);
1525+
1526+ appRemovedSpy.clear();
1527+
1528+ var closedAppId = ApplicationManager.get(2).appId;
1529+ var appDelegate2 = appRepeater.itemAt(2);
1530+ var closeMouseArea = findChild(appDelegate2, "closeMouseArea");
1531+
1532+ // Move the mosue over tile 2 and verify the close button becomes visible
1533+ var x = 0;
1534+ var y = shell.height * .5;
1535+ mouseMove(shell, x, y)
1536+ while (appRepeater.highlightedIndex !== 2 && x <= 4000) {
1537+ x+=10;
1538+ mouseMove(shell, x, y)
1539+ waitForRendering(shell)
1540+ }
1541+ tryCompare(closeMouseArea, "enabled", true)
1542+
1543+ // Close the app using the close button
1544+ mouseClick(closeMouseArea, closeMouseArea.width / 2, closeMouseArea.height / 2)
1545+
1546+ // Verify applicationRemoved has been emitted correctly
1547+ tryCompare(appRemovedSpy, "count", 1)
1548+ compare(appRemovedSpy.signalArguments[0][0], closedAppId);
1549+
1550+ keyRelease(Qt.Key_Control);
1551+ }
1552+
1553+ function test_selectFromSpreadWithMouse_data() {
1554+ return [
1555+ {tag: "click on tileInfo", tileInfo: true },
1556+ {tag: "click on surface", tileInfo: false },
1557+ ]
1558+ }
1559+
1560+ function test_selectFromSpreadWithMouse(data) {
1561+ loadDesktopShellWithApps()
1562+
1563+ var appRepeater = findInvisibleChild(shell, "appRepeater");
1564+ verify(appRepeater !== null);
1565+
1566+ keyPress(Qt.Key_Control)
1567+ keyClick(Qt.Key_Tab);
1568+
1569+ appRemovedSpy.clear();
1570+
1571+ var focusAppId = ApplicationManager.get(2).appId;
1572+ var appDelegate2 = appRepeater.itemAt(2);
1573+ var decoratedWindow = findChild(appDelegate2, "decoratedWindow");
1574+ var stage = findChild(shell, "stage");
1575+
1576+ tryCompare(stage, "state", "altTab");
1577+
1578+ // Move the mouse over tile 2 and verify the highlight becomes visible
1579+ var x = 0;
1580+ var y = shell.height * (data.tileInfo ? .5 : 0.95)
1581+ mouseMove(shell, x, y)
1582+ while (appRepeater.highlightedIndex !== 2 && x <= 4000) {
1583+ x+=10;
1584+ mouseMove(shell, x, y)
1585+ waitForRendering(shell)
1586+ }
1587+ tryCompare(decoratedWindow, "highlightShown", true);
1588+
1589+ // Click the tile
1590+ mouseClick(decoratedWindow, decoratedWindow.width / 2, decoratedWindow.height / 2)
1591+
1592+ // Verify that we left the spread and app2 is the focused one now
1593+ tryCompare(stage, "state", "");
1594+ tryCompare(ApplicationManager, "focusedApplicationId", focusAppId);
1595+
1596+ keyRelease(Qt.Key_Control);
1597+ }
12311598 }
12321599}
tests/scripts/READMEnew file+9
@@ -0,0 +1,9 @@
1+This directory holds a few handy shell-scripts to simplify runtime-testing of DBus-APIs (requires qdbus-qt5 to be installed).
2+
3+launcher-icon related:
4+ * list-launcher-icons.sh - get all icons of unity8-launcher
5+ * alert-launcher-icon.sh - trigger alert/wiggle of unfocused launcher-icon
6+ * get-progress.sh - get progress-value of a launcher-icon
7+ * set-progress.sh - set progress-value of a launcher-icon
8+ * set-count.sh - set the count of a launcher-icon
9+ * set-count-visible.sh - set the count-visible flag of a launcher-icon
tests/scripts/alert-launcher-icon.shnew file+3
@@ -0,0 +1,3 @@
1+#!/bin/sh
2+
3+qdbus com.canonical.Unity.Launcher /com/canonical/Unity/Launcher/$1 com.canonical.Unity.Launcher.Item.Alert
tests/scripts/get-progress.shnew file+3
@@ -0,0 +1,3 @@
1+#!/bin/sh
2+
3+qdbus com.canonical.Unity.Launcher /com/canonical/Unity/Launcher/$1 org.freedesktop.DBus.Properties.Get com.canonical.Unity.Launcher.Item progress
tests/scripts/list-launcher-icons.shnew file+3
@@ -0,0 +1,3 @@
1+#!/bin/sh
2+
3+qdbus com.canonical.Unity | grep Launcher | cut -f6 -d/
tests/scripts/set-count-visible.shnew file+3
@@ -0,0 +1,3 @@
1+#!/bin/sh
2+
3+qdbus com.canonical.Unity.Launcher /com/canonical/Unity/Launcher/$1 org.freedesktop.DBus.Properties.Set com.canonical.Unity.Launcher.Item countVisible $2
tests/scripts/set-count.shnew file+3
@@ -0,0 +1,3 @@
1+#!/bin/sh
2+
3+qdbus com.canonical.Unity.Launcher /com/canonical/Unity/Launcher/$1 org.freedesktop.DBus.Properties.Set com.canonical.Unity.Launcher.Item count $2
tests/scripts/set-progress.shnew file+3
@@ -0,0 +1,3 @@
1+#!/bin/sh
2+
3+qdbus com.canonical.Unity.Launcher /com/canonical/Unity/Launcher/$1 org.freedesktop.DBus.Properties.Set com.canonical.Unity.Launcher.Item progress $2