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