on xenial help sign in
Merge lp:unity8
commit55708f12786407394ae95ec99cfc259181769ccf authorAlbert Astals Cid <albert.astals@canonical.com> dateMon 3 Aug 2015, 15:49 (11 years ago) parentcc649f67c13987
78 files changed+2984−338
debian/changelog+28−30 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−1 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−1 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 qml/Components/PhysicalKeysMapper.qml+24 qml/Components/ScreenGrabber.qml+1−5 qml/Dash/Dash.qml+1−1 qml/Dash/DashContent.qml+1 qml/Dash/GenericScopeView.qml+5 qml/Launcher/LauncherDelegate.qml+101−15 qml/Launcher/LauncherPanel.qml+94−14 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/Panel/IndicatorsMenu.qml+3−1 qml/Panel/Panel.qml+1 qml/Shell.qml+6−1 qml/Stages/ApplicationWindow.qml+3 qml/Stages/DecoratedWindow.qml+72−8 qml/Stages/DesktopStage.qml+498−57 qml/Stages/SpreadMaths.qml+164 qml/Stages/graphics/window-close.svg+64 tests/libs/UbuntuGestures/tst_TouchRegistry.cpp+47 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−1 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+64−7 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/tst_Shell.qml+353−12 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
debian/changelog+28−30
@@ -1,44 +1,42 @@
1−unity8 (8.10+15.10.20150721-0ubuntu1) wily; urgency=medium
2−
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
1+unity8 (8.10+15.10.20150729-0ubuntu1) wily; urgency=medium
132
143 [ CI Train Bot ]
154 * Resync trunk.
16−
17− [ Josh Arenson ]
18− * Add handling for the location page to the autopilot settings wizard
19− test
20−
21− [ Michael Zanetti ]
225 * allow opening the manage dash area by clicking with a mouse on the
23− arrow label
6+ arrow label (LP: #1431564)
247
25− [ handsome_feng ]
26− * Added touch state on navigation. (LP: #1383213)
8+ [ Daniel d'Andrada ]
9+ * TouchRegistry: remove null candidates from list of candidates (LP:
10+ #1473492)
2711
28− [ handsome_feng<445865575@qq.com> ]
29− * Added touch state on navigation. (LP: #1383213)
12+ [ Lukáš Tinkl ]
13+ * Fix power dialogs on desktop
14+ * Provide DBUS compatibility with various session services
15+ (suspend/hibernate, lock/unlock, screensaver, etc)
16+ * React on PrtScr keyboard shortcut for taking screenshots on desktop
17+ (LP: #1474149)
18+ * launcher parity: close apps from quicklist (LP: #1457201)
3019
31− -- CI Train Bot <ci-train-bot@canonical.com> Tue, 21 Jul 2015 09:10:05 +0000
20+ [ Michael Zanetti ]
21+ * Implement first edition for a desktop Alt+Tab spread
22+ * drop the gcc-4.9 dependency (LP: #1452348)
3223
33−unity8 (8.10+15.10.20150713-0ubuntu1) wily; urgency=medium
24+ [ Mirco Müller ]
25+ * Added corresponding tests and visual tweaks to a launcher-item's
26+ progress-overlay.
27+ * Added corresponding tests and visual tweaks to a launcher-item's
28+ progress-overlay.
29+ * Implemented alert/wiggle feature for launcher-icons.
30+ * Implemented alert/wiggle feature for launcher-icons.
3431
35− [ Albert Astals Cid ]
36− * Revert Session Screenshotter (LP: #1473476)
32+ [ handsome_feng ]
33+ * makes left swip reset the search string. (LP: #1413791)
3734
38− [ Daniel d'Andrada ]
39− * Dismiss the apps spread if greeter gets shown (LP: #1471609)
35+ [ handsome_feng<445865575@qq.com> ]
36+ * Don't expand indicators when tap to return to call. (LP: #1453217)
37+ * makes left swip reset the search string. (LP: #1413791)
4038
41− -- CI Train Bot <ci-train-bot@canonical.com> Mon, 13 Jul 2015 14:17:20 +0000
39+ -- CI Train Bot <ci-train-bot@canonical.com> Wed, 29 Jul 2015 12:39:01 +0000
4240
4341unity8 (8.10+15.10.20150708.2-0ubuntu1) wily; urgency=medium
4442
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.98),
3432 libusermetricsoutput1-dev,
3533 libxcb1-dev,
3634 pkg-config,
@@ -102,7 +100,7 @@ Depends: gsettings-desktop-schemas,
102100 qtdeclarative5-ubuntu-telephony0.1,
103101 qtdeclarative5-ubuntu-web-plugin,
104102 ubuntu-system-settings,
105− unity-launcher-impl-4,
103+ unity-launcher-impl-7,
106104 unity8-common (= ${source:Version}),
107105 unity8-private (= ${binary:Version}),
108106 unity8-private | unity-launcher-impl,
@@ -192,7 +190,7 @@ Depends: accountsservice-ubuntu-schemas (>= 0.0.3),
192190 ${misc:Depends},
193191 ${shlibs:Depends},
194192Provides: unity-launcher-impl,
195− unity-launcher-impl-4,
193+ unity-launcher-impl-7,
196194Description: Unity 8 private libs
197195 The Unity 8 shell is the primary user interface for Ubuntu devices.
198196 .
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−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)
22pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=6)
33pkg_check_modules(GSETTINGS_QT REQUIRED gsettings-qt)
44
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−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)
22pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=6)
33pkg_check_modules(GSETTINGS_QT REQUIRED gsettings-qt)
44
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
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/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+101−15
@@ -15,7 +15,7 @@
1515 */
1616
1717import QtQuick 2.0
18−import Ubuntu.Components 0.1
18+import Ubuntu.Components 1.1
1919
2020Item {
2121 id: root
@@ -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
@@ -75,7 +153,7 @@ Item {
75153 objectName: "countEmblem"
76154 anchors {
77155 right: parent.right
78− top: parent.top
156+ bottom: parent.bottom
79157 margins: units.dp(3)
80158 }
81159 width: Math.min(root.itemWidth, Math.max(units.gu(2), countLabel.implicitWidth + units.gu(1)))
@@ -100,23 +178,21 @@ Item {
100178 }
101179 }
102180
103− BorderImage {
181+ UbuntuShape {
104182 id: progressOverlay
105183 objectName: "progressOverlay"
184+
106185 anchors {
107186 left: iconItem.left
108187 right: iconItem.right
109− bottom: iconItem.bottom
110− leftMargin: units.gu(1)
111− rightMargin: units.gu(1)
112− bottomMargin: units.gu(1)
188+ verticalCenter: parent.verticalCenter
189+ leftMargin: units.gu(1.5)
190+ rightMargin: units.gu(1.5)
113191 }
114− height: units.gu(1.5)
192+ height: units.gu(1)
115193 visible: root.progress > -1
116− source: "graphics/progressbar-trough.sci"
117−
118− // For fill calculation we need to remove the 2 units of border defined in .sci file
119− property int adjustedWidth: width - units.gu(2)
194+ color: UbuntuColors.darkGrey
195+ borderSource: "none"
120196
121197 Item {
122198 anchors {
@@ -124,20 +200,22 @@ Item {
124200 top: parent.top
125201 bottom: parent.bottom
126202 }
127− width: Math.min(100, root.progress) / 100 * parent.adjustedWidth + units.gu(1)
203+ width: Math.min(100, root.progress) / 100 * parent.width
128204 clip: true
129205
130− BorderImage {
206+ UbuntuShape {
131207 anchors {
132208 left: parent.left
133209 top: parent.top
134210 bottom: parent.bottom
135211 }
212+ color: "white"
213+ borderSource: "none"
136214 width: progressOverlay.width
137− source: "graphics/progressbar-fill.sci"
138215 }
139216 }
140217 }
218+
141219 Image {
142220 objectName: "focusedHighlight"
143221 anchors {
@@ -167,6 +245,14 @@ Item {
167245 }
168246
169247 transform: [
248+ // The rotation about the icon's center/z-axis for the wiggle
249+ // needs to happen here too, because there's no other way to
250+ // align the wiggle with the icon-folding otherwise
251+ Rotation {
252+ axis { x: 0; y: 0; z: 1 }
253+ origin { x: iconItem.width / 2; y: iconItem.height / 2; z: 0 }
254+ angle: priv.wiggleAngle
255+ },
170256 // Rotating 3 times at top/bottom because that increases the perspective.
171257 // This is a hack, but as QML does not support real 3D coordinates
172258 // getting a higher perspective can only be done by a hack. This is the most
qml/Launcher/LauncherPanel.qml+94−14
@@ -15,7 +15,7 @@
1515 */
1616
1717import QtQuick 2.3
18−import Ubuntu.Components 1.1
18+import Ubuntu.Components 1.2
1919import Ubuntu.Components.ListItems 1.0 as ListItems
2020import Unity.Launcher 0.1
2121import Ubuntu.Components.Popups 0.1
@@ -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/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+6−1
@@ -178,6 +178,7 @@ Item {
178178
179179 PhysicalKeysMapper {
180180 id: physicalKeysMapper
181+ objectName: "physicalKeysMapper"
181182
182183 onPowerKeyLongPressed: dialogs.showPowerDialog()
183184 onVolumeDownTriggered: volumeControl.volumeDown();
@@ -188,7 +189,6 @@ Item {
188189 ScreenGrabber {
189190 id: screenGrabber
190191 z: dialogs.z + 10
191− enabled: Powerd.status === Powerd.On
192192 }
193193
194194 Binding {
@@ -352,6 +352,11 @@ Item {
352352 property: "beingResized"
353353 value: shell.beingResized
354354 }
355+ Binding {
356+ target: applicationsDisplayLoader.item
357+ property: "altTabPressed"
358+ value: physicalKeysMapper.altTabPressed
359+ }
355360 }
356361
357362 Tutorial {
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+498−57
@@ -17,13 +17,15 @@
1717 */
1818
1919import QtQuick 2.3
20+import QtQuick.Layouts 1.1
2021import Ubuntu.Components 1.1
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
@@ -50,6 +52,7 @@ Rectangle {
5052 readonly property bool orientationChangesEnabled: false
5153
5254 property alias background: wallpaper.source
55+ property bool altTabPressed: false
5356
5457 CrossFadeImage {
5558 id: wallpaper
@@ -61,15 +64,18 @@ Rectangle {
6164 Connections {
6265 target: ApplicationManager
6366 onApplicationAdded: {
64− ApplicationManager.requestFocusApplication(ApplicationManager.get(ApplicationManager.count-1).appId)
67+ if (root.state == "altTab") {
68+ root.state = "";
69+ }
70+
71+ ApplicationManager.requestFocusApplication(appId)
6572 }
6673
6774 onFocusRequested: {
6875 var appIndex = priv.indexOf(appId);
6976 var appDelegate = appRepeater.itemAt(appIndex);
70− if (appDelegate.state === "minimized") {
71− appDelegate.state = "normal"
72− }
77+ appDelegate.minimized = false;
78+ appDelegate.focus = true;
7379 ApplicationManager.focusApplication(appId);
7480 }
7581 }
@@ -104,8 +110,8 @@ Rectangle {
104110 onClose: {
105111 ApplicationManager.stopApplication(ApplicationManager.focusedApplicationId)
106112 }
107− onMinimize: appRepeater.itemAt(0).state = "minimized"
108− onMaximize: appRepeater.itemAt(0).state = "normal"
113+ onMinimize: appRepeater.itemAt(0).minimize();
114+ onMaximize: appRepeater.itemAt(0).unmaximize();
109115 }
110116
111117 Binding {
@@ -114,67 +120,502 @@ Rectangle {
114120 value: priv.focusedAppDelegate !== null && priv.focusedAppDelegate.state === "maximized"
115121 }
116122
117− Repeater {
118− id: appRepeater
119− model: ApplicationManager
123+ Rectangle {
124+ id: spreadBackground
125+ anchors.fill: parent
126+ color: "#55000000"
127+ visible: false
128+ }
120129
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)
130+ FocusScope {
131+ id: appContainer
132+ objectName: "appContainer"
133+ anchors.fill: parent
127134
128− onFocusChanged: {
129− if (focus) {
130− ApplicationManager.requestFocusApplication(model.appId);
131− }
135+ Keys.onPressed: {
136+ switch (event.key) {
137+ case Qt.Key_Left:
138+ case Qt.Key_Backtab:
139+ selectPrevious(event.isAutoRepeat)
140+ break;
141+ case Qt.Key_Right:
142+ case Qt.Key_Tab:
143+ selectNext(event.isAutoRepeat)
144+ break;
145+ case Qt.Key_Escape:
146+ appRepeater.highlightedIndex = -1
147+ case Qt.Key_Enter:
148+ case Qt.Key_Return:
149+ case Qt.Key_Space:
150+ root.state = ""
151+ }
152+ }
153+
154+ function selectNext(isAutoRepeat) {
155+ if (isAutoRepeat && appRepeater.highlightedIndex >= ApplicationManager.count -1) {
156+ return; // AutoRepeat is not allowed to wrap around
157+ }
158+
159+ appRepeater.highlightedIndex = (appRepeater.highlightedIndex + 1) % ApplicationManager.count;
160+ var newContentX = ((spreadFlickable.contentWidth) / (ApplicationManager.count + 1)) * Math.max(0, Math.min(ApplicationManager.count - 5, appRepeater.highlightedIndex - 3));
161+ if (spreadFlickable.contentX < newContentX || appRepeater.highlightedIndex == 0) {
162+ spreadFlickable.snapTo(newContentX)
163+ }
164+ }
165+
166+ function selectPrevious(isAutoRepeat) {
167+ if (isAutoRepeat && appRepeater.highlightedIndex == 0) {
168+ return; // AutoRepeat is not allowed to wrap around
169+ }
170+
171+ var newIndex = appRepeater.highlightedIndex - 1 >= 0 ? appRepeater.highlightedIndex - 1 : ApplicationManager.count - 1;
172+ appRepeater.highlightedIndex = newIndex;
173+ var newContentX = ((spreadFlickable.contentWidth) / (ApplicationManager.count + 1)) * Math.max(0, Math.min(ApplicationManager.count - 5, appRepeater.highlightedIndex - 1));
174+ if (spreadFlickable.contentX > newContentX || newIndex == ApplicationManager.count -1) {
175+ spreadFlickable.snapTo(newContentX)
176+ }
177+ }
178+
179+ function focusSelected() {
180+ if (appRepeater.highlightedIndex != -1) {
181+ appRepeater.itemAt(appRepeater.highlightedIndex).focus = true;
132182 }
183+ }
184+
185+ Repeater {
186+ id: appRepeater
187+ model: ApplicationManager
188+ objectName: "appRepeater"
189+
190+ property int highlightedIndex: -1
191+ property int closingIndex: -1
192+
193+ delegate: FocusScope {
194+ id: appDelegate
195+ z: ApplicationManager.count - index
196+ y: units.gu(3)
197+ width: units.gu(60)
198+ height: units.gu(50)
199+
200+ readonly property int minWidth: units.gu(10)
201+ readonly property int minHeight: units.gu(10)
202+
203+ property bool maximized: false
204+ property bool minimized: false
205+
206+ onFocusChanged: {
207+ if (focus && ApplicationManager.focusedApplicationId !== model.appId) {
208+ ApplicationManager.requestFocusApplication(model.appId);
209+ decoratedWindow.forceActiveFocus();
210+ }
211+ }
212+ Component.onCompleted: {
213+ if (ApplicationManager.focusedApplicationId == model.appId) {
214+ decoratedWindow.forceActiveFocus();
215+ }
216+ }
217+
218+ function maximize() {
219+ minimized = false;
220+ maximized = true;
221+ }
222+ function minimize() {
223+ maximized = false;
224+ minimized = true;
225+ }
226+ function unmaximize() {
227+ minimized = false;
228+ maximized = false;
229+ }
230+
231+ Behavior on x {
232+ id: closeBehavior
233+ enabled: appRepeater.closingIndex >= 0
234+ UbuntuNumberAnimation {
235+ onRunningChanged: if (!running) appRepeater.closingIndex = -1
236+ }
237+ }
238+
239+ states: [
240+ State {
241+ name: "normal"; when: !appDelegate.maximized && !appDelegate.minimized && root.state !== "altTab"
242+ },
243+ State {
244+ name: "maximized"; when: appDelegate.maximized && (root.state !== "altTab" || (root.state == "altTab" && !root.workspacesUpdated))
245+ PropertyChanges { target: appDelegate; x: 0; y: 0; width: root.width; height: root.height }
246+ },
247+ State {
248+ name: "minimized"; when: appDelegate.minimized && (root.state !== "altTab" || (root.state == "altTab" && !root.workspacesUpdated))
249+ PropertyChanges { target: appDelegate; x: -appDelegate.width / 2; scale: units.gu(5) / appDelegate.width; opacity: 0 }
250+ },
251+ State {
252+ name: "altTab"; when: root.state == "altTab" && root.workspacesUpdated
253+ PropertyChanges {
254+ target: appDelegate
255+ x: spreadMaths.animatedX
256+ y: spreadMaths.animatedY + (appDelegate.height - decoratedWindow.height)
257+ angle: spreadMaths.animatedAngle
258+ itemScale: spreadMaths.scale
259+ itemScaleOriginY: decoratedWindow.height / 2;
260+ z: index
261+ visible: spreadMaths.itemVisible
262+ }
263+ PropertyChanges {
264+ target: decoratedWindow
265+ decorationShown: false
266+ highlightShown: index == appRepeater.highlightedIndex
267+ state: "transformed"
268+ width: spreadMaths.spreadHeight
269+ height: spreadMaths.spreadHeight
270+ shadowOpacity: spreadMaths.shadowOpacity
271+ }
272+ PropertyChanges {
273+ target: tileInfo
274+ visible: true
275+ opacity: spreadMaths.tileInfoOpacity
276+ }
277+ PropertyChanges {
278+ target: spreadSelectArea
279+ enabled: true
280+ }
281+ PropertyChanges {
282+ target: windowMoveResizeArea
283+ enabled: false
284+ }
285+ }
286+ ]
287+ transitions: [
288+ Transition {
289+ from: "maximized,minimized,normal,"
290+ to: "maximized,minimized,normal,"
291+ PropertyAnimation { target: appDelegate; properties: "x,y,opacity,width,height,scale" }
292+ }
293+ ]
294+ property real angle: 0
295+ property real itemScale: 1
296+ property int itemScaleOriginX: 0
297+ property int itemScaleOriginY: 0
298+
299+ SpreadMaths {
300+ id: spreadMaths
301+ flickable: spreadFlickable
302+ itemIndex: index
303+ totalItems: Math.max(6, ApplicationManager.count)
304+ sceneHeight: root.height
305+ itemHeight: appDelegate.height
306+ }
307+
308+ WindowMoveResizeArea {
309+ id: windowMoveResizeArea
310+ target: appDelegate
311+ minWidth: appDelegate.minWidth
312+ minHeight: appDelegate.minHeight
313+ resizeHandleWidth: units.gu(2)
314+ windowId: model.appId // FIXME: Change this to point to windowId once we have such a thing
315+
316+ onPressed: appDelegate.focus = true;
317+ }
318+
319+ DecoratedWindow {
320+ id: decoratedWindow
321+ objectName: "decoratedWindow"
322+ anchors.left: appDelegate.left
323+ anchors.top: appDelegate.top
324+ windowWidth: appDelegate.width
325+ windowHeight: appDelegate.height
326+ application: ApplicationManager.get(index)
327+ active: ApplicationManager.focusedApplicationId === model.appId
328+ focus: false
329+
330+ onClose: ApplicationManager.stopApplication(model.appId)
331+ onMaximize: appDelegate.maximize()
332+ onMinimize: appDelegate.minimize()
333+
334+ transform: [
335+ Scale {
336+ origin.x: itemScaleOriginX
337+ origin.y: itemScaleOriginY
338+ xScale: itemScale
339+ yScale: itemScale
340+ },
341+ Rotation {
342+ origin { x: 0; y: (decoratedWindow.height - (decoratedWindow.height * itemScale / 2)) }
343+ axis { x: 0; y: 1; z: 0 }
344+ angle: appDelegate.angle
345+ }
346+ ]
347+
348+ MouseArea {
349+ id: spreadSelectArea
350+ anchors.fill: parent
351+ anchors.margins: -units.gu(2)
352+ enabled: false
353+ hoverEnabled: enabled
354+
355+ // There is a bug in MouseArea where containsMouse doesn't
356+ // return to false if the MouseArea is disabled while
357+ // containing the mouse. Let's manage the property our own.
358+ property bool upperThirdContainsMouse: false
359+ onContainsMouseChanged: evaluateContainsMouse()
360+ onMouseYChanged: evaluateContainsMouse()
361+ function evaluateContainsMouse() {
362+ if (containsMouse) {
363+ appRepeater.highlightedIndex = index
364+ }
133365
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 }
366+ if (containsMouse && mouseY < height / 3) {
367+ spreadSelectArea.upperThirdContainsMouse = true
368+ } else {
369+ spreadSelectArea.upperThirdContainsMouse = false;
370+ }
371+ }
372+ onEnabledChanged: {
373+ if (!enabled) {
374+ spreadSelectArea.upperThirdContainsMouse = false
375+ }
376+ }
377+
378+ onClicked: {
379+ root.state = ""
380+ }
381+ }
148382 }
149− ]
150− transitions: [
151− Transition {
152− PropertyAnimation { target: appDelegate; properties: "x,y,opacity,width,height,scale" }
383+
384+ Image {
385+ id: closeImage
386+ anchors { left: parent.left; top: parent.top; leftMargin: -height / 2; topMargin: -height / 2 + spreadMaths.closeIconOffset }
387+ source: "graphics/window-close.svg"
388+ visible: spreadSelectArea.upperThirdContainsMouse
389+ height: units.gu(1.5)
390+ width: height
391+ sourceSize.width: width
392+ sourceSize.height: height
153393 }
154− ]
155394
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
395+ MouseArea {
396+ id: closeMouseArea
397+ objectName: "closeMouseArea"
398+ anchors.fill: closeImage
399+ anchors.margins: -units.gu(2)
400+ enabled: spreadSelectArea.upperThirdContainsMouse
401+ onClicked: {
402+ appRepeater.closingIndex = index;
403+ ApplicationManager.stopApplication(model.appId)
404+ }
405+ }
406+
407+ MouseArea {
408+ id: tileInfo
409+ objectName: "tileInfo"
410+ anchors { left: parent.left; top: decoratedWindow.bottom; topMargin: units.gu(5) }
411+ width: units.gu(30)
412+ height: titleInfoColumn.height
413+ visible: false
414+ hoverEnabled: true
415+
416+ onContainsMouseChanged: {
417+ if (containsMouse) {
418+ appRepeater.highlightedIndex = index
419+ }
420+ }
162421
163− onPressed: appDelegate.focus = true;
422+ onClicked: {
423+ root.state = ""
424+ }
425+
426+ ColumnLayout {
427+ id: titleInfoColumn
428+ anchors { left: parent.left; top: parent.top; right: parent.right }
429+ spacing: units.gu(1)
430+
431+ UbuntuShape {
432+ Layout.preferredHeight: Math.min(units.gu(6), root.height * .05)
433+ Layout.preferredWidth: height * 8 / 7.6
434+ image: Image {
435+ anchors.fill: parent
436+ source: model.icon
437+ }
438+ }
439+ Label {
440+ Layout.fillWidth: true
441+ Layout.preferredHeight: units.gu(6)
442+ text: model.name
443+ wrapMode: Text.WordWrap
444+ maximumLineCount: 2
445+ }
446+ }
447+ }
164448 }
449+ }
450+ }
451+
452+ FloatingFlickable {
453+ id: spreadFlickable
454+ anchors.fill: parent
455+ contentWidth: Math.max(6, ApplicationManager.count) * Math.min(height / 4, width / 5)
456+ enabled: false
457+
458+ function snapTo(contentX) {
459+ snapAnimation.stop();
460+ snapAnimation.to = contentX
461+ snapAnimation.start();
462+ }
463+
464+ UbuntuNumberAnimation {
465+ id: snapAnimation
466+ target: spreadFlickable
467+ property: "contentX"
468+ }
469+ }
470+
471+ Item {
472+ id: workspaceSelector
473+ anchors {
474+ left: parent.left
475+ top: parent.top
476+ right: parent.right
477+ topMargin: units.gu(3.5) // TODO: should be root.panelHeight
478+ }
479+ height: root.height * 0.25
480+ visible: false
481+
482+ RowLayout {
483+ anchors.fill: parent
484+ spacing: units.gu(1)
485+ Item { Layout.fillWidth: true }
486+ Repeater {
487+ model: 1 // TODO: will be a workspacemodel in the future
488+ Item {
489+ Layout.fillHeight: true
490+ Layout.preferredWidth: ((height - units.gu(6)) * root.width / root.height)
491+ Image {
492+ source: root.background
493+ anchors {
494+ left: parent.left
495+ right: parent.right
496+ verticalCenter: parent.verticalCenter
497+ }
498+ height: parent.height * 0.75
499+
500+ // FIXME: This is temporary until we can have multiple Items per surface
501+ ShaderEffect {
502+ anchors.fill: parent
503+
504+ property var source: ShaderEffectSource {
505+ id: shaderEffectSource
506+ live: false
507+ sourceItem: appContainer
508+ Connections { target: root; onUpdateWorkspaces: shaderEffectSource.scheduleUpdate() }
509+ }
510+
511+ fragmentShader: "
512+ varying highp vec2 qt_TexCoord0;
513+ uniform sampler2D source;
514+ void main(void)
515+ {
516+ highp vec4 sourceColor = texture2D(source, qt_TexCoord0);
517+ gl_FragColor = sourceColor;
518+ }"
519+ }
520+ }
521+
522+ // TODO: This is the bar for the currently selected workspace
523+ // Enable this once the workspace stuff is implemented
524+// Rectangle {
525+// anchors { left: parent.left; right: parent.right; bottom: parent.bottom }
526+// height: units.dp(2)
527+// color: UbuntuColors.orange
528+// visible: index == 0 // TODO: should be active workspace index
529+// }
530+ }
165531
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"
177532 }
533+ // TODO: This is the "new workspace" button. Enable this once workspaces are implemented
534+// Item {
535+// Layout.fillHeight: true
536+// Layout.preferredWidth: ((height - units.gu(6)) * root.width / root.height)
537+// Rectangle {
538+// anchors {
539+// left: parent.left
540+// right: parent.right
541+// verticalCenter: parent.verticalCenter
542+// }
543+// height: parent.height * 0.75
544+// color: "#22ffffff"
545+
546+// Label {
547+// anchors.centerIn: parent
548+// font.pixelSize: parent.height / 2
549+// text: "+"
550+// }
551+// }
552+// }
553+ Item { Layout.fillWidth: true }
554+ }
555+ }
556+
557+ Label {
558+ id: currentSelectedLabel
559+ anchors { bottom: parent.bottom; bottomMargin: root.height * 0.625; horizontalCenter: parent.horizontalCenter }
560+ text: appRepeater.highlightedIndex >= 0 ? ApplicationManager.get(appRepeater.highlightedIndex).name : ""
561+ visible: false
562+ fontSize: "large"
563+ }
564+
565+ states: [
566+ State {
567+ name: "altTab"; when: root.altTabPressed
568+ PropertyChanges { target: workspaceSelector; visible: true }
569+ PropertyChanges { target: spreadFlickable; enabled: true }
570+ PropertyChanges { target: currentSelectedLabel; visible: true }
571+ PropertyChanges { target: spreadBackground; visible: true }
572+ PropertyChanges { target: appContainer; focus: true }
573+ }
574+ ]
575+ signal updateWorkspaces();
576+ property bool workspacesUpdated: false
577+ transitions: [
578+ Transition {
579+ from: "*"
580+ to: "altTab"
581+ SequentialAnimation {
582+ PropertyAction { target: appRepeater; property: "highlightedIndex"; value: Math.min(ApplicationManager.count - 1, 1) }
583+ PauseAnimation { duration: 50 }
584+ PropertyAction { target: workspaceSelector; property: "visible" }
585+ ScriptAction { script: root.updateWorkspaces() }
586+ // FIXME: Updating of shaderEffectSource take a bit of time. This is temporary until we can paint multiple items per surface
587+ PauseAnimation { duration: 10 }
588+ PropertyAction { target: root; property: "workspacesUpdated"; value: true }
589+ PropertyAction { target: spreadFlickable; property: "visible" }
590+ PropertyAction { targets: [currentSelectedLabel,spreadBackground]; property: "visible" }
591+ PropertyAction { target: spreadFlickable; property: "contentX"; value: 0 }
592+ }
593+ },
594+ Transition {
595+ from: "*"
596+ to: "*"
597+ PropertyAnimation { property: "opacity" }
598+ PropertyAction { target: root; property: "workspacesUpdated"; value: false }
599+ ScriptAction { script: { appContainer.focusSelected() } }
600+ PropertyAction { target: appRepeater; property: "highlightedIndex"; value: -1 }
178601 }
602+
603+ ]
604+
605+ MouseArea {
606+ anchors {
607+ top: parent.top
608+ right: parent.right
609+ bottom: parent.bottom
610+ }
611+ // TODO: Make this a push to edge thing like the launcher when we can,
612+ // for now, yes, we want 1 pixel, regardless of the scaling
613+ width: 1
614+ hoverEnabled: true
615+ onContainsMouseChanged: {
616+ if (containsMouse) {
617+ root.state = "altTab"
618+ }
619+ }
179620 }
180621}
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.1
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/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/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−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)
22pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=6)
33
44include_directories(
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+64−7
@@ -68,6 +68,7 @@ class MockAppManager: public unity::shell::application::ApplicationManagerInterf
6868 Q_OBJECT
6969public:
7070 MockAppManager(QObject *parent = 0): ApplicationManagerInterface(parent) {}
71+ ~MockAppManager() {}
7172 int rowCount(const QModelIndex &) const override { return m_list.count(); }
7273 QVariant data(const QModelIndex &, int ) const override { return QVariant(); }
7374 QString focusedApplicationId() const override {
@@ -325,6 +326,46 @@ private Q_SLOTS:
325326 QCOMPARE(launcherModel->get(0)->appId(), QLatin1String("abs-icon"));
326327 }
327328
329+ void testQuitMenuItem() {
330+ // we have 2 apps running, both should have the Quit action in its quick list
331+ QCOMPARE(launcherModel->rowCount(), 2);
332+
333+ // stop the second one keeping it pinned so that it doesn't go away
334+ launcherModel->pin("no-icon");
335+ appManager->stopApplication("no-icon");
336+
337+ // find the first Quit item, should be there
338+ QuickListModel *model = qobject_cast<QuickListModel*>(launcherModel->get(0)->quickList());
339+ int quitActionIndex = -1;
340+ for (int i = 0; i < model->rowCount(); ++i) {
341+ if (model->get(i).actionId() == "stop_item") {
342+ quitActionIndex = i;
343+ break;
344+ }
345+ }
346+ QVERIFY(quitActionIndex >= 0);
347+
348+ // find the second Quit item, should NOT be there, the app is stopped
349+ QuickListModel *model2 = qobject_cast<QuickListModel*>(launcherModel->get(1)->quickList());
350+ int quitActionIndex2 = -1;
351+ for (int i = 0; i < model2->rowCount(); ++i) {
352+ if (model2->get(i).actionId() == "stop_item") {
353+ quitActionIndex2 = i;
354+ break;
355+ }
356+ }
357+ QVERIFY(quitActionIndex2 == -1);
358+
359+ // trigger the first quit item quicklist action
360+ launcherModel->quickListActionInvoked(launcherModel->get(0)->appId(), quitActionIndex);
361+ // first app should be gone...
362+ QCOMPARE(launcherModel->rowCount(QModelIndex()), 1);
363+ // ... the second app (now at index 0) should still be there, pinned and stopped
364+ QCOMPARE(launcherModel->get(0)->appId(), QStringLiteral("no-icon"));
365+ QCOMPARE(launcherModel->get(0)->pinned(), true);
366+ QCOMPARE(launcherModel->get(0)->running(), false);
367+ }
368+
328369 void testGetUrlForAppId() {
329370 QCOMPARE(launcherModel->getUrlForAppId(QString()), QString());
330371 QCOMPARE(launcherModel->getUrlForAppId(""), QString());
@@ -368,11 +409,18 @@ private Q_SLOTS:
368409 }
369410
370411 void testCountEmblems() {
412+ QSignalSpy spy(launcherModel, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector<int>)));
413+
371414 // Call GetAll on abs-icon
372415 QDBusInterface interface("com.canonical.Unity.Launcher", "/com/canonical/Unity/Launcher/abs_2Dicon", "org.freedesktop.DBus.Properties");
373416 QDBusReply<QVariantMap> reply = interface.call("GetAll");
374417 QVariantMap map = reply.value();
375418
419+ // Check that the alerting-status is still false, and the item on the upper side of the API
420+ int index = launcherModel->findApplication("abs-icon");
421+ QCOMPARE(index >= 0, true);
422+ QVERIFY(launcherModel->get(index)->alerting() == false);
423+
376424 // Make sure GetAll returns a map with count and countVisible props
377425 QCOMPARE(map.contains("count"), true);
378426 QCOMPARE(map.contains("countVisible"), true);
@@ -393,13 +441,8 @@ private Q_SLOTS:
393441 QCOMPARE(map.value("count").toInt(), 55);
394442 QCOMPARE(map.value("countVisible").toBool(), true);
395443
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);
444+ // Finally check, that the change to "count" implicitly also set the alerting-state to true
445+ QVERIFY(launcherModel->get(index)->alerting() == true);
403446 }
404447
405448 void testCountEmblemAddsRemovesItem_data() {
@@ -459,6 +502,20 @@ private Q_SLOTS:
459502 QCOMPARE(index == -1, !isRunning && !isPinned && !startWhenVisible);
460503 }
461504
505+ void testAlert() {
506+ // Check that the alerting-status is still false
507+ int index = launcherModel->findApplication("abs-icon");
508+ QCOMPARE(index >= 0, true);
509+ QVERIFY(launcherModel->get(index)->alerting() == false);
510+
511+ // Call Alert() on "abs-icon"
512+ QDBusInterface interface("com.canonical.Unity.Launcher", "/com/canonical/Unity/Launcher/abs_2Dicon", "com.canonical.Unity.Launcher.Item");
513+ interface.call("Alert");
514+
515+ // Check that the alerting-status is now true
516+ QVERIFY(launcherModel->get(index)->alerting() == true);
517+ }
518+
462519 void testRefreshAfterDeletedDesktopFiles_data() {
463520 QTest::addColumn<bool>("deleted");
464521 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 0.1
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/tst_Shell.qml+353−12
@@ -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
@@ -66,6 +67,11 @@ Rectangle {
6667 width: units.gu(40)
6768 height: units.gu(71)
6869 }
70+ StateChangeScript {
71+ script: {
72+ GSettingsController.setUsageMode("Staged")
73+ }
74+ }
6975 },
7076 State {
7177 name: "tablet"
@@ -74,6 +80,28 @@ Rectangle {
7480 width: units.gu(100)
7581 height: units.gu(71)
7682 }
83+ StateChangeScript {
84+ script: {
85+ GSettingsController.setUsageMode("Staged")
86+ }
87+ }
88+ },
89+ State {
90+ name: "desktop"
91+ PropertyChanges {
92+ target: shellLoader
93+ width: shellContainer.width
94+ height: shellContainer.height
95+ }
96+ PropertyChanges {
97+ target: mouseEmulation
98+ checked: false
99+ }
100+ StateChangeScript {
101+ script: {
102+ GSettingsController.setUsageMode("Windowed")
103+ }
104+ }
77105 }
78106 ]
79107
@@ -81,6 +109,7 @@ Rectangle {
81109 property bool itemDestroyed: false
82110 sourceComponent: Component {
83111 Shell {
112+ id: __shell
84113 usageScenario: usageScenarioSelector.model[usageScenarioSelector.selectedIndex]
85114 orientation: Qt.PortraitOrientation
86115 primaryOrientation: Qt.PortraitOrientation
@@ -88,6 +117,10 @@ Rectangle {
88117 Component.onDestruction: {
89118 shellLoader.itemDestroyed = true;
90119 }
120+ Component.onCompleted: {
121+ var keyMapper = testCase.findChild(__shell, "physicalKeysMapper");
122+ keyMapper.controlInsteadOfAlt = true;
123+ }
91124 }
92125 }
93126 }
@@ -112,16 +145,32 @@ Rectangle {
112145 anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
113146 spacing: units.gu(1)
114147
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();
148+ Row {
149+ spacing: units.gu(1)
150+ Button {
151+ text: "Show Greeter"
152+ activeFocusOnPress: false
153+ onClicked: {
154+ if (shellLoader.status !== Loader.Ready)
155+ return;
156+
157+ var greeter = testCase.findChild(shellLoader.item, "greeter");
158+ if (!greeter.shown) {
159+ LightDM.Greeter.showGreeter();
160+ }
161+ }
162+ }
163+ Button {
164+ text: "Hide Greeter"
165+ activeFocusOnPress: false
166+ onClicked: {
167+ if (shellLoader.status !== Loader.Ready)
168+ return;
169+
170+ var greeter = testCase.findChild(shellLoader.item, "greeter");
171+ if (greeter.shown) {
172+ greeter.hide()
173+ }
125174 }
126175 }
127176 }
@@ -141,7 +190,7 @@ Rectangle {
141190 anchors { left: parent.left; right: parent.right }
142191 activeFocusOnPress: false
143192 text: "Size"
144− model: ["phone", "tablet"]
193+ model: ["phone", "tablet", "desktop"]
145194 onSelectedIndexChanged: {
146195 shellLoader.active = false;
147196 shellLoader.state = model[selectedIndex];
@@ -163,6 +212,17 @@ Rectangle {
163212
164213 Label { text: "Applications"; font.bold: true }
165214
215+ Button {
216+ text: "Start all apps"
217+ width: parent.width
218+ onClicked: {
219+ for (var i = 0; i < ApplicationManager.availableApplications.length; i++) {
220+ var appId = ApplicationManager.availableApplications[i];
221+ ApplicationManager.startApplication(appId)
222+ }
223+ }
224+ }
225+
166226 Repeater {
167227 id: appRepeater
168228 model: ApplicationManager.availableApplications
@@ -226,6 +286,12 @@ Rectangle {
226286 signalName: "actionInvoked"
227287 }
228288
289+ SignalSpy {
290+ id: appRemovedSpy
291+ target: ApplicationManager
292+ signalName: "applicationRemoved"
293+ }
294+
229295 Telephony.CallEntry {
230296 id: phoneCall
231297 phoneNumber: "+447812221111"
@@ -248,6 +314,7 @@ Rectangle {
248314 }
249315
250316 function cleanup() {
317+ mouseEmulation.checked = true;
251318 tryCompare(shell, "enabled", true); // make sure greeter didn't leave us in disabled state
252319 tearDown();
253320 GSettingsController.setUsageMode("Staged");
@@ -269,6 +336,20 @@ Rectangle {
269336 waitForGreeterToStabilize();
270337 }
271338
339+ function loadDesktopShellWithApps() {
340+ loadShell("desktop");
341+ shell.usageScenario = "desktop"
342+ waitForRendering(root)
343+ var app1 = ApplicationManager.startApplication("dialer-app")
344+ var app2 = ApplicationManager.startApplication("webbrowser-app")
345+ var app3 = ApplicationManager.startApplication("camera-app")
346+ var app4 = ApplicationManager.startApplication("facebook-webapp")
347+ var app5 = ApplicationManager.startApplication("calendar-app")
348+ var app6 = ApplicationManager.startApplication("gallery-app")
349+ var app7 = ApplicationManager.startApplication("camera-app")
350+ waitUntilAppWindowIsFullyLoaded(app7);
351+ }
352+
272353 function waitForGreeterToStabilize() {
273354 var greeter = findChild(shell, "greeter");
274355 verify(greeter);
@@ -472,6 +553,41 @@ Rectangle {
472553 confirmLoggedIn(data.loggedIn)
473554 }
474555
556+ function test_longLeftEdgeSwipeTakesToAppsAndResetSearchString() {
557+ loadShell("phone");
558+ swipeAwayGreeter();
559+ dragLauncherIntoView();
560+ dashCommunicatorSpy.clear();
561+
562+ tapOnAppIconInLauncher();
563+ waitUntilApplicationWindowIsFullyVisible();
564+
565+ verify(ApplicationManager.focusedApplicationId !== "unity8-dash")
566+
567+ //Long left swipe
568+ swipeFromLeftEdge(units.gu(30));
569+
570+ tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
571+
572+ compare(dashCommunicatorSpy.count, 1);
573+ }
574+
575+ function test_ClickUbuntuIconInLauncherTakesToAppsAndResetSearchString() {
576+ loadShell("phone");
577+ swipeAwayGreeter();
578+ dragLauncherIntoView();
579+ dashCommunicatorSpy.clear();
580+
581+ var launcher = findChild(shell, "launcher");
582+ var dashIcon = findChild(launcher, "dashItem");
583+ verify(dashIcon != undefined);
584+ mouseClick(dashIcon);
585+
586+ tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
587+
588+ compare(dashCommunicatorSpy.count, 1);
589+ }
590+
475591 function test_suspend() {
476592 loadShell("phone");
477593 swipeAwayGreeter();
@@ -724,7 +840,7 @@ Rectangle {
724840
725841 // check that the desktop stage and window have been loaded
726842 {
727− var desktopWindow = findChild(shell, "decoratedWindow_webbrowser-app");
843+ var desktopWindow = findChild(shell, "appWindow_webbrowser-app");
728844 verify(desktopWindow);
729845 }
730846
@@ -1082,6 +1198,8 @@ Rectangle {
10821198 }
10831199
10841200 function test_tapUbuntuIconInLauncherOverAppSpread() {
1201+ launcherShowDashHomeSpy.clear();
1202+
10851203 loadShell("phone");
10861204 swipeAwayGreeter();
10871205
@@ -1228,5 +1346,228 @@ Rectangle {
12281346 var launcher = findChild(shell, "launcher");
12291347 compare(launcher.inverted, data.launcherInverted);
12301348 }
1349+
1350+ function test_altTabSwitchesFocus() {
1351+ loadShell("desktop");
1352+ shell.usageScenario = "desktop"
1353+ waitForRendering(root)
1354+
1355+ var desktopStage = findChild(shell, "stage");
1356+ verify(desktopStage != null)
1357+
1358+ var app1 = ApplicationManager.startApplication("dialer-app")
1359+ waitUntilAppWindowIsFullyLoaded(app1);
1360+ var app2 = ApplicationManager.startApplication("webbrowser-app")
1361+ waitUntilAppWindowIsFullyLoaded(app2);
1362+ var app3 = ApplicationManager.startApplication("camera-app")
1363+ waitUntilAppWindowIsFullyLoaded(app3);
1364+
1365+ // Do a quick alt-tab and see if focus changes
1366+ tryCompare(app3.session.surface, "activeFocus", true)
1367+ keyClick(Qt.Key_Tab, Qt.ControlModifier)
1368+ tryCompare(app2.session.surface, "activeFocus", true)
1369+
1370+
1371+ tryCompare(desktopStage, "state", "")
1372+
1373+ // Just press Alt, make sure the spread comes up
1374+ keyPress(Qt.Key_Control);
1375+ keyClick(Qt.Key_Tab);
1376+ tryCompare(desktopStage, "state", "altTab")
1377+
1378+ // Release control, check if spread disappears
1379+ keyRelease(Qt.Key_Control)
1380+ tryCompare(desktopStage, "state", "")
1381+
1382+ // Focus should have switched back now
1383+ tryCompare(app3.session.surface, "activeFocus", true)
1384+ }
1385+
1386+ function test_altTabWrapAround() {
1387+ loadDesktopShellWithApps();
1388+
1389+ var desktopStage = findChild(shell, "stage");
1390+ verify(desktopStage !== null)
1391+
1392+ var appContainer = findInvisibleChild(shell, "appContainer")
1393+ verify(appContainer !== null)
1394+
1395+ var appRepeater = findInvisibleChild(shell, "appRepeater")
1396+ verify(appRepeater !== null)
1397+
1398+ // remember the focused appId
1399+ var focused = ApplicationManager.get(ApplicationManager.findApplication(ApplicationManager.focusedApplicationId));
1400+
1401+ tryCompare(desktopStage, "state", "")
1402+
1403+ // Just press Alt, make sure the spread comes up
1404+ keyPress(Qt.Key_Control);
1405+ keyClick(Qt.Key_Tab);
1406+ tryCompare(desktopStage, "state", "altTab")
1407+ tryCompare(appRepeater, "highlightedIndex", 1)
1408+ waitForRendering(shell)
1409+
1410+ // Now press and hold Tab, make sure the highlight moves all the way but stops at the last one
1411+ // We can't simulate a pressed key with keyPress() currently, so let's inject the events
1412+ // at API level. Jump for 10 times, verify that it's still at the last one and didn't wrap around.
1413+ for (var i = 0; i < 10; i++) {
1414+ appContainer.selectNext(true); // true == isAutoRepeat
1415+ wait(0); // Trigger the event loop to make sure all the things happen
1416+ }
1417+ tryCompare(appRepeater, "highlightedIndex", 6)
1418+
1419+ // Now release it once, and verify that it does wrap around with an additional Tab press
1420+ keyRelease(Qt.Key_Tab);
1421+ keyClick(Qt.Key_Tab);
1422+ tryCompare(appRepeater, "highlightedIndex", 0)
1423+
1424+ // Release control, check if spread disappears
1425+ keyRelease(Qt.Key_Control)
1426+ tryCompare(desktopStage, "state", "")
1427+
1428+ // Make sure that after wrapping around once, we have the same one focused as at the beginning
1429+ tryCompare(focused.session.surface, "activeFocus", true)
1430+ }
1431+
1432+ function test_altBackTabNavigation() {
1433+ loadDesktopShellWithApps();
1434+
1435+ var appRepeater = findInvisibleChild(shell, "appRepeater");
1436+ verify(appRepeater !== null);
1437+
1438+ keyPress(Qt.Key_Control)
1439+ keyClick(Qt.Key_Tab);
1440+ tryCompare(appRepeater, "highlightedIndex", 1);
1441+
1442+ keyClick(Qt.Key_Tab);
1443+ tryCompare(appRepeater, "highlightedIndex", 2);
1444+
1445+ keyClick(Qt.Key_Tab);
1446+ tryCompare(appRepeater, "highlightedIndex", 3);
1447+
1448+ keyClick(Qt.Key_Tab);
1449+ tryCompare(appRepeater, "highlightedIndex", 4);
1450+
1451+ keyClick(Qt.Key_Backtab);
1452+ tryCompare(appRepeater, "highlightedIndex", 3);
1453+
1454+ keyClick(Qt.Key_Backtab);
1455+ tryCompare(appRepeater, "highlightedIndex", 2);
1456+
1457+ keyClick(Qt.Key_Backtab);
1458+ tryCompare(appRepeater, "highlightedIndex", 1);
1459+
1460+ keyRelease(Qt.Key_Control);
1461+ }
1462+
1463+ function test_highlightFollowsMouse() {
1464+ loadDesktopShellWithApps()
1465+
1466+ var appRepeater = findInvisibleChild(shell, "appRepeater");
1467+ verify(appRepeater !== null);
1468+
1469+ keyPress(Qt.Key_Control)
1470+ keyClick(Qt.Key_Tab);
1471+
1472+ tryCompare(appRepeater, "highlightedIndex", 1);
1473+
1474+ var x = 0;
1475+ var y = shell.height * .75;
1476+ mouseMove(shell, x, y)
1477+
1478+ for (var i = 0; i < 7; i++) {
1479+ while (appRepeater.highlightedIndex != i && x <= 4000) {
1480+ x+=10;
1481+ mouseMove(shell, x, y)
1482+ waitForRendering(shell)
1483+ }
1484+ }
1485+
1486+ verify(y < 4000);
1487+
1488+ keyRelease(Qt.Key_Control);
1489+ }
1490+
1491+ function test_closeFromSpread() {
1492+ loadDesktopShellWithApps()
1493+
1494+ var appRepeater = findInvisibleChild(shell, "appRepeater");
1495+ verify(appRepeater !== null);
1496+
1497+ keyPress(Qt.Key_Control)
1498+ keyClick(Qt.Key_Tab);
1499+
1500+ appRemovedSpy.clear();
1501+
1502+ var closedAppId = ApplicationManager.get(2).appId;
1503+ var appDelegate2 = appRepeater.itemAt(2);
1504+ var closeMouseArea = findChild(appDelegate2, "closeMouseArea");
1505+
1506+ // Move the mosue over tile 2 and verify the close button becomes visible
1507+ var x = 0;
1508+ var y = shell.height * .5;
1509+ mouseMove(shell, x, y)
1510+ while (appRepeater.highlightedIndex !== 2 && x <= 4000) {
1511+ x+=10;
1512+ mouseMove(shell, x, y)
1513+ waitForRendering(shell)
1514+ }
1515+ tryCompare(closeMouseArea, "enabled", true)
1516+
1517+ // Close the app using the close button
1518+ mouseClick(closeMouseArea, closeMouseArea.width / 2, closeMouseArea.height / 2)
1519+
1520+ // Verify applicationRemoved has been emitted correctly
1521+ tryCompare(appRemovedSpy, "count", 1)
1522+ compare(appRemovedSpy.signalArguments[0][0], closedAppId);
1523+
1524+ keyRelease(Qt.Key_Control);
1525+ }
1526+
1527+ function test_selectFromSpreadWithMouse_data() {
1528+ return [
1529+ {tag: "click on tileInfo", tileInfo: true },
1530+ {tag: "click on surface", tileInfo: false },
1531+ ]
1532+ }
1533+
1534+ function test_selectFromSpreadWithMouse(data) {
1535+ loadDesktopShellWithApps()
1536+
1537+ var appRepeater = findInvisibleChild(shell, "appRepeater");
1538+ verify(appRepeater !== null);
1539+
1540+ keyPress(Qt.Key_Control)
1541+ keyClick(Qt.Key_Tab);
1542+
1543+ appRemovedSpy.clear();
1544+
1545+ var focusAppId = ApplicationManager.get(2).appId;
1546+ var appDelegate2 = appRepeater.itemAt(2);
1547+ var decoratedWindow = findChild(appDelegate2, "decoratedWindow");
1548+ var stage = findChild(shell, "stage");
1549+
1550+ tryCompare(stage, "state", "altTab");
1551+
1552+ // Move the mouse over tile 2 and verify the highlight becomes visible
1553+ var x = 0;
1554+ var y = shell.height * (data.tileInfo ? .5 : 0.95)
1555+ mouseMove(shell, x, y)
1556+ while (appRepeater.highlightedIndex !== 2 && x <= 4000) {
1557+ x+=10;
1558+ mouseMove(shell, x, y)
1559+ waitForRendering(shell)
1560+ }
1561+ tryCompare(decoratedWindow, "highlightShown", true);
1562+
1563+ // Click the tile
1564+ mouseClick(decoratedWindow, decoratedWindow.width / 2, decoratedWindow.height / 2)
1565+
1566+ // Verify that we left the spread and app2 is the focused one now
1567+ tryCompare(stage, "state", "");
1568+ tryCompare(ApplicationManager, "focusedApplicationId", focusAppId);
1569+
1570+ keyRelease(Qt.Key_Control);
1571+ }
12311572 }
12321573}
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