Merge lp:~osomon/unity-2d/webfavs into lp:unity-2d/0.4

Proposed by Florian Boucault
Status: Merged
Approved by: Florian Boucault
Approved revision: 394
Merged at revision: 402
Proposed branch: lp:~osomon/unity-2d/webfavs
Merge into: lp:unity-2d/0.4
Diff against target: 928 lines (+617/-12)
18 files modified
.bzrignore (+2/-0)
debian/unity-2d-launcher.install (+1/-0)
launcher/Launcher.qml (+6/-0)
launcher/UnityApplications/CMakeLists.txt (+5/-0)
launcher/UnityApplications/launcherapplication.cpp (+77/-4)
launcher/UnityApplications/launcherapplication.h (+7/-1)
launcher/UnityApplications/launcherapplicationslist.cpp (+17/-0)
launcher/UnityApplications/launcherapplicationslist.h (+2/-0)
launcher/UnityApplications/webfavorite.cpp (+240/-0)
launcher/UnityApplications/webfavorite.h (+62/-0)
launcher/app/CMakeLists.txt (+15/-0)
launcher/app/launcher.cpp (+6/-0)
launcher/app/launcher.xml (+22/-0)
launcher/app/launchercontrol.cpp (+55/-0)
launcher/app/launchercontrol.h (+47/-0)
launcher/app/launcherview.cpp (+46/-7)
launcher/app/launcherview.h (+4/-0)
launcher/app/unity-2d-launcher.service.in (+3/-0)
To merge this branch: bzr merge lp:~osomon/unity-2d/webfavs
Reviewer Review Type Date Requested Status
Florian Boucault (community) Approve
Review via email: mp+46831@code.launchpad.net

This proposal supersedes a proposal from 2011-01-18.

Commit message

[launcher] Add web favorites support.

Web pages can be bookmarked in the launcher like other favorite applications. They will open in the default browser.
Two mechanisms are provided to favorite a web page:
 - drag’n’drop a URL from your browser’s address bar to the launcher (tested with firefox and chromium)
 - a DBus API that accepts a URL string as parameter (interface: com.canonical.Unity2d.Launcher).

The full URL, including the scheme (e.g. "http://") must be provided.

Browser plugins could be implemented to bookmark the current page using the DBus API.

Description of the change

This branch implements web favorites support (bug #669926).

See details of the functionality implemented in the commit message.

To post a comment you must log in.
lp:~osomon/unity-2d/webfavs updated
373. By Olivier Tilloy

Cope with chromium’s unfriendly handling of drag events
in order to accept URLs dragged from its address bar as web favorites.

374. By Olivier Tilloy

Enclose the URL in double quotes in the desktop file.
This fixes opening web favorites with a fragment in the URL.

375. By Olivier Tilloy

Cope with badly configured web servers that don’t return error codes on non-existing files.
If the data is not an actual image, try the next one.

Revision history for this message
Florian Boucault (fboucault) wrote :

I get a SEGFAULT in a special case.

Steps to reproduce:
1. Add a webfav
2. Add the same webfav again
3. Remove the second webfav

review: Needs Fixing (functional)
lp:~osomon/unity-2d/webfavs updated
376. By Olivier Tilloy

Made the web scrapper a child object of the application, so if the application is deleted (i.e. removed from the launcher)
before the scrapping is complete, the launcher won’t segfault.

Revision history for this message
Olivier Tilloy (osomon) wrote :

Fixed in revision 376.
The actual issue was not with duplicate web favorites, but with deleting a favorite before its icon had been fetched.

Revision history for this message
Florian Boucault (fboucault) wrote :

static const QString REL_STORE = ".local/share/applications/";

don't use abbreviations. In this case, you don't need REL_STORE at all, get rid of it.

review: Needs Fixing
Revision history for this message
Florian Boucault (fboucault) wrote :

In LauncherApplicationsList::slotWebscrapperFinished:

don't use abbreviation:

        gboolean res = [...]

review: Needs Fixing
Revision history for this message
Florian Boucault (fboucault) wrote :

Duplicated code detected inn LauncherApplicationsList::insertWebFavorite and LauncherApplicationsList::slotWebscrapperFinished:

    QFile file(desktop_file);
    file.open(QIODevice::WriteOnly);
    file.write(contents.toUtf8());
    file.close();

[...]

    QFile file(filename);
    file.open(QIODevice::WriteOnly);
    file.write(contents);
    file.close();

And that duplication has a delta:

    file.write(contents.toUtf8());

vs.

    file.write(contents);

review: Needs Fixing
Revision history for this message
Florian Boucault (fboucault) wrote :

In LauncherApplicationsList::slotWebscrapperFinished:

application->setDesktopFile(filename);

does not seem necessary since application has already the same desktop filename set. I suppose it's a hack to workaround a bug. Please document it.

review: Needs Fixing
Revision history for this message
Florian Boucault (fboucault) wrote :

In webscrapper.cpp same remark as above for REL_ICON_STORE, drop REL_ICON_STORE and rename ABS_ICON_STORE into ICON_STORE.

review: Needs Fixing
lp:~osomon/unity-2d/webfavs updated
377. By Olivier Tilloy

Cosmetics: renamed a local variable to a more evocative name.

378. By Olivier Tilloy

Simplified the code that checks for the existence of specific directories in $HOME/.local/share/ a good deal.

379. By Olivier Tilloy

Comment to explain a hack.

Revision history for this message
Florian Boucault (fboucault) wrote :

webscrapper's functionality is probably implemented in QWebSettings::iconForUrl but it seems that using it would have a couple of drawbacks:
- dependency on libqtwebkit
- possible bugs: https://bugs.webkit.org/show_bug.cgi?id=29440

lp:~osomon/unity-2d/webfavs updated
380. By Olivier Tilloy

Factor out some common code that writes a desktop file to disk.

Revision history for this message
Olivier Tilloy (osomon) wrote :

I ruled out the dependency on QtWebkit from the beginning as it seemed like a huge one for little benefits (all the more so if it has bugs which we’d need to work around).

lp:~osomon/unity-2d/webfavs updated
381. By Olivier Tilloy

Major refactoring to isolate all the code related to web favorites in its own class.
WebScrapper was renamed to WebFavorite.

Missing bit: the Applications need to monitor their desktop files for changes,k
the hack that was in place to avoid this has been removed.

382. By Olivier Tilloy

Applications now monitor their desktop file for live changes.
This fixes the deferred update of the icon and name of web favorites.

383. By Olivier Tilloy

Remove some useless leftover includes.

384. By Olivier Tilloy

Do not check for null-ness, delete does it already.

Revision history for this message
Florian Boucault (fboucault) wrote :

Looks pretty good.

Make LauncherApplication::setDesktopPath a Q_SLOT and get rid of LauncherApplication::slotDesktopFileChanged

More comments to come.

review: Needs Fixing
lp:~osomon/unity-2d/webfavs updated
385. By Olivier Tilloy

Make LauncherApplication::setDesktopFile(…) a public slot, so it can be directly invoked
when the file system watcher detects that the desktop file has changed.

Revision history for this message
Florian Boucault (fboucault) wrote :

14:32 < Kaleo> oSoMoN: I don't know if you tried but changing contents in a desktop file displayed in the launcher does not work really well
14:32 < Kaleo> oSoMoN: for example I take gcalctool's desktop file
14:32 < Kaleo> oSoMoN: change the icon or description or exec string
14:32 < Kaleo> oSoMoN: and nothing seems to really get updated

Revision history for this message
Florian Boucault (fboucault) wrote :

I get a SEGFAULT in a special case.

Steps to reproduce:
1. Add a webfav
3. Remove the webfav *before* the favicon and title have been retrieved

review: Needs Fixing
lp:~osomon/unity-2d/webfavs updated
386. By Olivier Tilloy

Fix desktop file monitoring in various tricky cases, including a potential bug in QT.
This is largely documented.

387. By Olivier Tilloy

When a desktop file is deleted, remove the corresponding application from the launcher.

Revision history for this message
Olivier Tilloy (osomon) wrote :

All the issues introduced with the major refactoring (see revision 381) should now be fixed.

As a bonus, applications now monitor their desktop files for changes and update their attributes accordingly. If a desktop file is deleted (which may happen for example when a package is removed), the corresponding launcher entry will be removed from favorites, and removed from the launcher if not running.

Please test again and review the changes.

lp:~osomon/unity-2d/webfavs updated
388. By Olivier Tilloy

Merge the latest changes from the trunk, resolving conflicts in launcher/app/launcher.cpp.

Revision history for this message
Florian Boucault (fboucault) wrote :

Issues with commits 386 and 387:

- emitting desktopFileChanged with an empty string when the property's value has not really changed is misleading and will cause trouble in the future.
- signal handlers (slot methods) should not be prefixed 'slot'; 'on' is the convention for prefixing signal handlers though it's much better to name the method by what it actually does when possible.
- m_removedDesktopFile seems to always have the same value as desktop_file().
- LauncherApplicationsList already has a protocol to remove applications when needed through the favoriting/sticky property. The additional code of revision 387 is not welcome.

Thinking that will resolve these issues:
Essentially you do _not_ want to touch desktop_file() at all. It is fine for it to point to a non existent file. What needs to happen is to apply the regular treatment for applications that have no desktop files:
- if the application is running then it should be impossible to favorite it and if it is already favorited, then it should be un-favorited
- if the application is not running then it means it is favorited and should be un-favorited

Bottom line: whenever the desktop file's existence changes LauncherApplication::setSticky should be called to be updated.

Implementing that roughly equates to:
- reverting commit 387
- realising that LauncherApplication::slotDesktopFileChanged is in fact about updating the sticky status depending on the existence of the desktop file and should therefore always call setSticky (and be renamed to something that represents that)
- making LauncherApplication::createMenuActions check not only for !desktop_file().isEmpty() but also for the existence of the desktop file

- OPTIONALLY remember the desktop files of the applications that were favorited by the user and monitor the parent folder of the desktop file for their creation and re-add them to the launcher as favorites them when appropriate (that will also avoid implementing the grace period timer for vi & co at the expense of having a tiny visual glitch where the icon disappears and then reappears immediately in the launcher)

review: Needs Fixing
Revision history for this message
Olivier Tilloy (osomon) wrote :

I agree on most of your points, I’ll update my code accordingly. Please see my comments below.

> signal handlers (slot methods) should not be prefixed 'slot';
> 'on' is the convention for prefixing signal handlers though
> it's much better to name the method by what it actually does
> when possible.

This convention is not part of the coding guidelines. A quick analysis of the header files shows that the two prefixes currently happily coexist in the trunk:

$ grep -r --include="*.h" "void\ on" * | wc -l
32
$ grep -r --include="*.h" "void\ slot" * | wc -l
19

I agree that it would be better to name the methods according to what they actually do, so I’ll do that.

> OPTIONALLY remember the desktop files of the applications that
> were favorited by the user and monitor the parent folder of the
> desktop file for their creation and re-add them to the launcher
> as favorites them when appropriate (that will also avoid implementing
> the grace period timer for vi & co at the expense of having a tiny
> visual glitch where the icon disappears and then reappears
> immediately in the launcher)

This is by no means a "tiny" visual glitch, it’s rather a major issue that will make the launcher look broken. The grace period is fine, it just delays the removal by one second, which I think is perfectly acceptable. Note that it’s not just "vi & co", it’s also the more frequent use case of an application being upgraded: the new desktop file is copied over the old one, effectively equivalent to the file being removed and then written again.
On top of that, monitoring several directories that may contain several hundreds of desktop files has a cost. And if an application is really removed for good, keeping on monitoring the parent folder of its desktop file is useless.

Revision history for this message
Florian Boucault (fboucault) wrote :

> I agree on most of your points, I’ll update my code accordingly. Please see my
> comments below.
>
> > signal handlers (slot methods) should not be prefixed 'slot';
> > 'on' is the convention for prefixing signal handlers though
> > it's much better to name the method by what it actually does
> > when possible.
>
> This convention is not part of the coding guidelines. A quick analysis of the
> header files shows that the two prefixes currently happily coexist in the
> trunk:
>
> $ grep -r --include="*.h" "void\ on" * | wc -l
> 32
> $ grep -r --include="*.h" "void\ slot" * | wc -l
> 19
>
The convention is indeed not in the CODING file though the prefix "on" is in majority in the code base, not only in C++ as you shown already but also in QML:

$ grep -r --include="*.qml" \ on[A-Z] . | wc -l
168

> I agree that it would be better to name the methods according to what they
> actually do, so I’ll do that.

That's the best option indeed.

>
>
> > OPTIONALLY remember the desktop files of the applications that
> > were favorited by the user and monitor the parent folder of the
> > desktop file for their creation and re-add them to the launcher
> > as favorites them when appropriate (that will also avoid implementing
> > the grace period timer for vi & co at the expense of having a tiny
> > visual glitch where the icon disappears and then reappears
> > immediately in the launcher)
>
> This is by no means a "tiny" visual glitch, it’s rather a major issue that
> will make the launcher look broken. The grace period is fine, it just delays
> the removal by one second, which I think is perfectly acceptable. Note that
> it’s not just "vi & co", it’s also the more frequent use case of an
> application being upgraded: the new desktop file is copied over the old one,
> effectively equivalent to the file being removed and then written again.
> On top of that, monitoring several directories that may contain several
> hundreds of desktop files has a cost. And if an application is really removed
> for good, keeping on monitoring the parent folder of its desktop file is
> useless.

I agree. I misread your comment in the code and completely ignored that important case. The timer is therefore not optional.

lp:~osomon/unity-2d/webfavs updated
389. By Olivier Tilloy

Merged the latest changes to trunk.

390. By Olivier Tilloy

Removed a useless member: m_removedDesktopFile was always equal to desktop_file().

391. By Olivier Tilloy

When a desktop file is removed, simply mark the application as not sticky.
The launcher will act accordingly.

392. By Olivier Tilloy

Only applications with a valid, existent desktop file can be favorited.

393. By Olivier Tilloy

Cosmetics: renamed some slots.

Revision history for this message
Florian Boucault (fboucault) wrote :

Olivier, have you pushed everything you wanted to? Can I review?

Revision history for this message
Olivier Tilloy (osomon) wrote :

Yes, it is ready for review!

lp:~osomon/unity-2d/webfavs updated
394. By Olivier Tilloy

Merge the latest changes from the trunk, resolving several conflicts.

Revision history for this message
Olivier Tilloy (osomon) wrote :

Sorry, it turns out recent changes in the trunk prevented this branch from merging correctly, I fixed those issues, it is now really ready for review.

Revision history for this message
Florian Boucault (fboucault) wrote :

Good job!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2011-01-27 16:42:10 +0000
3+++ .bzrignore 2011-02-10 08:40:10 +0000
4@@ -3,6 +3,8 @@
5
6 launcher/UnityApplications/libUnityApplications.so*
7 launcher/app/unity-2d-launcher
8+launcher/app/launcheradaptor.*
9+launcher/app/unity-2d-launcher.service
10 launcher/tests/launchermenutest
11 launcher/tests/launcherviewtest
12
13
14=== modified file 'debian/unity-2d-launcher.install'
15--- debian/unity-2d-launcher.install 2011-01-14 21:41:39 +0000
16+++ debian/unity-2d-launcher.install 2011-02-10 08:40:10 +0000
17@@ -1,4 +1,5 @@
18 usr/bin/unity-2d-launcher
19+usr/share/dbus-1/services/unity-2d-launcher.service
20 usr/share/applications/unity-2d-launcher.desktop
21 usr/share/unity-2d/launcher/*.qml
22 usr/share/unity-2d/launcher/launchermenu.css
23
24=== modified file 'launcher/Launcher.qml'
25--- launcher/Launcher.qml 2011-02-08 11:33:57 +0000
26+++ launcher/Launcher.qml 2011-02-10 08:40:10 +0000
27@@ -147,5 +147,11 @@
28 Connections {
29 target: launcherView
30 onDesktopFileDropped: applications.insertFavoriteApplication(path)
31+ onWebpageUrlDropped: applications.insertWebFavorite(url)
32+ }
33+
34+ Connections {
35+ target: launcherControl
36+ onAddWebFavorite: applications.insertWebFavorite(url)
37 }
38 }
39
40=== modified file 'launcher/UnityApplications/CMakeLists.txt'
41--- launcher/UnityApplications/CMakeLists.txt 2011-02-09 10:48:43 +0000
42+++ launcher/UnityApplications/CMakeLists.txt 2011-02-10 08:40:10 +0000
43@@ -3,6 +3,7 @@
44 pkg_check_modules(QTGCONF REQUIRED libqtgconf)
45 pkg_check_modules(QTDEE REQUIRED libqtdee)
46 pkg_check_modules(DBUSMENUQT REQUIRED dbusmenu-qt)
47+pkg_check_modules(GLIB REQUIRED glib-2.0)
48 pkg_check_modules(GDK REQUIRED gdk-2.0)
49 pkg_check_modules(GIO REQUIRED gio-2.0)
50 pkg_check_modules(WNCK REQUIRED libwnck-1.0)
51@@ -21,6 +22,7 @@
52 launcherplaceslist.cpp
53 trash.cpp
54 launchermenu.cpp
55+ webfavorite.cpp
56 plugin.cpp
57 workspaces.cpp
58 )
59@@ -37,6 +39,7 @@
60 launcherplaceslist.h
61 trash.h
62 launchermenu.h
63+ webfavorite.h
64 plugin.h
65 workspaces.h
66 )
67@@ -59,6 +62,7 @@
68 ${QTGCONF_INCLUDE_DIRS}
69 ${QTDEE_INCLUDE_DIRS}
70 ${DBUSMENUQT_INCLUDE_DIRS}
71+ ${GLIB_INCLUDE_DIRS}
72 ${GDK_INCLUDE_DIRS}
73 ${GIO_INCLUDE_DIRS}
74 ${WNCK_INCLUDE_DIRS}
75@@ -72,6 +76,7 @@
76 ${QTGCONF_LDFLAGS}
77 ${QTDEE_LDFLAGS}
78 ${DBUSMENUQT_LDFLAGS}
79+ ${GLIB_LDFLAGS}
80 ${GDK_LDFLAGS}
81 ${GIO_LDFLAGS}
82 ${WNCK_LDFLAGS}
83
84=== modified file 'launcher/UnityApplications/launcherapplication.cpp'
85--- launcher/UnityApplications/launcherapplication.cpp 2011-02-09 16:44:38 +0000
86+++ launcher/UnityApplications/launcherapplication.cpp 2011-02-10 08:40:10 +0000
87@@ -39,9 +39,15 @@
88 #include <QAction>
89 #include <QDBusInterface>
90 #include <QDBusReply>
91+#include <QFile>
92+#include <QFileSystemWatcher>
93
94-LauncherApplication::LauncherApplication() :
95- m_application(NULL), m_appInfo(NULL), m_sticky(false), m_has_visible_window(false)
96+LauncherApplication::LauncherApplication()
97+ : m_application(NULL)
98+ , m_desktopFileWatcher(NULL)
99+ , m_appInfo(NULL)
100+ , m_sticky(false)
101+ , m_has_visible_window(false)
102 {
103 /* Make sure wnck_set_client_type is called only once */
104 static bool client_type_set = false;
105@@ -174,7 +180,7 @@
106 }
107
108 void
109-LauncherApplication::setDesktopFile(QString desktop_file)
110+LauncherApplication::setDesktopFile(const QString& desktop_file)
111 {
112 QByteArray byte_array = desktop_file.toUtf8();
113 gchar *file = byte_array.data();
114@@ -200,6 +206,73 @@
115 emit nameChanged(name());
116 emit iconChanged(icon());
117 }
118+
119+ monitorDesktopFile(this->desktop_file());
120+}
121+
122+void
123+LauncherApplication::monitorDesktopFile(const QString& path)
124+{
125+ /* Monitor the desktop file for live changes */
126+ if (m_desktopFileWatcher == NULL) {
127+ m_desktopFileWatcher = new QFileSystemWatcher(this);
128+ connect(m_desktopFileWatcher, SIGNAL(fileChanged(const QString&)),
129+ SLOT(onDesktopFileChanged(const QString&)));
130+ }
131+
132+ /* If the file is already being monitored, we shouldn’t need to do anything.
133+ However it seems that in some cases, a change to the file will stop
134+ emiting further fileChanged signals, despite the file still being in the
135+ list of monitored files. This is the case when the desktop file is being
136+ edited in gedit for example. This may be a bug in QT itself.
137+ To work around this issue, remove the path and add it again. */
138+ if (m_desktopFileWatcher->files().contains(path)) {
139+ m_desktopFileWatcher->removePath(path);
140+ }
141+ m_desktopFileWatcher->addPath(path);
142+}
143+
144+void
145+LauncherApplication::onDesktopFileChanged(const QString& path)
146+{
147+ if (m_desktopFileWatcher->files().contains(path) || QFile::exists(path)) {
148+ /* The contents of the file have changed. */
149+ setDesktopFile(path);
150+ }
151+ else {
152+ /* The desktop file has been deleted.
153+ This can happen in a number of cases:
154+ - the package it belongs to has been uninstalled
155+ - the package it belongs to has been upgraded, in which case it is
156+ likely that the desktop file has been removed and a new version of
157+ it has been installed in place of the old version
158+ - the file has been written to using an editor that first saves to a
159+ temporary file and then moves this temporary file to the
160+ destination file, which effectively results in the file being
161+ temporarily deleted (vi for example does that, whereas gedit
162+ doesn’t)
163+ In the first case, we want to remove the application from the
164+ launcher. In the last two cases, we need to consider that the desktop
165+ file’s contents have changed. At this point there is no way to be
166+ sure that the file has been permanently removed, so we want to give
167+ the application a grace period before checking for real deletion. */
168+ QTimer::singleShot(1000, this, SLOT(checkDesktopFileReallyRemoved()));
169+ }
170+}
171+
172+void
173+LauncherApplication::checkDesktopFileReallyRemoved()
174+{
175+ QString path = desktop_file();
176+ if (QFile::exists(path)) {
177+ /* The desktop file hasn’t really been removed, it was only temporarily
178+ deleted. */
179+ setDesktopFile(path);
180+ }
181+ else {
182+ /* The desktop file has really been removed. */
183+ setSticky(false);
184+ }
185 }
186
187 void
188@@ -610,7 +683,7 @@
189 bool is_running = running();
190
191 /* Only applications with a corresponding desktop file can be kept in the launcher */
192- if (!desktop_file().isEmpty()) {
193+ if (QFile::exists(desktop_file())) {
194 QAction* keep = new QAction(m_menu);
195 keep->setCheckable(is_running);
196 keep->setChecked(sticky());
197
198=== modified file 'launcher/UnityApplications/launcherapplication.h'
199--- launcher/UnityApplications/launcherapplication.h 2011-02-09 16:44:38 +0000
200+++ launcher/UnityApplications/launcherapplication.h 2011-02-10 08:40:10 +0000
201@@ -32,6 +32,7 @@
202 #include "bamf-application.h"
203
204 class DBusMenuImporter;
205+class QFileSystemWatcher;
206
207 class LauncherApplication : public LauncherItem
208 {
209@@ -62,8 +63,8 @@
210 bool has_visible_window() const;
211
212 /* setters */
213+ void setDesktopFile(const QString& desktop_file);
214 void setSticky(bool sticky);
215- void setDesktopFile(QString desktop_file);
216 void setBamfApplication(BamfApplication *application);
217
218 /* methods */
219@@ -106,8 +107,12 @@
220 void slotChildRemoved(BamfView*);
221 void onIndicatorMenuUpdated();
222
223+ void onDesktopFileChanged(const QString&);
224+ void checkDesktopFileReallyRemoved();
225+
226 private:
227 BamfApplication *m_application;
228+ QFileSystemWatcher *m_desktopFileWatcher;
229 GDesktopAppInfo *m_appInfo;
230 bool m_sticky;
231 int m_priority;
232@@ -117,6 +122,7 @@
233 int m_indicatorMenusReady;
234
235 void updateBamfApplicationDependentProperties();
236+ void monitorDesktopFile(const QString&);
237 void fetchIndicatorMenus();
238 void createStaticMenuActions();
239 int windowCountOnCurrentWorkspace();
240
241=== modified file 'launcher/UnityApplications/launcherapplicationslist.cpp'
242--- launcher/UnityApplications/launcherapplicationslist.cpp 2011-01-15 01:41:03 +0000
243+++ launcher/UnityApplications/launcherapplicationslist.cpp 2011-02-10 08:40:10 +0000
244@@ -16,6 +16,7 @@
245
246 #include "launcherapplication.h"
247 #include "launcherapplicationslist.h"
248+#include "webfavorite.h"
249
250 #include "bamf-matcher.h"
251 #include "bamf-application.h"
252@@ -129,6 +130,22 @@
253 }
254
255 void
256+LauncherApplicationsList::insertWebFavorite(const QUrl& url)
257+{
258+ if (!url.isValid() || url.isRelative()) {
259+ qWarning() << "Invalid URL:" << url;
260+ return;
261+ }
262+
263+ LauncherApplication* application = new LauncherApplication;
264+ WebFavorite* webfav = new WebFavorite(url, application);
265+
266+ application->setDesktopFile(webfav->desktopFile());
267+ insertApplication(application);
268+ application->setSticky(true);
269+}
270+
271+void
272 LauncherApplicationsList::load()
273 {
274 /* FIXME: applications should be sorted depending on their priority */
275
276=== modified file 'launcher/UnityApplications/launcherapplicationslist.h'
277--- launcher/UnityApplications/launcherapplicationslist.h 2011-01-15 01:41:03 +0000
278+++ launcher/UnityApplications/launcherapplicationslist.h 2011-02-10 08:40:10 +0000
279@@ -21,6 +21,7 @@
280 #include <QList>
281 #include <QVariant>
282 #include <QString>
283+#include <QUrl>
284 #include <QObject>
285 #include <QtDeclarative/qdeclarative.h>
286
287@@ -40,6 +41,7 @@
288 int rowCount(const QModelIndex & parent = QModelIndex()) const;
289
290 Q_INVOKABLE void insertFavoriteApplication(QString desktop_file);
291+ Q_INVOKABLE void insertWebFavorite(const QUrl& url);
292
293 private:
294 void load();
295
296=== added file 'launcher/UnityApplications/webfavorite.cpp'
297--- launcher/UnityApplications/webfavorite.cpp 1970-01-01 00:00:00 +0000
298+++ launcher/UnityApplications/webfavorite.cpp 2011-02-10 08:40:10 +0000
299@@ -0,0 +1,240 @@
300+/*
301+ * Copyright (C) 2011 Canonical, Ltd.
302+ *
303+ * Authors:
304+ * Olivier Tilloy <olivier.tilloy@canonical.com>
305+ *
306+ * This program is free software; you can redistribute it and/or modify
307+ * it under the terms of the GNU General Public License as published by
308+ * the Free Software Foundation; version 3.
309+ *
310+ * This program is distributed in the hope that it will be useful,
311+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
312+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
313+ * GNU General Public License for more details.
314+ *
315+ * You should have received a copy of the GNU General Public License
316+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
317+ */
318+
319+#include <glib.h>
320+
321+#include "webfavorite.h"
322+
323+#include <QDir>
324+#include <QtNetwork/QNetworkAccessManager>
325+#include <QtNetwork/QNetworkRequest>
326+#include <QtNetwork/QNetworkReply>
327+#include <QRegExp>
328+#include <QPixmap>
329+#include <QCryptographicHash>
330+
331+static const QString WEBFAV_STORE = QDir::homePath() + "/.local/share/applications/";
332+static const QString ICON_STORE = QDir::homePath() + "/.local/share/icons/";
333+
334+static void check_store_exists(const QString& path)
335+{
336+ if (!QDir(path).exists()) {
337+ QDir().mkpath(path);
338+ }
339+}
340+
341+#define check_webfav_store_exists() check_store_exists(WEBFAV_STORE)
342+#define check_icon_store_exists() check_store_exists(ICON_STORE)
343+
344+static const QString WEBFAV_DESKTOP_ENTRY =
345+ "[Desktop Entry]\n"
346+ "Version=1.0\n"
347+ "Name={name}\n"
348+ "Exec=xdg-open \"{url}\"\n"
349+ "Type=Application\n"
350+ "Icon=emblem-web\n"
351+ "Categories=Network;\n"
352+ "MimeType=text/html;\n"
353+ "StartupNotify=true\n";
354+
355+static const uint MAX_REDIRECTS = 6;
356+
357+WebFavorite::WebFavorite(const QUrl& url, QObject* parent)
358+ : QObject(parent)
359+ , m_url(url)
360+ , m_redirects(0)
361+{
362+ m_desktopFile = WEBFAV_STORE + "webfav-" + computeUrlHash(url) + ".desktop";
363+
364+ QString contents = WEBFAV_DESKTOP_ENTRY;
365+ QByteArray encoded = url.toEncoded();
366+ contents.replace("{name}", encoded);
367+ contents.replace("{url}", encoded);
368+ writeDesktopFile(contents.toUtf8());
369+
370+ fetchPage();
371+}
372+
373+WebFavorite::~WebFavorite()
374+{
375+}
376+
377+const QString&
378+WebFavorite::desktopFile() const
379+{
380+ return m_desktopFile;
381+}
382+
383+void
384+WebFavorite::writeDesktopFile(const QByteArray& contents) const
385+{
386+ check_webfav_store_exists();
387+ QFile file(m_desktopFile);
388+ file.open(QIODevice::WriteOnly);
389+ file.write(contents);
390+ file.close();
391+}
392+
393+void
394+WebFavorite::modifyDesktopFile(const QString& key, const QString& value) const
395+{
396+ GKeyFile* keyFile = g_key_file_new();
397+ gboolean loaded = g_key_file_load_from_file(keyFile, m_desktopFile.toUtf8().constData(),
398+ (GKeyFileFlags) (G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS), NULL);
399+ if (loaded) {
400+ g_key_file_set_string(keyFile, "Desktop Entry", key.toUtf8().constData(), value.toUtf8().constData());
401+ QByteArray contents = g_key_file_to_data(keyFile, NULL, NULL);
402+ g_key_file_free(keyFile);
403+ writeDesktopFile(contents);
404+ }
405+}
406+
407+void
408+WebFavorite::fetchPage()
409+{
410+ QNetworkAccessManager* manager = new QNetworkAccessManager(this);
411+ connect(manager, SIGNAL(finished(QNetworkReply*)),
412+ SLOT(slotFetchPageFinished(QNetworkReply*)));
413+ manager->get(QNetworkRequest(m_url));
414+}
415+
416+void
417+WebFavorite::slotFetchPageFinished(QNetworkReply* reply)
418+{
419+ QNetworkAccessManager* manager = static_cast<QNetworkAccessManager*>(sender());
420+
421+ if (reply->error() == QNetworkReply::NoError) {
422+ QVariant redirect = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
423+ if (redirect.isValid()) {
424+ m_redirects++;
425+ if (m_redirects < MAX_REDIRECTS) {
426+ m_url = redirect.toUrl();
427+ fetchPage();
428+ }
429+ }
430+ else {
431+ QString data = QString::fromUtf8(reply->readAll());
432+
433+ /* lookup title */
434+ QRegExp reTitle("<title>(.*)</title>", Qt::CaseInsensitive);
435+ int index = reTitle.indexIn(data);
436+ if (index != -1) {
437+ modifyDesktopFile("Name", reTitle.cap(1).simplified());
438+ }
439+
440+ /* lookup favicons */
441+ QRegExp reFavicon1("<link rel=\"apple-touch-icon\".*href=\"(.*)\"", Qt::CaseInsensitive);
442+ reFavicon1.setMinimal(true);
443+ index = reFavicon1.indexIn(data);
444+ if (index != -1) {
445+ m_favicons << reFavicon1.cap(1);
446+ }
447+ QRegExp reFavicon2("<link rel=\"(shortcut )?icon\".*href=\"(.*)\"", Qt::CaseInsensitive);
448+ reFavicon2.setMinimal(true);
449+ index = reFavicon2.indexIn(data);
450+ if (index != -1) {
451+ m_favicons << reFavicon2.cap(2);
452+ }
453+ m_favicons << "/apple-touch-icon.png";
454+ m_favicons << "/favicon.ico";
455+
456+ m_current_favicon = m_favicons.begin();
457+ m_redirects = 0;
458+ tryNextFavicon();
459+ }
460+ }
461+
462+ reply->deleteLater();
463+ manager->deleteLater();
464+}
465+
466+void
467+WebFavorite::tryNextFavicon()
468+{
469+ if (m_current_favicon == m_favicons.end()) {
470+ return;
471+ }
472+
473+ QUrl url(*m_current_favicon);
474+ if (url.isRelative()) {
475+ url = m_url.resolved(url);
476+ }
477+
478+ QNetworkAccessManager* manager = new QNetworkAccessManager(this);
479+ connect(manager, SIGNAL(finished(QNetworkReply*)),
480+ SLOT(slotFetchFaviconFinished(QNetworkReply*)));
481+ manager->get(QNetworkRequest(url));
482+}
483+
484+void
485+WebFavorite::slotFetchFaviconFinished(QNetworkReply* reply)
486+{
487+ QNetworkAccessManager* manager = static_cast<QNetworkAccessManager*>(sender());
488+
489+ if (reply->error() == QNetworkReply::NoError) {
490+ QVariant redirect = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
491+ if (redirect.isValid()) {
492+ m_redirects++;
493+ if (m_redirects < MAX_REDIRECTS) {
494+ *m_current_favicon = redirect.toUrl().toEncoded();
495+ }
496+ else {
497+ m_current_favicon++;
498+ m_redirects = 0;
499+ }
500+ tryNextFavicon();
501+ }
502+ else {
503+ /* Check that the data is actually an image. This will cope with
504+ badly configured web servers that don’t return error codes on
505+ non-existing files. */
506+ QPixmap pixmap;
507+ bool valid = pixmap.loadFromData(reply->readAll());
508+ if (valid) {
509+ check_icon_store_exists();
510+ QUrl url = reply->url();
511+ QString filepath = ICON_STORE + computeUrlHash(url);
512+ QString extension = url.path().mid(url.path().lastIndexOf("."));
513+ QString filename = filepath + extension;
514+ pixmap.save(filename);
515+ modifyDesktopFile("Icon", filename);
516+ }
517+ else {
518+ m_current_favicon++;
519+ m_redirects = 0;
520+ tryNextFavicon();
521+ }
522+ }
523+ }
524+ else {
525+ m_current_favicon++;
526+ m_redirects = 0;
527+ tryNextFavicon();
528+ }
529+
530+ reply->deleteLater();
531+ manager->deleteLater();
532+}
533+
534+QString
535+WebFavorite::computeUrlHash(const QUrl& url)
536+{
537+ return QCryptographicHash::hash(url.toEncoded(), QCryptographicHash::Md5).toHex().constData();
538+}
539+
540
541=== added file 'launcher/UnityApplications/webfavorite.h'
542--- launcher/UnityApplications/webfavorite.h 1970-01-01 00:00:00 +0000
543+++ launcher/UnityApplications/webfavorite.h 2011-02-10 08:40:10 +0000
544@@ -0,0 +1,62 @@
545+/*
546+ * Copyright (C) 2011 Canonical, Ltd.
547+ *
548+ * Authors:
549+ * Olivier Tilloy <olivier.tilloy@canonical.com>
550+ *
551+ * This program is free software; you can redistribute it and/or modify
552+ * it under the terms of the GNU General Public License as published by
553+ * the Free Software Foundation; version 3.
554+ *
555+ * This program is distributed in the hope that it will be useful,
556+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
557+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
558+ * GNU General Public License for more details.
559+ *
560+ * You should have received a copy of the GNU General Public License
561+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
562+ */
563+
564+#ifndef WebFavorite_H
565+#define WebFavorite_H
566+
567+#include <QObject>
568+#include <QUrl>
569+#include <QString>
570+#include <QStringList>
571+
572+class QNetworkReply;
573+
574+class WebFavorite : public QObject
575+{
576+ Q_OBJECT
577+
578+public:
579+ WebFavorite(const QUrl& url, QObject* parent=0);
580+ ~WebFavorite();
581+
582+ const QString& desktopFile() const;
583+
584+private:
585+ QUrl m_url;
586+ QString m_desktopFile;
587+
588+ uint m_redirects;
589+ QStringList m_favicons;
590+ QStringList::iterator m_current_favicon;
591+
592+ static QString computeUrlHash(const QUrl& url);
593+
594+ void writeDesktopFile(const QByteArray& contents) const;
595+ void modifyDesktopFile(const QString& key, const QString& value) const;
596+
597+ void fetchPage();
598+ void tryNextFavicon();
599+
600+private Q_SLOTS:
601+ void slotFetchPageFinished(QNetworkReply*);
602+ void slotFetchFaviconFinished(QNetworkReply*);
603+};
604+
605+#endif // WebFavorite_H
606+
607
608=== modified file 'launcher/app/CMakeLists.txt'
609--- launcher/app/CMakeLists.txt 2011-02-07 22:19:04 +0000
610+++ launcher/app/CMakeLists.txt 2011-02-10 08:40:10 +0000
611@@ -5,19 +5,28 @@
612 # Sources
613 set(launcher_SRCS
614 launcherview.cpp
615+ launchercontrol.cpp
616 )
617
618 set(launcher_MOC_HDRS
619 launcherview.h
620+ launchercontrol.h
621 )
622
623 qt4_wrap_cpp(launcher_MOC_SRCS ${launcher_MOC_HDRS})
624
625+configure_file(unity-2d-launcher.service.in unity-2d-launcher.service)
626+
627+qt4_add_dbus_adaptor(launcher_SRCS launcher.xml
628+ launchercontrol.h LauncherControl
629+ )
630+
631 # Build
632 add_library(uqlauncher ${launcher_SRCS} ${launcher_MOC_SRCS})
633 add_executable(unity-2d-launcher launcher.cpp)
634
635 include_directories(
636+ ${CMAKE_CURRENT_SOURCE_DIR}
637 ${CMAKE_CURRENT_BINARY_DIR}
638 ${GTK_INCLUDE_DIRS}
639 ${X11_INCLUDE_DIRS}
640@@ -27,6 +36,7 @@
641 target_link_libraries(uqlauncher
642 ${QT_QTCORE_LIBRARIES}
643 ${QT_QTGUI_LIBRARIES}
644+ ${QT_QTDBUS_LIBRARIES}
645 ${QT_QTDECLARATIVE_LIBRARIES}
646 ${GTK_LDFLAGS}
647 ${X11_LDFLAGS}
648@@ -44,3 +54,8 @@
649 install(FILES unity-2d-launcher.desktop
650 DESTINATION share/applications
651 )
652+
653+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/unity-2d-launcher.service
654+ DESTINATION share/dbus-1/services
655+ )
656+
657
658=== modified file 'launcher/app/launcher.cpp'
659--- launcher/app/launcher.cpp 2011-02-07 22:19:04 +0000
660+++ launcher/app/launcher.cpp 2011-02-10 08:40:10 +0000
661@@ -31,6 +31,7 @@
662
663 #include "config.h"
664 #include "launcherview.h"
665+#include "launchercontrol.h"
666 #include "unity2dpanel.h"
667
668 int main(int argc, char *argv[])
669@@ -81,6 +82,11 @@
670
671 launcherView->rootContext()->setContextProperty("launcherView", launcherView);
672 launcherView->rootContext()->setContextProperty("panel", &panel);
673+
674+ LauncherControl control;
675+ launcherView->rootContext()->setContextProperty("launcherControl", &control);
676+ control.connectToBus();
677+
678 launcherView->setSource(QUrl("./Launcher.qml"));
679
680 /* Composing the QML declarative view inside the panel */
681
682=== added file 'launcher/app/launcher.xml'
683--- launcher/app/launcher.xml 1970-01-01 00:00:00 +0000
684+++ launcher/app/launcher.xml 2011-02-10 08:40:10 +0000
685@@ -0,0 +1,22 @@
686+<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
687+<node xmlns:dox="http://www.ayatana.org/dbus/dox.dtd">
688+ <dox:d><![CDATA[
689+ @mainpage
690+
691+ An interface to activate the launcher in Unity2d.
692+ ]]></dox:d>
693+ <interface name="com.canonical.Unity2d.Launcher" xmlns:dox="http://www.ayatana.org/dbus/dox.dtd">
694+ <dox:d>
695+ An interface to activate the launcher in Unity2d.
696+ </dox:d>
697+ <method name="AddWebFavorite">
698+ <dox:d><![CDATA[
699+ Request a URL to be added to the launcher as a web favorite.
700+ ]]></dox:d>
701+ <arg name="url" type="s" direction="in">
702+ <dox:d>The URL to be favorited.</dox:d>
703+ </arg>
704+ </method>
705+ </interface>
706+</node>
707+
708
709=== added file 'launcher/app/launchercontrol.cpp'
710--- launcher/app/launchercontrol.cpp 1970-01-01 00:00:00 +0000
711+++ launcher/app/launchercontrol.cpp 2011-02-10 08:40:10 +0000
712@@ -0,0 +1,55 @@
713+/*
714+ * Copyright (C) 2011 Canonical, Ltd.
715+ *
716+ * Authors:
717+ * Olivier Tilloy <olivier.tilloy@canonical.com>
718+ *
719+ * This program is free software; you can redistribute it and/or modify
720+ * it under the terms of the GNU General Public License as published by
721+ * the Free Software Foundation; version 3.
722+ *
723+ * This program is distributed in the hope that it will be useful,
724+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
725+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
726+ * GNU General Public License for more details.
727+ *
728+ * You should have received a copy of the GNU General Public License
729+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
730+ */
731+
732+#include "launchercontrol.h"
733+#include "launcheradaptor.h"
734+
735+#include <QtDBus/QDBusConnection>
736+
737+static const char* LAUNCHER_DBUS_SERVICE = "com.canonical.Unity2d.Launcher";
738+static const char* LAUNCHER_DBUS_OBJECT_PATH = "/Launcher";
739+
740+LauncherControl::LauncherControl(QObject* parent) : QObject(parent)
741+{
742+}
743+
744+LauncherControl::~LauncherControl()
745+{
746+ QDBusConnection::sessionBus().unregisterService(LAUNCHER_DBUS_SERVICE);
747+}
748+
749+bool
750+LauncherControl::connectToBus()
751+{
752+ bool ok = QDBusConnection::sessionBus().registerService(LAUNCHER_DBUS_SERVICE);
753+ if (!ok) {
754+ return false;
755+ }
756+ new LauncherAdaptor(this);
757+ QDBusConnection::sessionBus().registerObject(LAUNCHER_DBUS_OBJECT_PATH, this);
758+
759+ return true;
760+}
761+
762+void
763+LauncherControl::AddWebFavorite(const QString& url)
764+{
765+ Q_EMIT addWebFavorite(url);
766+}
767+
768
769=== added file 'launcher/app/launchercontrol.h'
770--- launcher/app/launchercontrol.h 1970-01-01 00:00:00 +0000
771+++ launcher/app/launchercontrol.h 2011-02-10 08:40:10 +0000
772@@ -0,0 +1,47 @@
773+/*
774+ * Copyright (C) 2011 Canonical, Ltd.
775+ *
776+ * Authors:
777+ * Olivier Tilloy <olivier.tilloy@canonical.com>
778+ *
779+ * This program is free software; you can redistribute it and/or modify
780+ * it under the terms of the GNU General Public License as published by
781+ * the Free Software Foundation; version 3.
782+ *
783+ * This program is distributed in the hope that it will be useful,
784+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
785+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
786+ * GNU General Public License for more details.
787+ *
788+ * You should have received a copy of the GNU General Public License
789+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
790+ */
791+
792+#ifndef LauncherControl_H
793+#define LauncherControl_H
794+
795+#include <QtCore/QObject>
796+#include <QtDBus/QDBusContext>
797+#include <QtDeclarative/qdeclarative.h>
798+
799+class LauncherControl : public QObject, protected QDBusContext
800+{
801+ Q_OBJECT
802+
803+public:
804+ explicit LauncherControl(QObject* parent=0);
805+ ~LauncherControl();
806+
807+ bool connectToBus();
808+
809+public Q_SLOTS:
810+ Q_NOREPLY void AddWebFavorite(const QString& url);
811+
812+Q_SIGNALS:
813+ void addWebFavorite(const QString& url);
814+};
815+
816+QML_DECLARE_TYPE(LauncherControl)
817+
818+#endif // LauncherControl_H
819+
820
821=== modified file 'launcher/app/launcherview.cpp'
822--- launcher/app/launcherview.cpp 2011-01-28 01:56:32 +0000
823+++ launcher/app/launcherview.cpp 2011-02-10 08:40:10 +0000
824@@ -38,14 +38,48 @@
825 setAcceptDrops(true);
826 }
827
828+QList<QUrl>
829+LauncherView::getEventUrls(QDropEvent* event)
830+{
831+ const QMimeData* mimeData = event->mimeData();
832+ if (mimeData->hasUrls()) {
833+ return mimeData->urls();
834+ }
835+ else if (mimeData->hasText()) {
836+ /* When dragging an URL from firefox’s address bar, it is properly
837+ recognized as such by the event. However, the same doesn’t work
838+ for chromium: the URL is recognized as plain text.
839+ We cope with this unfriendly behaviour by trying to build a URL out
840+ of the text. This assumes there’s only one URL. */
841+ QString text = mimeData->text();
842+ QUrl url(text);
843+ if (url.isRelative()) {
844+ /* On top of that, chromium sometimes chops off the scheme… */
845+ url = QUrl("http://" + text);
846+ }
847+ if (url.isValid()) {
848+ QList<QUrl> urls;
849+ urls.append(url);
850+ return urls;
851+ }
852+ }
853+
854+ return QList<QUrl>();
855+}
856+
857 void LauncherView::dragEnterEvent(QDragEnterEvent *event)
858 {
859- // Check that data has a list of URLs and that at least one is
860- // a desktop file.
861- if (!event->mimeData()->hasUrls()) return;
862-
863- foreach (QUrl url, event->mimeData()->urls()) {
864- if (url.scheme() == "file" && url.path().endsWith(".desktop")) {
865+ // Check that data has a list of URLs and that at least one is either
866+ // a desktop file or a web page.
867+ QList<QUrl> urls = getEventUrls(event);
868+
869+ if (urls.isEmpty()) {
870+ return;
871+ }
872+
873+ foreach (QUrl url, urls) {
874+ if ((url.scheme() == "file" && url.path().endsWith(".desktop")) ||
875+ url.scheme().startsWith("http")) {
876 event->acceptProposedAction();
877 break;
878 }
879@@ -61,11 +95,16 @@
880 {
881 bool accepted = false;
882
883- foreach (QUrl url, event->mimeData()->urls()) {
884+ QList<QUrl> urls = getEventUrls(event);
885+ foreach (QUrl url, urls) {
886 if (url.scheme() == "file" && url.path().endsWith(".desktop")) {
887 emit desktopFileDropped(url.path());
888 accepted = true;
889 }
890+ else if (url.scheme().startsWith("http")) {
891+ emit webpageUrlDropped(url);
892+ accepted = true;
893+ }
894 }
895
896 if (accepted) event->accept();
897
898=== modified file 'launcher/app/launcherview.h'
899--- launcher/app/launcherview.h 2011-01-28 01:56:32 +0000
900+++ launcher/app/launcherview.h 2011-02-10 08:40:10 +0000
901@@ -21,6 +21,7 @@
902 #define LAUNCHERVIEW
903
904 #include <QDeclarativeView>
905+#include <QUrl>
906 #include <QDragEnterEvent>
907
908 class LauncherView : public QDeclarativeView
909@@ -33,8 +34,11 @@
910
911 signals:
912 void desktopFileDropped(QString path);
913+ void webpageUrlDropped(const QUrl& url);
914
915 private:
916+ QList<QUrl> getEventUrls(QDropEvent*);
917+
918 void dragEnterEvent(QDragEnterEvent *event);
919 void dropEvent(QDropEvent *event);
920 void dragMoveEvent(QDragMoveEvent *event);
921
922=== added file 'launcher/app/unity-2d-launcher.service.in'
923--- launcher/app/unity-2d-launcher.service.in 1970-01-01 00:00:00 +0000
924+++ launcher/app/unity-2d-launcher.service.in 2011-02-10 08:40:10 +0000
925@@ -0,0 +1,3 @@
926+[D-BUS Service]
927+Name=com.canonical.Unity2d.Launcher
928+Exec=@CMAKE_INSTALL_PREFIX@/bin/unity-2d-launcher

Subscribers

People subscribed via source and target branches

to all changes: