on xenial help sign in
Make the launcher update on dconf changes Fixes: 1376707
324c3e7Michael Zanetti
plugins/Unity/Launcher/desktopfilehandler.cpp+1−1
@@ -70,7 +70,7 @@ void DesktopFileHandler::load()
70
70
71 QStringList searchDirs = QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation);
71 QStringList searchDirs = QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation);
72#ifdef LAUNCHER_TESTING
72#ifdef LAUNCHER_TESTING
73− searchDirs << "";
73+ searchDirs << ".";
74#endif
74#endif
75
75
76 QString path;
76 QString path;
plugins/Unity/Launcher/gsettings.cpp+11−7
@@ -19,22 +19,20 @@
19
19
20#include "gsettings.h"
20#include "gsettings.h"
21
21
22−#include <QGSettings>
23#include <QVariant>
22#include <QVariant>
24
23
25GSettings::GSettings(QObject *parent):
24GSettings::GSettings(QObject *parent):
26 QObject(parent)
25 QObject(parent)
27{
26{
28−
27+ m_gSettings = new QGSettings("com.canonical.Unity.Launcher", "/com/canonical/unity/launcher/", this);
28+ connect(m_gSettings, &QGSettings::changed, this, &GSettings::onSettingsChanged);
29}
29}
30
30
31QStringList GSettings::storedApplications() const
31QStringList GSettings::storedApplications() const
32{
32{
33 QStringList storedApps;
33 QStringList storedApps;
34
34
35− QGSettings gSettings("com.canonical.Unity.Launcher", "/com/canonical/unity/launcher/");
36−
37− Q_FOREACH(const QString &entry, gSettings.get("items").toStringList()) {
35+ Q_FOREACH(const QString &entry, m_gSettings->get("items").toStringList()) {
38 if (entry.startsWith("application:///")) {
36 if (entry.startsWith("application:///")) {
39 // convert legacy entries to new world appids
37 // convert legacy entries to new world appids
40 QString appId = entry;
38 QString appId = entry;
@@ -57,10 +55,16 @@ QStringList GSettings::storedApplications() const
57
55
58void GSettings::setStoredApplications(const QStringList &storedApplications)
56void GSettings::setStoredApplications(const QStringList &storedApplications)
59{
57{
60− QGSettings gSettings("com.canonical.Unity.Launcher", "/com/canonical/unity/launcher/");
61 QStringList gSettingsList;
58 QStringList gSettingsList;
62 Q_FOREACH(const QString &entry, storedApplications) {
59 Q_FOREACH(const QString &entry, storedApplications) {
63 gSettingsList << QString("appid://%1").arg(entry);
60 gSettingsList << QString("appid://%1").arg(entry);
64 }
61 }
65− gSettings.set("items", gSettingsList);
62+ m_gSettings->set("items", gSettingsList);
63+}
64+
65+void GSettings::onSettingsChanged(const QString &key)
66+{
67+ if (key == "items") {
68+ Q_EMIT changed();
69+ }
66}
70}
plugins/Unity/Launcher/gsettings.h+10−1
@@ -22,7 +22,7 @@
22
22
23#include <QObject>
23#include <QObject>
24#include <QStringList>
24#include <QStringList>
25−
25+#include <QGSettings>
26
26
27class GSettings: public QObject
27class GSettings: public QObject
28{
28{
@@ -32,6 +32,15 @@ public:
32
32
33 QStringList storedApplications() const;
33 QStringList storedApplications() const;
34 void setStoredApplications(const QStringList &storedApplications);
34 void setStoredApplications(const QStringList &storedApplications);
35+
36+Q_SIGNALS:
37+ void changed();
38+
39+private Q_SLOTS:
40+ void onSettingsChanged(const QString &key);
41+
42+private:
43+ QGSettings *m_gSettings;
35};
44};
36
45
37#endif
46#endif
plugins/Unity/Launcher/launchermodel.cpp+75−32
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright 2013 Canonical Ltd.
2+ * Copyright 2013-2014 Canonical Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License as published by
5 * it under the terms of the GNU Lesser General Public License as published by
@@ -40,20 +40,9 @@ LauncherModel::LauncherModel(QObject *parent):
40 connect(m_dbusIface, &DBusInterface::countVisibleChanged, this, &LauncherModel::countVisibleChanged);
40 connect(m_dbusIface, &DBusInterface::countVisibleChanged, this, &LauncherModel::countVisibleChanged);
41 connect(m_dbusIface, &DBusInterface::refreshCalled, this, &LauncherModel::refresh);
41 connect(m_dbusIface, &DBusInterface::refreshCalled, this, &LauncherModel::refresh);
42
42
43− Q_FOREACH (const QString &entry, m_settings->storedApplications()) {
43+ connect(m_settings, &GSettings::changed, this, &LauncherModel::refresh);
44− DesktopFileHandler desktopFile(entry);
45− if (!desktopFile.isValid()) {
46− qWarning() << "Couldn't find a .desktop file for" << entry << ". Skipping...";
47− continue;
48− }
49
44
50− LauncherItem *item = new LauncherItem(entry,
45+ refresh();
51− desktopFile.displayName(),
52− desktopFile.icon(),
53− this);
54− item->setPinned(true);
55− m_list.append(item);
56− }
57}
46}
58
47
59LauncherModel::~LauncherModel()
48LauncherModel::~LauncherModel()
@@ -173,22 +162,7 @@ void LauncherModel::pin(const QString &appId, int index)
173
162
174void LauncherModel::requestRemove(const QString &appId)
163void LauncherModel::requestRemove(const QString &appId)
175{
164{
176− int index = findApplication(appId);
165+ unpin(appId);
177− if (index < 0) {
178− return;
179− }
180−
181− if (m_appManager->findApplication(appId)) {
182− m_list.at(index)->setPinned(false);
183− QModelIndex modelIndex = this->index(index);
184− Q_EMIT dataChanged(modelIndex, modelIndex, QVector<int>() << RolePinned);
185− return;
186− }
187−
188− beginRemoveRows(QModelIndex(), index, index);
189− m_list.takeAt(index)->deleteLater();
190− endRemoveRows();
191−
192 storeAppList();
166 storeAppList();
193}
167}
194
168
@@ -299,6 +273,24 @@ void LauncherModel::storeAppList()
299 m_settings->setStoredApplications(appIds);
273 m_settings->setStoredApplications(appIds);
300}
274}
301
275
276+void LauncherModel::unpin(const QString &appId)
277+{
278+ int index = findApplication(appId);
279+ if (index < 0) {
280+ return;
281+ }
282+
283+ if (m_appManager->findApplication(appId)) {
284+ m_list.at(index)->setPinned(false);
285+ QModelIndex modelIndex = this->index(index);
286+ Q_EMIT dataChanged(modelIndex, modelIndex, QVector<int>() << RolePinned);
287+ } else {
288+ beginRemoveRows(QModelIndex(), index, index);
289+ m_list.takeAt(index)->deleteLater();
290+ endRemoveRows();
291+ }
292+}
293+
302int LauncherModel::findApplication(const QString &appId)
294int LauncherModel::findApplication(const QString &appId)
303{
295{
304 for (int i = 0; i < m_list.count(); ++i) {
296 for (int i = 0; i < m_list.count(); ++i) {
@@ -320,7 +312,6 @@ void LauncherModel::progressChanged(const QString &appId, int progress)
320 }
312 }
321}
313}
322
314
323−
324void LauncherModel::countChanged(const QString &appId, int count)
315void LauncherModel::countChanged(const QString &appId, int count)
325{
316{
326 int idx = findApplication(appId);
317 int idx = findApplication(appId);
@@ -362,10 +353,15 @@ void LauncherModel::countVisibleChanged(const QString &appId, int countVisible)
362
353
363void LauncherModel::refresh()
354void LauncherModel::refresh()
364{
355{
356+ // First walk through all the existing items and see if we need to remove something
365 QList<LauncherItem*> toBeRemoved;
357 QList<LauncherItem*> toBeRemoved;
366 Q_FOREACH (LauncherItem* item, m_list) {
358 Q_FOREACH (LauncherItem* item, m_list) {
367 DesktopFileHandler desktopFile(item->appId());
359 DesktopFileHandler desktopFile(item->appId());
368 if (!desktopFile.isValid()) {
360 if (!desktopFile.isValid()) {
361+ // Desktop file not available for this app => drop it!
362+ toBeRemoved << item;
363+ } else if (!m_settings->storedApplications().contains(item->appId())) {
364+ // Item not in settings any more => drop it!
369 toBeRemoved << item;
365 toBeRemoved << item;
370 } else {
366 } else {
371 int idx = m_list.indexOf(item);
367 int idx = m_list.indexOf(item);
@@ -376,7 +372,54 @@ void LauncherModel::refresh()
376 }
372 }
377
373
378 Q_FOREACH (LauncherItem* item, toBeRemoved) {
374 Q_FOREACH (LauncherItem* item, toBeRemoved) {
379− requestRemove(item->appId());
375+ unpin(item->appId());
376+ }
377+
378+ // This brings the Launcher into sync with the settings backend again. There's an issue though:
379+ // If we can't find a .desktop file for an entry we need to skip it. That makes our settingsIndex
380+ // go out of sync with the actual index of items. So let's also use an addedIndex which reflects
381+ // the settingsIndex minus the skipped items.
382+ int addedIndex = 0;
383+
384+ // Now walk through settings and see if we need to add something
385+ for (int settingsIndex = 0; settingsIndex < m_settings->storedApplications().count(); ++settingsIndex) {
386+ QString entry = m_settings->storedApplications().at(settingsIndex);
387+ int itemIndex = -1;
388+ for (int i = 0; i < m_list.count(); ++i) {
389+ if (m_list.at(i)->appId() == entry) {
390+ itemIndex = i;
391+ break;
392+ }
393+ }
394+
395+ if (itemIndex == -1) {
396+ // Need to add it. Just add it into the addedIndex to keep same ordering as the list
397+ // in the settings.
398+ DesktopFileHandler desktopFile(entry);
399+ if (!desktopFile.isValid()) {
400+ qWarning() << "Couldn't find a .desktop file for" << entry << ". Skipping...";
401+ continue;
402+ }
403+
404+ LauncherItem *item = new LauncherItem(entry,
405+ desktopFile.displayName(),
406+ desktopFile.icon(),
407+ this);
408+ item->setPinned(true);
409+ beginInsertRows(QModelIndex(), addedIndex, addedIndex);
410+ m_list.insert(addedIndex, item);
411+ endInsertRows();
412+ } else if (itemIndex != addedIndex) {
413+ // The item is already there, but it is in a different place then in the settings.
414+ // Move it to the addedIndex
415+ beginMoveRows(QModelIndex(), itemIndex, itemIndex, QModelIndex(), addedIndex);
416+ m_list.move(itemIndex, addedIndex);
417+ endMoveRows();
418+ }
419+
420+ // Just like settingsIndex, this will increase with every item, except the ones we
421+ // skipped with the "continue" call above.
422+ addedIndex++;
380 }
423 }
381}
424}
382
425
plugins/Unity/Launcher/launchermodel.h+5−1
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright 2013 Canonical Ltd.
2+ * Copyright 2013-2014 Canonical Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License as published by
5 * it under the terms of the GNU Lesser General Public License as published by
@@ -62,6 +62,8 @@ public Q_SLOTS:
62private:
62private:
63 void storeAppList();
63 void storeAppList();
64
64
65+ void unpin(const QString &appId);
66+
65private Q_SLOTS:
67private Q_SLOTS:
66 void countChanged(const QString &appId, int count);
68 void countChanged(const QString &appId, int count);
67 void countVisibleChanged(const QString &appId, int count);
69 void countVisibleChanged(const QString &appId, int count);
@@ -79,6 +81,8 @@ private:
79 DBusInterface *m_dbusIface;
81 DBusInterface *m_dbusIface;
80
82
81 ApplicationManagerInterface *m_appManager;
83 ApplicationManagerInterface *m_appManager;
84+
85+ friend class LauncherModelTest;
82};
86};
83
87
84#endif // LAUNCHERMODEL_H
88#endif // LAUNCHERMODEL_H
tests/plugins/Unity/Launcher/gsettings.cpp+8−4
@@ -19,8 +19,6 @@
19
19
20#include "gsettings.h"
20#include "gsettings.h"
21
21
22−#include <QDebug>
23−
24// This is a mock implementation to not touch GSettings for real during tests
22// This is a mock implementation to not touch GSettings for real during tests
25
23
26GSettings::GSettings(QObject *parent):
24GSettings::GSettings(QObject *parent):
@@ -31,10 +29,16 @@ GSettings::GSettings(QObject *parent):
31
29
32QStringList GSettings::storedApplications() const
30QStringList GSettings::storedApplications() const
33{
31{
34− return QStringList();
32+ return m_entries;
35}
33}
36
34
37void GSettings::setStoredApplications(const QStringList &storedApplications)
35void GSettings::setStoredApplications(const QStringList &storedApplications)
38{
36{
37+ m_entries = storedApplications;
38+}
39+
40+void GSettings::simulateDConfChanged(const QStringList &storedApplications)
41+{
39− Q_UNUSED(storedApplications)
42+ setStoredApplications(storedApplications);
43+ Q_EMIT changed();
40}
44}
tests/plugins/Unity/Launcher/gsettings.h+8
@@ -33,6 +33,14 @@ public:
33
33
34 QStringList storedApplications() const;
34 QStringList storedApplications() const;
35 void setStoredApplications(const QStringList &storedApplications);
35 void setStoredApplications(const QStringList &storedApplications);
36+
37+ void simulateDConfChanged(const QStringList &storedApplications);
38+
39+Q_SIGNALS:
40+ void changed();
41+
42+private:
43+ QStringList m_entries;
36};
44};
37
45
38#endif
46#endif
tests/plugins/Unity/Launcher/launchermodeltest.cpp+87−11
@@ -24,6 +24,7 @@
24#include "launcheritem.h"
24#include "launcheritem.h"
25#include "launchermodel.h"
25#include "launchermodel.h"
26#include "dbusinterface.h"
26#include "dbusinterface.h"
27+#include "gsettings.h"
27
28
28#include <QtTest>
29#include <QtTest>
29#include <QDBusInterface>
30#include <QDBusInterface>
@@ -81,7 +82,15 @@ public:
81 return nullptr;
82 return nullptr;
82 }
83 }
83 unity::shell::application::ApplicationInfoInterface *startApplication(const QString &, const QStringList &) { return nullptr; }
84 unity::shell::application::ApplicationInfoInterface *startApplication(const QString &, const QStringList &) { return nullptr; }
84− bool stopApplication(const QString &) { return false; }
85+ bool stopApplication(const QString &appId) {
86+ Q_FOREACH(MockApp* app, m_list) {
87+ if (app->appId() == appId) {
88+ removeApplication(m_list.indexOf(app));
89+ return true;
90+ }
91+ }
92+ return false;
93+ }
85 bool focusApplication(const QString &appId) {
94 bool focusApplication(const QString &appId) {
86 Q_FOREACH(MockApp* app, m_list) {
95 Q_FOREACH(MockApp* app, m_list) {
87 app->setFocused(app->appId() == appId);
96 app->setFocused(app->appId() == appId);
@@ -137,6 +146,8 @@ private Q_SLOTS:
137
146
138 appManager->addApplication(new MockApp("no-icon"));
147 appManager->addApplication(new MockApp("no-icon"));
139 QCOMPARE(launcherModel->rowCount(QModelIndex()), 2);
148 QCOMPARE(launcherModel->rowCount(QModelIndex()), 2);
149+
150+ launcherModel->m_settings->setStoredApplications(QStringList());
140 }
151 }
141
152
142 // Removing apps from appmanager and launcher as pinned ones would stick
153 // Removing apps from appmanager and launcher as pinned ones would stick
@@ -172,14 +183,15 @@ private Q_SLOTS:
172 launcherModel->pin(launcherModel->get(0)->appId());
183 launcherModel->pin(launcherModel->get(0)->appId());
173 QCOMPARE(launcherModel->get(0)->pinned(), true);
184 QCOMPARE(launcherModel->get(0)->pinned(), true);
174 QCOMPARE(launcherModel->get(1)->pinned(), false);
185 QCOMPARE(launcherModel->get(1)->pinned(), false);
175− QCOMPARE(spy.count(), 1);
186+ QCOMPARE(spy.count() > 0, true);
176 QCOMPARE(spy.at(0).at(2).value<QVector<int>>().first(), (int)LauncherModelInterface::RolePinned);
187 QCOMPARE(spy.at(0).at(2).value<QVector<int>>().first(), (int)LauncherModelInterface::RolePinned);
177
188
189+ spy.clear();
178 launcherModel->requestRemove(launcherModel->get(0)->appId());
190 launcherModel->requestRemove(launcherModel->get(0)->appId());
179 QCOMPARE(launcherModel->get(0)->pinned(), false);
191 QCOMPARE(launcherModel->get(0)->pinned(), false);
180 QCOMPARE(launcherModel->get(1)->pinned(), false);
192 QCOMPARE(launcherModel->get(1)->pinned(), false);
181− QCOMPARE(spy.count(), 2);
193+ QCOMPARE(spy.count(), 1);
182− QCOMPARE(spy.at(1).at(2).value<QVector<int>>().first(), (int)LauncherModelInterface::RolePinned);
194+ QCOMPARE(spy.at(0).at(2).value<QVector<int>>().first(), (int)LauncherModelInterface::RolePinned);
183 }
195 }
184
196
185 void testRemove_data() {
197 void testRemove_data() {
@@ -188,7 +200,7 @@ private Q_SLOTS:
188 QTest::addColumn<bool>("running");
200 QTest::addColumn<bool>("running");
189
201
190 QTest::newRow("non-pinned, running") << false << true;
202 QTest::newRow("non-pinned, running") << false << true;
191− QTest::newRow("pinned, running") << true << false;
203+ QTest::newRow("pinned, running") << true << true;
192 QTest::newRow("pinned, non-running") << true << false;
204 QTest::newRow("pinned, non-running") << true << false;
193 }
205 }
194
206
@@ -201,23 +213,24 @@ private Q_SLOTS:
201
213
202 // pin one if required
214 // pin one if required
203 if (pinned) {
215 if (pinned) {
204− launcherModel->pin(launcherModel->get(1)->appId());
216+ launcherModel->pin("abs-icon");
205 }
217 }
206
218
207 // stop it if required
219 // stop it if required
208 if (!running) {
220 if (!running) {
209− appManager->removeApplication(1);
221+ appManager->stopApplication("abs-icon");
210 }
222 }
211
223
212 // Now remove it
224 // Now remove it
213− launcherModel->requestRemove(launcherModel->get(1)->appId());
225+ launcherModel->requestRemove("abs-icon");
214
226
215 if (running) {
227 if (running) {
216 // both apps are running, both apps must still be here
228 // both apps are running, both apps must still be here
217 QCOMPARE(launcherModel->rowCount(QModelIndex()), 2);
229 QCOMPARE(launcherModel->rowCount(QModelIndex()), 2);
218
230
219 // Item must be unpinned now
231 // Item must be unpinned now
232+ int index = launcherModel->findApplication("abs-icon");
220− QCOMPARE(launcherModel->get(1)->pinned(), false);
233+ QCOMPARE(launcherModel->get(index)->pinned(), false);
221
234
222 } else if (pinned) {
235 } else if (pinned) {
223 // Item 1 must go away, item 0 is here to stay
236 // Item 1 must go away, item 0 is here to stay
@@ -226,7 +239,7 @@ private Q_SLOTS:
226
239
227 // done our checks. now stop the app if was still running
240 // done our checks. now stop the app if was still running
228 if (running) {
241 if (running) {
229− appManager->removeApplication(1);
242+ appManager->stopApplication("abs-icon");
230 }
243 }
231
244
232 // It needs to go away in any case now
245 // It needs to go away in any case now
@@ -371,12 +384,75 @@ private Q_SLOTS:
371 QCOMPARE(launcherModel->get(index)->count(), 55);
384 QCOMPARE(launcherModel->get(index)->count(), 55);
372 }
385 }
373
386
374− void testRefresh() {
387+ void testRefreshAfterDeletedDesktopFiles_data() {
388+ QTest::addColumn<bool>("deleted");
389+ QTest::newRow("have .desktop files") << false;
390+ QTest::newRow("deleted .desktop files") << true;
391+ }
392+
393+ void testRefreshAfterDeletedDesktopFiles() {
394+ QFETCH(bool, deleted);
395+
396+ // pin both apps
397+ launcherModel->pin("abs-icon");
398+ launcherModel->pin("no-icon");
399+ // close both apps
400+ appManager->removeApplication(0);
401+ appManager->removeApplication(0);
402+
403+ // "delete" the .desktop files
404+ QString oldCurrent = QDir::currentPath();
405+ if (deleted) {
406+ // In testing mode, the launcher searches the current dir for the sample .desktop file
407+ // We can make that fail by changing the current dir
408+ QDir::setCurrent("..");
409+ }
410+
411+ // Call refresh
375 QDBusInterface interface("com.canonical.Unity.Launcher", "/com/canonical/Unity/Launcher", "com.canonical.Unity.Launcher");
412 QDBusInterface interface("com.canonical.Unity.Launcher", "/com/canonical/Unity/Launcher", "com.canonical.Unity.Launcher");
376 QDBusReply<void> reply = interface.call("Refresh");
413 QDBusReply<void> reply = interface.call("Refresh");
377
414
378 // Make sure the call to Refresh returned without error.
415 // Make sure the call to Refresh returned without error.
379 QCOMPARE(reply.isValid(), true);
416 QCOMPARE(reply.isValid(), true);
417+
418+ QCOMPARE(launcherModel->rowCount(), deleted ? 0 : 2);
419+
420+ // Restoring current dir
421+ QDir::setCurrent(oldCurrent);
422+ }
423+
424+ void testSettings() {
425+ GSettings *settings = launcherModel->m_settings;
426+
427+ // Nothing pinned at startup
428+ QCOMPARE(settings->storedApplications().count(), 0);
429+
430+ // pin both apps
431+ launcherModel->pin("abs-icon");
432+ launcherModel->pin("no-icon");
433+
434+ // Now settings should have 2 apps
435+ QCOMPARE(settings->storedApplications().count(), 2);
436+
437+ // close both apps
438+ appManager->removeApplication(0);
439+ appManager->removeApplication(0);
440+
441+ // Now settings should have 2 apps
442+ QCOMPARE(settings->storedApplications().count(), 2);
443+
444+ // Now remove 1 app through the backend, make sure one is still there
445+ settings->simulateDConfChanged(QStringList() << "abs-icon");
446+ QCOMPARE(settings->storedApplications().count(), 1);
447+
448+ // Check if it disappeared from the frontend too
449+ QCOMPARE(launcherModel->rowCount(), 1);
450+
451+ // Add them back but in reverse order
452+ settings->simulateDConfChanged(QStringList() << "no-icon" << "abs-icon");
453+ QCOMPARE(launcherModel->rowCount(), 2);
454+ QCOMPARE(launcherModel->get(0)->appId(), QString("no-icon"));
455+ QCOMPARE(launcherModel->get(1)->appId(), QString("abs-icon"));
380 }
456 }
381};
457};
382
458