2
* Copyright (C) 2010-2011 Canonical, Ltd.
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.
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.
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/>.
17
#include "application.h"
18
#include "applicationslist.h"
19
#include "webfavorite.h"
20
#include "applicationslistdbus.h"
22
#include "bamf-matcher.h"
23
#include "bamf-application.h"
24
#include "gconfitem-qml-wrapper.h"
30
#include <QStringList>
32
#include <QDBusConnection>
33
#include <QDBusMessage>
45
#define DBUS_SERVICE_UNITY "com.canonical.Unity"
46
#define DBUS_SERVICE_LAUNCHER_ENTRY "com.canonical.Unity.LauncherEntry"
47
#define DBUS_SERVICE_LAUNCHER "com.canonical.Unity.Launcher"
48
#define DBUS_OBJECT_LAUNCHER "/com/canonical/Unity/Launcher"
50
/* List of executables that are too generic to be matched against a single application. */
51
static const QStringList EXECUTABLES_BLACKLIST = (QStringList() << "xdg-open");
52
static const QByteArray LATEST_SETTINGS_MIGRATION = "3.2.10";
54
ApplicationsList::ApplicationsList(QObject *parent) :
55
QAbstractListModel(parent)
57
QDBusConnection session = QDBusConnection::sessionBus();
58
/* FIXME: libunity will send out the Update signal for LauncherEntries
59
only if it finds com.canonical.Unity on the bus, so let's just quickly
60
register ourselves as Unity here. Should be moved somewhere else more proper */
61
if (!session.registerService(DBUS_SERVICE_UNITY)) {
62
UQ_WARNING << "The name" << DBUS_SERVICE_UNITY << "is already taken on DBUS";
64
/* Set ourselves up to receive any Update signal coming from any
66
session.connect(QString(), QString(),
67
DBUS_SERVICE_LAUNCHER_ENTRY, "Update",
68
this, SLOT(onRemoteEntryUpdated(QString,QMap<QString,QVariant>)));
71
if (!session.registerService(DBUS_SERVICE_LAUNCHER)) {
72
UQ_WARNING << "The name" << DBUS_SERVICE_LAUNCHER << "is already taken on DBUS";
74
/* Set ourselves up to receive a method call from Software Center asking us to add
75
to favorites an application that is being installed and that the user requested
77
ApplicationsListDBUS *dbusAdapter = new ApplicationsListDBUS(this);
78
if (!session.registerObject(DBUS_OBJECT_LAUNCHER, dbusAdapter,
79
QDBusConnection::ExportAllSlots)) {
80
UQ_WARNING << "The object" << DBUS_OBJECT_LAUNCHER << "on" << DBUS_SERVICE_LAUNCHER
81
<< "is already present on DBUS.";
85
/* Register the display to receive startup notifications */
86
Display *xdisplay = QX11Info::display();
87
m_snDisplay = sn_display_new(xdisplay, NULL, NULL);
88
m_snContext = sn_monitor_context_new(m_snDisplay, QX11Info::appScreen(),
89
ApplicationsList::snEventHandler,
91
Unity2dApplication* application = Unity2dApplication::instance();
92
if (application == NULL) {
93
/* This can happen for example when using qmlviewer to run the launcher */
94
UQ_WARNING << "The application is not an Unity2dApplication."
95
"Applications startup notifications will be ignored.";
97
application->installX11EventFilter(this);
100
/* Get the system applications data dirs and be flexible if / is not at the
102
QString xdgDataDir = QFile::decodeName(getenv("XDG_DATA_DIRS"));
103
if (xdgDataDir.isEmpty()) {
104
xdgDataDir = "/usr/local/share/:/usr/share/";
106
Q_FOREACH(const QString& dirName, xdgDataDir.split(':')) {
107
m_xdgApplicationDirs << QDir::cleanPath(dirName + "/applications") + "/";
114
ApplicationsList::snEventHandler(SnMonitorEvent *event, void *user_data)
116
/* This method is static and only forwards the event to a non static method. */
117
((ApplicationsList*)user_data)->onSnMonitorEventReceived(event);
121
ApplicationsList::onSnMonitorEventReceived(SnMonitorEvent *event)
123
SnStartupSequence *sequence = sn_monitor_event_get_startup_sequence(event);
125
switch (sn_monitor_event_get_type (event)) {
126
case SN_MONITOR_EVENT_INITIATED:
127
insertSnStartupSequence(sequence);
129
case SN_MONITOR_EVENT_CHANGED:
130
case SN_MONITOR_EVENT_COMPLETED:
131
case SN_MONITOR_EVENT_CANCELED:
132
/* These events are ignored for now. This is acceptable since the
133
case of a failed application startup is handled by
134
Application::launching being automatically reset to
135
false after a timeout. */
142
ApplicationsList::x11EventFilter(XEvent* xevent)
144
/* libsn specifies that all events need to be forwarded to
145
sn_display_process_event but it is not actually necessary.
146
Forwarding only the events of type ClientMessage.
148
if (xevent->type == ClientMessage) {
149
sn_display_process_event(m_snDisplay, xevent);
155
ApplicationsList::onRemoteEntryUpdated(QString applicationURI, QMap<QString, QVariant> properties)
157
UQ_RETURN_IF_FAIL(calledFromDBus());
158
QString sender = message().service();
160
if (applicationURI.indexOf("application://") == 0) {
161
desktopFile = applicationURI.mid(14);
163
UQ_WARNING << "Ignoring update that didn't come from an application:// URI but from:" << applicationURI;
167
Q_FOREACH(Application *application, m_applications) {
168
if (QFileInfo(application->desktop_file()).fileName() == desktopFile) {
169
application->updateOverlaysState(sender, properties);
174
UQ_WARNING << "Application sent an update but we don't seem to have it in the launcher:" << applicationURI;
177
ApplicationsList::~ApplicationsList()
179
sn_monitor_context_unref(m_snContext);
180
sn_display_unref(m_snDisplay);
182
qDeleteAll(m_applications);
186
ApplicationsList::favoriteFromDesktopFilePath(const QString& _desktopFile) const
188
QString desktopFile(_desktopFile);
189
Q_FOREACH(const QString& applicationDir, m_xdgApplicationDirs) {
190
if (_desktopFile.startsWith(applicationDir)) {
191
desktopFile.remove(applicationDir);
192
desktopFile.replace("/", "-");
200
ApplicationsList::insertApplication(Application* application)
202
/* Insert at the end of the list. */
203
int index = m_applications.size();
205
beginInsertRows(QModelIndex(), index, index);
206
m_applications.insert(index, application);
208
if (!application->desktop_file().isEmpty()) {
209
m_applicationForDesktopFile.insert(application->desktop_file(), application);
211
QString executable = application->executable();
212
if (!executable.isEmpty() && !EXECUTABLES_BLACKLIST.contains(executable)) {
213
m_applicationForExecutable.insert(executable, application);
217
QObject::connect(application, SIGNAL(closed()), this, SLOT(onApplicationClosed()));
218
QObject::connect(application, SIGNAL(stickyChanged(bool)), this, SLOT(onApplicationStickyChanged(bool)));
219
QObject::connect(application, SIGNAL(launchingChanged(bool)), this, SLOT(onApplicationLaunchingChanged(bool)));
220
QObject::connect(application, SIGNAL(urgentChanged(bool)), this, SLOT(onApplicationUrgentChanged(bool)));
224
ApplicationsList::removeApplication(Application* application)
226
int index = m_applications.indexOf(application);
229
/* application is not present in m_applications */
233
beginRemoveRows(QModelIndex(), index, index);
234
m_applications.removeAt(index);
235
m_applicationForDesktopFile.remove(application->desktop_file());
236
m_applicationForExecutable.remove(application->executable());
239
/* ApplicationsList::removeApplication might have been called in
240
response to a signal emitted by application itself. Do not delete
241
immediately to cater for this case.
243
application->deleteLater();
247
ApplicationsList::onApplicationUserVisibleChanged(bool user_visible)
249
BamfApplication* bamf_application = qobject_cast<BamfApplication*>(sender());
251
insertBamfApplication(bamf_application);
253
/* FIXME: this case has not been implemented yet but it has not been
254
affecting anybody so far. */
258
void ApplicationsList::insertBamfApplication(BamfApplication* bamf_application)
260
/* Only insert BamfApplications for which the user_visible property is true.
261
Monitor that property so that they are inserted/removed dynamically when it changes.
263
Not doing it led to KDE3 applications not showing up in the launcher.
264
Ref.: https://bugs.launchpad.net/unity-2d/+bug/719983
266
QObject::connect(bamf_application, SIGNAL(UserVisibleChanged(bool)), this, SLOT(onApplicationUserVisibleChanged(bool)), Qt::UniqueConnection);
268
if (!bamf_application->user_visible()) {
272
Application* matchingApplication = NULL;
273
Application* newApplication = new Application;
274
newApplication->setBamfApplication(bamf_application);
276
QString executable = newApplication->executable();
277
QString desktop_file = newApplication->desktop_file();
278
if (m_applicationForDesktopFile.contains(desktop_file)) {
279
/* A Application with the same desktop file already exists */
280
matchingApplication = m_applicationForDesktopFile[desktop_file];
281
} else if (m_applicationForExecutable.contains(executable)) {
282
/* A Application with the same executable already exists */
283
matchingApplication = m_applicationForExecutable[executable];
284
/* If the application already registered for that executable has a
285
desktop file assigned then make sure that the one to be inserted
286
has the same desktop file.
288
QString matchingDesktopFile = matchingApplication->desktop_file();
289
if (!matchingDesktopFile.isEmpty() && !desktop_file.isEmpty() &&
290
matchingDesktopFile != desktop_file) {
291
matchingApplication = NULL;
295
if (matchingApplication != NULL) {
296
/* A Application that corresponds to bamf_application already exists */
297
/* FIXME: this deletion blocks for a long time (around 100ms here) and
298
leads to a visual glitch in the launcher when an application finished
299
starting up. This is due to the deletion of the QFileSystemWatcher
300
belonging to the Application. */
301
delete newApplication;
302
matchingApplication->setBamfApplication(bamf_application);
304
insertApplication(newApplication);
309
ApplicationsList::insertFavoriteApplication(const QString& desktop_file)
311
if (m_applicationForDesktopFile.contains(desktop_file)) {
315
/* Create a new Application */
316
Application* application = new Application;
317
application->setDesktopFile(desktop_file);
319
/* If the desktop_file property is empty after setting it, it
320
means glib couldn't load the desktop file (probably corrupted) */
321
if (application->desktop_file().isEmpty()) {
322
UQ_WARNING << "Favorite application not added due to desktop file missing or corrupted ("
323
<< desktop_file << ")";
326
/* Register favorite desktop file into BAMF: applications with the same
327
executable file will match with the given desktop file. This replicates
328
the behaviour of Unity that does it automatically when calling libbamf's
329
bamf_matcher_get_application_for_desktop_file.
330
It fixes bug https://bugs.launchpad.net/unity-2d/+bug/739454
331
The need for that API call is odd and causes at least one bug:
332
https://bugs.launchpad.net/unity/+bug/762898
334
BamfMatcher& matcher = BamfMatcher::get_default();
335
matcher.register_favorites(QStringList(application->desktop_file()));
337
insertApplication(application);
338
application->setSticky(true);
343
ApplicationsList::insertWebFavorite(const QUrl& url)
345
if (!url.isValid() || url.isRelative()) {
346
UQ_WARNING << "Invalid URL:" << url;
350
Application* application = new Application;
351
WebFavorite* webfav = new WebFavorite(url, application);
353
application->setDesktopFile(webfav->desktopFile());
354
insertApplication(application);
355
application->setSticky(true);
359
ApplicationsList::insertSnStartupSequence(SnStartupSequence* sequence)
361
if (sequence == NULL) {
365
QString executable = sn_startup_sequence_get_binary_name(sequence);
366
if (EXECUTABLES_BLACKLIST.contains(executable)) {
370
if (m_applicationForExecutable.contains(executable)) {
371
/* A Application with the same executable already exists */
372
m_applicationForExecutable[executable]->setSnStartupSequence(sequence);
374
/* Create a new Application and append it to the list */
375
Application* newApplication = new Application;
376
newApplication->setSnStartupSequence(sequence);
377
insertApplication(newApplication);
382
ApplicationsList::load()
384
/* Migrate the favorites if needed and ignore errors */
385
QByteArray latest_migration = launcherConfiguration().property("favoriteMigration").toString().toAscii();
386
if (latest_migration < LATEST_SETTINGS_MIGRATION) {
387
if(QProcess::execute(INSTALL_PREFIX "/lib/unity/migrate_favorites.py") != 0) {
388
UQ_WARNING << "Unable to run the migrate favorites tool successfully";
392
/* Insert favorites */
393
QString desktop_file;
394
QStringList favorites = launcherConfiguration().property("favorites").toStringList();
396
Q_FOREACH(const QString& favorite, favorites) {
397
insertFavoriteApplication(favorite);
400
/* Insert running applications from Bamf */
401
BamfMatcher& matcher = BamfMatcher::get_default();
402
QScopedPointer<BamfApplicationList> running_applications(matcher.running_applications());
403
BamfApplication* bamf_application;
405
for(int i=0; i<running_applications->size(); i++) {
406
bamf_application = running_applications->at(i);
407
insertBamfApplication(bamf_application);
410
QObject::connect(&matcher, SIGNAL(ViewOpened(BamfView*)), SLOT(onBamfViewOpened(BamfView*)));
414
ApplicationsList::onBamfViewOpened(BamfView* bamf_view)
416
/* Make sure bamf_view is in fact a BamfApplication */
417
BamfApplication* bamf_application;
418
bamf_application = dynamic_cast<BamfApplication*>(bamf_view);
420
if (bamf_application == NULL) {
424
insertBamfApplication(bamf_application);
427
void ApplicationsList::onApplicationClosed()
429
Application* application = static_cast<Application*>(sender());
431
if (!application->sticky() && !application->running()) {
432
removeApplication(application);
437
ApplicationsList::onApplicationStickyChanged(bool sticky)
439
Application* application = static_cast<Application*>(sender());
441
writeFavoritesToGConf();
443
if (!sticky && !application->running()) {
444
removeApplication(application);
449
ApplicationsList::onApplicationLaunchingChanged(bool launching)
451
Application* application = static_cast<Application*>(sender());
453
if (!application->sticky() && !application->running() && !application->launching()) {
454
removeApplication(application);
459
ApplicationsList::onApplicationUrgentChanged(bool urgent)
461
Application* application = static_cast<Application*>(sender());
463
Q_EMIT applicationBecameUrgent(m_applications.indexOf(application));
468
ApplicationsList::writeFavoritesToGConf()
470
QStringList favorites;
472
Q_FOREACH(Application *application, m_applications) {
473
QString desktop_file = application->desktop_file();
474
if (application->sticky()) {
475
favorites.append(favoriteFromDesktopFilePath(desktop_file));
479
launcherConfiguration().blockSignals(true);
480
launcherConfiguration().setProperty("favorites", QVariant(favorites));
481
launcherConfiguration().blockSignals(false);
485
ApplicationsList::rowCount(const QModelIndex &parent) const
489
return m_applications.size();
493
ApplicationsList::data(const QModelIndex &index, int role) const
497
if (!index.isValid()) {
501
return QVariant::fromValue(m_applications.at(index.row()));
505
ApplicationsList::move(int from, int to)
508
/* When moving an item down, the destination index needs to be incremented
509
by one, as explained in the documentation:
510
http://doc.qt.nokia.com/qabstractitemmodel.html#beginMoveRows */
511
beginMoveRows(parent, from, from, parent, to + (to > from ? 1 : 0));
512
m_applications.move(from, to);
515
if (m_applications[from]->sticky() || m_applications[to]->sticky()) {
516
/* Update favorites only if at least one of the applications is a favorite */
517
writeFavoritesToGConf();
521
#include "applicationslist.moc"