Unity 8
launchermodel.cpp
1 /*
2  * Copyright 2013-2014 Canonical Ltd.
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU Lesser General Public License as published by
6  * the Free Software Foundation; version 3.
7  *
8  * This program is distributed in the hope that it will be useful,
9  * but WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11  * GNU Lesser General Public License for more details.
12  *
13  * You should have received a copy of the GNU Lesser General Public License
14  * along with this program. If not, see <http://www.gnu.org/licenses/>.
15  *
16  * Authors:
17  * Michael Zanetti <michael.zanetti@canonical.com>
18  */
19 
20 #include "launchermodel.h"
21 #include "launcheritem.h"
22 #include "gsettings.h"
23 #include "desktopfilehandler.h"
24 #include "dbusinterface.h"
25 #include "asadapter.h"
26 
27 #include <unity/shell/application/ApplicationInfoInterface.h>
28 #include <unity/shell/application/MirSurfaceListInterface.h>
29 
30 #include <QDesktopServices>
31 #include <QDebug>
32 
33 using namespace unity::shell::application;
34 
35 LauncherModel::LauncherModel(QObject *parent):
36  LauncherModelInterface(parent),
37  m_settings(new GSettings(this)),
38  m_dbusIface(new DBusInterface(this)),
39  m_asAdapter(new ASAdapter()),
40  m_appManager(nullptr)
41 {
42  connect(m_dbusIface, &DBusInterface::countChanged, this, &LauncherModel::countChanged);
43  connect(m_dbusIface, &DBusInterface::countVisibleChanged, this, &LauncherModel::countVisibleChanged);
44  connect(m_dbusIface, &DBusInterface::progressChanged, this, &LauncherModel::progressChanged);
45  connect(m_dbusIface, &DBusInterface::refreshCalled, this, &LauncherModel::refresh);
46  connect(m_dbusIface, &DBusInterface::alertCalled, this, &LauncherModel::alert);
47 
48  connect(m_settings, &GSettings::changed, this, &LauncherModel::refresh);
49 
50  refresh();
51 }
52 
53 LauncherModel::~LauncherModel()
54 {
55  while (!m_list.empty()) {
56  m_list.takeFirst()->deleteLater();
57  }
58 
59  delete m_asAdapter;
60 }
61 
62 int LauncherModel::rowCount(const QModelIndex &parent) const
63 {
64  Q_UNUSED(parent)
65  return m_list.count();
66 }
67 
68 QVariant LauncherModel::data(const QModelIndex &index, int role) const
69 {
70  LauncherItem *item = m_list.at(index.row());
71  switch(role) {
72  case RoleAppId:
73  return item->appId();
74  case RoleName:
75  return item->name();
76  case RoleIcon:
77  return item->icon();
78  case RolePinned:
79  return item->pinned();
80  case RoleCount:
81  return item->count();
82  case RoleCountVisible:
83  return item->countVisible();
84  case RoleProgress:
85  return item->progress();
86  case RoleFocused:
87  return item->focused();
88  case RoleAlerting:
89  return item->alerting();
90  case RoleRunning:
91  return item->running();
92  case RoleSurfaceCount:
93  return item->surfaceCount();
94  default:
95  qWarning() << Q_FUNC_INFO << "missing role, implement me";
96  return QVariant();
97  }
98 
99  return QVariant();
100 }
101 
102 unity::shell::launcher::LauncherItemInterface *LauncherModel::get(int index) const
103 {
104  if (index < 0 || index >= m_list.count()) {
105  return 0;
106  }
107  return m_list.at(index);
108 }
109 
110 void LauncherModel::move(int oldIndex, int newIndex)
111 {
112  // Make sure its not moved outside the lists
113  if (newIndex < 0) {
114  newIndex = 0;
115  }
116  if (newIndex >= m_list.count()) {
117  newIndex = m_list.count()-1;
118  }
119 
120  // Nothing to do?
121  if (oldIndex == newIndex) {
122  return;
123  }
124 
125  // QList's and QAbstractItemModel's move implementation differ when moving an item up the list :/
126  // While QList needs the index in the resulting list, beginMoveRows expects it to be in the current list
127  // adjust the model's index by +1 in case we're moving upwards
128  int newModelIndex = newIndex > oldIndex ? newIndex+1 : newIndex;
129 
130  beginMoveRows(QModelIndex(), oldIndex, oldIndex, QModelIndex(), newModelIndex);
131  m_list.move(oldIndex, newIndex);
132  endMoveRows();
133 
134  if (!m_list.at(newIndex)->pinned()) {
135  pin(m_list.at(newIndex)->appId());
136  } else {
137  storeAppList();
138  }
139 }
140 
141 void LauncherModel::pin(const QString &appId, int index)
142 {
143  int currentIndex = findApplication(appId);
144 
145  if (currentIndex >= 0) {
146  if (index == -1 || index == currentIndex) {
147  m_list.at(currentIndex)->setPinned(true);
148  QModelIndex modelIndex = this->index(currentIndex);
149  Q_EMIT dataChanged(modelIndex, modelIndex, {RolePinned});
150  } else {
151  move(currentIndex, index);
152  // move() will store the list to the backend itself, so just exit at this point.
153  return;
154  }
155  } else {
156  if (index == -1) {
157  index = m_list.count();
158  }
159 
160  DesktopFileHandler desktopFile(appId);
161  if (!desktopFile.isValid()) {
162  qWarning() << "Can't pin this application, there is no .desktop file available.";
163  return;
164  }
165 
166  beginInsertRows(QModelIndex(), index, index);
167  LauncherItem *item = new LauncherItem(appId,
168  desktopFile.displayName(),
169  desktopFile.icon(),
170  this);
171  item->setPinned(true);
172  m_list.insert(index, item);
173  endInsertRows();
174  }
175 
176  storeAppList();
177 }
178 
179 void LauncherModel::requestRemove(const QString &appId)
180 {
181  unpin(appId);
182  storeAppList();
183 }
184 
185 void LauncherModel::quickListActionInvoked(const QString &appId, int actionIndex)
186 {
187  const int index = findApplication(appId);
188  if (index < 0) {
189  return;
190  }
191 
192  LauncherItem *item = m_list.at(index);
193  QuickListModel *model = qobject_cast<QuickListModel*>(item->quickList());
194  if (model) {
195  const QString actionId = model->get(actionIndex).actionId();
196 
197  // Check if this is one of the launcher actions we handle ourselves
198  if (actionId == QLatin1String("pin_item")) {
199  if (item->pinned()) {
200  requestRemove(appId);
201  } else {
202  pin(appId);
203  }
204  } else if (actionId == QLatin1String("launch_item")) {
205  QDesktopServices::openUrl(getUrlForAppId(appId));
206  } else if (actionId == QLatin1String("stop_item")) { // Quit
207  if (m_appManager) {
208  m_appManager->stopApplication(appId);
209  }
210  // Nope, we don't know this action, let the backend forward it to the application
211  } else {
212  // TODO: forward quicklist action to app, possibly via m_dbusIface
213  }
214  }
215 }
216 
217 void LauncherModel::setUser(const QString &username)
218 {
219  Q_UNUSED(username)
220  qWarning() << "This backend doesn't support multiple users";
221 }
222 
223 QString LauncherModel::getUrlForAppId(const QString &appId) const
224 {
225  // appId is either an appId or a legacy app name. Let's find out which
226  if (appId.isEmpty()) {
227  return QString();
228  }
229 
230  if (!appId.contains('_')) {
231  return "application:///" + appId + ".desktop";
232  }
233 
234  QStringList parts = appId.split('_');
235  QString package = parts.value(0);
236  QString app = parts.value(1, QStringLiteral("first-listed-app"));
237  return "appid://" + package + "/" + app + "/current-user-version";
238 }
239 
240 ApplicationManagerInterface *LauncherModel::applicationManager() const
241 {
242  return m_appManager;
243 }
244 
245 void LauncherModel::setApplicationManager(unity::shell::application::ApplicationManagerInterface *appManager)
246 {
247  // Is there already another appmanager set?
248  if (m_appManager) {
249  // Disconnect any signals
250  disconnect(this, &LauncherModel::applicationAdded, 0, nullptr);
251  disconnect(this, &LauncherModel::applicationRemoved, 0, nullptr);
252  disconnect(this, &LauncherModel::focusedAppIdChanged, 0, nullptr);
253 
254  // remove any recent/running apps from the launcher
255  QList<int> recentAppIndices;
256  for (int i = 0; i < m_list.count(); ++i) {
257  if (m_list.at(i)->recent()) {
258  recentAppIndices << i;
259  }
260  }
261  int run = 0;
262  while (recentAppIndices.count() > 0) {
263  beginRemoveRows(QModelIndex(), recentAppIndices.first() - run, recentAppIndices.first() - run);
264  m_list.takeAt(recentAppIndices.first() - run)->deleteLater();
265  endRemoveRows();
266  recentAppIndices.takeFirst();
267  ++run;
268  }
269  }
270 
271  m_appManager = appManager;
272  connect(m_appManager, &ApplicationManagerInterface::rowsInserted, this, &LauncherModel::applicationAdded);
273  connect(m_appManager, &ApplicationManagerInterface::rowsAboutToBeRemoved, this, &LauncherModel::applicationRemoved);
274  connect(m_appManager, &ApplicationManagerInterface::focusedApplicationIdChanged, this, &LauncherModel::focusedAppIdChanged);
275 
276  Q_EMIT applicationManagerChanged();
277 
278  for (int i = 0; i < appManager->count(); ++i) {
279  applicationAdded(QModelIndex(), i);
280  }
281 }
282 
283 bool LauncherModel::onlyPinned() const
284 {
285  return false;
286 }
287 
288 void LauncherModel::setOnlyPinned(bool onlyPinned) {
289  Q_UNUSED(onlyPinned);
290  qWarning() << "This launcher implementation does not support showing only pinned apps";
291 }
292 
293 void LauncherModel::storeAppList()
294 {
295  QStringList appIds;
296  Q_FOREACH(LauncherItem *item, m_list) {
297  if (item->pinned()) {
298  appIds << item->appId();
299  }
300  }
301  m_settings->setStoredApplications(appIds);
302  m_asAdapter->syncItems(m_list);
303 }
304 
305 void LauncherModel::unpin(const QString &appId)
306 {
307  const int index = findApplication(appId);
308  if (index < 0) {
309  return;
310  }
311 
312  if (m_appManager->findApplication(appId)) {
313  if (m_list.at(index)->pinned()) {
314  m_list.at(index)->setPinned(false);
315  QModelIndex modelIndex = this->index(index);
316  Q_EMIT dataChanged(modelIndex, modelIndex, {RolePinned});
317  }
318  } else {
319  beginRemoveRows(QModelIndex(), index, index);
320  m_list.takeAt(index)->deleteLater();
321  endRemoveRows();
322  }
323 }
324 
325 int LauncherModel::findApplication(const QString &appId)
326 {
327  for (int i = 0; i < m_list.count(); ++i) {
328  LauncherItem *item = m_list.at(i);
329  if (item->appId() == appId) {
330  return i;
331  }
332  }
333  return -1;
334 }
335 
336 void LauncherModel::progressChanged(const QString &appId, int progress)
337 {
338  const int idx = findApplication(appId);
339  if (idx >= 0) {
340  LauncherItem *item = m_list.at(idx);
341  item->setProgress(progress);
342  Q_EMIT dataChanged(index(idx), index(idx), {RoleProgress});
343  }
344 }
345 
346 void LauncherModel::countChanged(const QString &appId, int count)
347 {
348  const int idx = findApplication(appId);
349  if (idx >= 0) {
350  LauncherItem *item = m_list.at(idx);
351  item->setCount(count);
352  QVector<int> changedRoles = {RoleCount};
353  if (item->countVisible() && !item->alerting() && !item->focused()) {
354  changedRoles << RoleAlerting;
355  item->setAlerting(true);
356  }
357  m_asAdapter->syncItems(m_list);
358  Q_EMIT dataChanged(index(idx), index(idx), changedRoles);
359  }
360 }
361 
362 void LauncherModel::countVisibleChanged(const QString &appId, bool countVisible)
363 {
364  int idx = findApplication(appId);
365  if (idx >= 0) {
366  LauncherItem *item = m_list.at(idx);
367  item->setCountVisible(countVisible);
368  QVector<int> changedRoles = {RoleCountVisible};
369  if (countVisible && !item->alerting() && !item->focused()) {
370  changedRoles << RoleAlerting;
371  item->setAlerting(true);
372  }
373  Q_EMIT dataChanged(index(idx), index(idx), changedRoles);
374 
375  // If countVisible goes to false, and the item is neither pinned nor recent we can drop it
376  if (!countVisible && !item->pinned() && !item->recent()) {
377  beginRemoveRows(QModelIndex(), idx, idx);
378  m_list.takeAt(idx)->deleteLater();
379  endRemoveRows();
380  }
381  } else {
382  // Need to create a new LauncherItem and show the highlight
383  DesktopFileHandler desktopFile(appId);
384  if (countVisible && desktopFile.isValid()) {
385  LauncherItem *item = new LauncherItem(appId,
386  desktopFile.displayName(),
387  desktopFile.icon(),
388  this);
389  item->setCountVisible(true);
390  beginInsertRows(QModelIndex(), m_list.count(), m_list.count());
391  m_list.append(item);
392  endInsertRows();
393  }
394  }
395  m_asAdapter->syncItems(m_list);
396 }
397 
398 void LauncherModel::refresh()
399 {
400  // First walk through all the existing items and see if we need to remove something
401  QList<LauncherItem*> toBeRemoved;
402  Q_FOREACH (LauncherItem* item, m_list) {
403  DesktopFileHandler desktopFile(item->appId());
404  if (!desktopFile.isValid()) {
405  // Desktop file not available for this app => drop it!
406  toBeRemoved << item;
407  } else if (!m_settings->storedApplications().contains(item->appId())) {
408  // Item not in settings any more => drop it!
409  toBeRemoved << item;
410  } else {
411  int idx = m_list.indexOf(item);
412  item->setName(desktopFile.displayName());
413  item->setPinned(item->pinned()); // update pinned text if needed
414  item->setRunning(item->running());
415  Q_EMIT dataChanged(index(idx), index(idx), {RoleName, RoleRunning});
416 
417  const QString oldIcon = item->icon();
418  if (oldIcon == desktopFile.icon()) { // same icon file, perhaps different contents, simulate changing the icon name to force reload
419  item->setIcon(QString());
420  Q_EMIT dataChanged(index(idx), index(idx), {RoleIcon});
421  }
422 
423  // now set the icon for real
424  item->setIcon(desktopFile.icon());
425  Q_EMIT dataChanged(index(idx), index(idx), {RoleIcon});
426  }
427  }
428 
429  Q_FOREACH (LauncherItem* item, toBeRemoved) {
430  unpin(item->appId());
431  }
432 
433  bool changed = toBeRemoved.count() > 0;
434 
435  // This brings the Launcher into sync with the settings backend again. There's an issue though:
436  // If we can't find a .desktop file for an entry we need to skip it. That makes our settingsIndex
437  // go out of sync with the actual index of items. So let's also use an addedIndex which reflects
438  // the settingsIndex minus the skipped items.
439  int addedIndex = 0;
440 
441  // Now walk through settings and see if we need to add something
442  for (int settingsIndex = 0; settingsIndex < m_settings->storedApplications().count(); ++settingsIndex) {
443  const QString entry = m_settings->storedApplications().at(settingsIndex);
444  int itemIndex = -1;
445  for (int i = 0; i < m_list.count(); ++i) {
446  if (m_list.at(i)->appId() == entry) {
447  itemIndex = i;
448  break;
449  }
450  }
451 
452  if (itemIndex == -1) {
453  // Need to add it. Just add it into the addedIndex to keep same ordering as the list
454  // in the settings.
455  DesktopFileHandler desktopFile(entry);
456  if (!desktopFile.isValid()) {
457  qWarning() << "Couldn't find a .desktop file for" << entry << ". Skipping...";
458  continue;
459  }
460 
461  LauncherItem *item = new LauncherItem(entry,
462  desktopFile.displayName(),
463  desktopFile.icon(),
464  this);
465  item->setPinned(true);
466  beginInsertRows(QModelIndex(), addedIndex, addedIndex);
467  m_list.insert(addedIndex, item);
468  endInsertRows();
469  changed = true;
470  } else if (itemIndex != addedIndex) {
471  // The item is already there, but it is in a different place than in the settings.
472  // Move it to the addedIndex
473  beginMoveRows(QModelIndex(), itemIndex, itemIndex, QModelIndex(), addedIndex);
474  m_list.move(itemIndex, addedIndex);
475  endMoveRows();
476  changed = true;
477  }
478 
479  // Just like settingsIndex, this will increase with every item, except the ones we
480  // skipped with the "continue" call above.
481  addedIndex++;
482  }
483 
484  if (changed) {
485  Q_EMIT hint();
486  }
487 
488  m_asAdapter->syncItems(m_list);
489 }
490 
491 void LauncherModel::alert(const QString &appId)
492 {
493  int idx = findApplication(appId);
494  if (idx >= 0) {
495  LauncherItem *item = m_list.at(idx);
496  if (!item->focused() && !item->alerting()) {
497  item->setAlerting(true);
498  Q_EMIT dataChanged(index(idx), index(idx), {RoleAlerting});
499  }
500  }
501 }
502 
503 void LauncherModel::applicationAdded(const QModelIndex &parent, int row)
504 {
505  Q_UNUSED(parent);
506 
507  ApplicationInfoInterface *app = m_appManager->get(row);
508  if (!app) {
509  qWarning() << "LauncherModel received an applicationAdded signal, but there's no such application!";
510  return;
511  }
512 
513  if (app->appId() == QLatin1String("unity8-dash")) {
514  // Not adding the dash app
515  return;
516  }
517 
518  const int itemIndex = findApplication(app->appId());
519  if (itemIndex != -1) {
520  LauncherItem *item = m_list.at(itemIndex);
521  if (!item->recent()) {
522  item->setRecent(true);
523  Q_EMIT dataChanged(index(itemIndex), index(itemIndex), {RoleRecent});
524  }
525  if (item->surfaceCount() != app->surfaceCount()) {
526  item->setSurfaceCount(app->surfaceCount());
527  Q_EMIT dataChanged(index(itemIndex), index(itemIndex), {RoleSurfaceCount});
528  }
529 
530  item->setRunning(true);
531  } else {
532  LauncherItem *item = new LauncherItem(app->appId(), app->name(), app->icon().toString(), this);
533  item->setRecent(true);
534  item->setRunning(true);
535  item->setFocused(app->focused());
536  item->setSurfaceCount(app->surfaceCount());
537  beginInsertRows(QModelIndex(), m_list.count(), m_list.count());
538  m_list.append(item);
539  endInsertRows();
540  }
541  connect(app, &ApplicationInfoInterface::surfaceCountChanged, this, &LauncherModel::applicationSurfaceCountChanged);
542  m_asAdapter->syncItems(m_list);
543  Q_EMIT dataChanged(index(itemIndex), index(itemIndex), {RoleRunning});
544 }
545 
546 void LauncherModel::applicationSurfaceCountChanged(int count)
547 {
548  ApplicationInfoInterface *app = static_cast<ApplicationInfoInterface*>(sender());
549  int idx = findApplication(app->appId());
550  if (idx < 0) {
551  qWarning() << "Received a surface count changed event from an app that's not in the Launcher model";
552  return;
553  }
554  LauncherItem *item = m_list.at(idx);
555  if (item->surfaceCount() != count) {
556  item->setSurfaceCount(count);
557  Q_EMIT dataChanged(index(idx), index(idx), {RoleSurfaceCount});
558  }
559 }
560 
561 void LauncherModel::applicationRemoved(const QModelIndex &parent, int row)
562 {
563  Q_UNUSED(parent)
564 
565  ApplicationInfoInterface *app = m_appManager->get(row);
566  int appIndex = -1;
567  for (int i = 0; i < m_list.count(); ++i) {
568  if (m_list.at(i)->appId() == app->appId()) {
569  appIndex = i;
570  break;
571  }
572  }
573 
574  if (appIndex < 0) {
575  qWarning() << Q_FUNC_INFO << "appIndex not found";
576  return;
577  }
578 
579  disconnect(app, &ApplicationInfoInterface::surfaceCountChanged, this, &LauncherModel::applicationSurfaceCountChanged);
580 
581  LauncherItem * item = m_list.at(appIndex);
582 
583  if (!item->pinned()) {
584  beginRemoveRows(QModelIndex(), appIndex, appIndex);
585  m_list.takeAt(appIndex)->deleteLater();
586  endRemoveRows();
587  m_asAdapter->syncItems(m_list);
588  } else {
589  QVector<int> changedRoles = {RoleRunning};
590  item->setRunning(false);
591  if (item->focused()) {
592  changedRoles << RoleFocused;
593  item->setFocused(false);
594  }
595  Q_EMIT dataChanged(index(appIndex), index(appIndex), changedRoles);
596  }
597 }
598 
599 void LauncherModel::focusedAppIdChanged()
600 {
601  const QString appId = m_appManager->focusedApplicationId();
602  for (int i = 0; i < m_list.count(); ++i) {
603  LauncherItem *item = m_list.at(i);
604  if (!item->focused() && item->appId() == appId) {
605  QVector<int> changedRoles;
606  changedRoles << RoleFocused;
607  item->setFocused(true);
608  if (item->alerting()) {
609  changedRoles << RoleAlerting;
610  item->setAlerting(false);
611  }
612  Q_EMIT dataChanged(index(i), index(i), changedRoles);
613  } else if (item->focused() && item->appId() != appId) {
614  item->setFocused(false);
615  Q_EMIT dataChanged(index(i), index(i), {RoleFocused});
616  }
617  }
618 }