Merge lp:~mhr3/unity-scope-click/handle-non-click into lp:unity-scope-click

Proposed by Michal Hruby
Status: Merged
Approved by: Alejandro J. Cura
Approved revision: 73
Merged at revision: 72
Proposed branch: lp:~mhr3/unity-scope-click/handle-non-click
Merge into: lp:unity-scope-click
Diff against target: 864 lines (+741/-5)
8 files modified
Makefile.am (+2/-4)
configure.ac (+8/-0)
non-click.scope (+13/-0)
src/Makefile.am (+6/-1)
src/click-scope.vala (+2/-0)
src/config.vala.in (+12/-0)
src/non-click-scope.vala (+582/-0)
src/utils.vala (+116/-0)
To merge this branch: bzr merge lp:~mhr3/unity-scope-click/handle-non-click
Reviewer Review Type Date Requested Status
Alejandro J. Cura (community) Approve
dobey (community) Abstain
PS Jenkins bot continuous-integration Approve
Review via email: mp+189639@code.launchpad.net

Commit message

Add a new scope which handles non-click apps and scopes.

Description of the change

Add a new scope which handles non-click apps and scopes, so we can remove the legacy applications scope from phone.

To post a comment you must log in.
70. By Michal Hruby

Make the binary link properly

71. By Michal Hruby

Make activation work

72. By Michal Hruby

Fix activation

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
73. By Michal Hruby

Wait to load local scopes

Revision history for this message
dobey (dobey) wrote :

It seems quite late in the cycle to be introducing this large of a change to the system, especially for something that is supposed to be going away very soon anyway, given all the core apps are supposed to be migrated to click packages.

It also seems like a bad idea to hard code a list of .desktop file names to be used here. What happens if the click package for an app is installed after it is migrated, on a system that still has the same app installed via dpkg?

And what about people who choose to test Unity 8 on non-phones in 13.10, given it will be installable as an environment to use as opposed to classic Unity or GNOME? How will they be able to launch Firefox, Inkscape, GIMP, and other apps, for example?

review: Needs Information
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
dobey (dobey) wrote :

Also, if you're going to keep making changes, then the branch isn't ready for review. Please stop adding commits to fix or change things, if it's ready for review. If you haven't finished testing and fixing it, then please change the status back to work in progress, and commit away.

Revision history for this message
Michal Hruby (mhr3) wrote :

> It seems quite late in the cycle to be introducing this large of a change to
> the system, especially for something that is supposed to be going away very
> soon anyway, given all the core apps are supposed to be migrated to click
> packages.

The original plan was that all apps would be click, and we'd be able to remove the apps scope by the end of this cycle, because the desktop apps scope has far too much legacy code in it. Obviously that didn't happen (and will not before 13.10 release), yet we still want to get rid of the desktop apps scope (problems with previews, libraries it uses keep waking up the system, etc..)

> It also seems like a bad idea to hard code a list of .desktop file names to be
> used here. What happens if the click package for an app is installed after it
> is migrated, on a system that still has the same app installed via dpkg?

This is really list of apps that should be click, yet are not. If you want handling of standard .desktop files, you'd install the standard unity-lens-applications.

> And what about people who choose to test Unity 8 on non-phones in 13.10, given
> it will be installable as an environment to use as opposed to classic Unity or
> GNOME? How will they be able to launch Firefox, Inkscape, GIMP, and other
> apps, for example?

See above.

Revision history for this message
Alejandro J. Cura (alecu) wrote :

I agree with dobey's comment about this branch being risky. In any case, I can see that the changes are isolated from the other bits of code, and while testing it on my device everything seems to work, but I'd like for more testing to be done before approving.

I need to run an unexpected errand, so I'm setting my review to abstain. I can re-review when I get back.

review: Abstain
Revision history for this message
dobey (dobey) wrote :

> The original plan was that all apps would be click, and we'd be able to remove
> the apps scope by the end of this cycle, because the desktop apps scope has
> far too much legacy code in it. Obviously that didn't happen (and will not
> before 13.10 release), yet we still want to get rid of the desktop apps scope
> (problems with previews, libraries it uses keep waking up the system, etc..)

Why is an 850+ line diff to move some of that code into this scope, better than smaller changes to fix some of the more pressing issues in the other scope? And how is it less risky to do so, this late in the cycle?

How does this set of changes fix only the linked bug? How does removing the existing scope and making this large change here, related to that bug? Shouldn't there be some other bug about the current scope you wish to get rid of, where these risks and such are being discussed, that this branch would then be linked to?

And how can we be certain there are no regressions from the current scope behavior, by moving the code here and dropping that scope?

Revision history for this message
Michal Hruby (mhr3) wrote :

> Why is an 850+ line diff to move some of that code into this scope, better
> than smaller changes to fix some of the more pressing issues in the other
> scope? And how is it less risky to do so, this late in the cycle?

Yes, it's 850 lines of additions, yet what you don't see here is the ~6k lines of removed code (no legacy apps scope). For example the power issue is not fixable in apps scope, cause we can't simply remove the backend bit that causes it. On the other hand, this uses parts of code that were present in apps scope for the entire cycle (the scopes + index bits), so it's well tested in a way.

> How does this set of changes fix only the linked bug? How does removing the
> existing scope and making this large change here, related to that bug?
> Shouldn't there be some other bug about the current scope you wish to get rid
> of, where these risks and such are being discussed, that this branch would
> then be linked to?

We're also fixing the power issue (lp:1231536) by removing apps scope, I didn't find it a fix for the bug though (click scope never had the issue) as legacy apps scope remains to have the bug, that's why I didn't link it.

> And how can we be certain there are no regressions from the current scope
> behavior, by moving the code here and dropping that scope?

Manually tested + as I said it's using similar, yet not the same code.

Revision history for this message
dobey (dobey) wrote :

> If you want handling of standard .desktop files, you'd install the standard unity-lens-applications.

This is installed on my workstation, but when running unity8 (from trunk with ./build and ./run), it is not showing all the apps I have installed. It's only showing 5 apps (and I have no idea why exactly it's showing only those), and won't let me launch them.

I also see that unity-lens-applications provides the "running apps" scope. Is that scope being moved into here as well? A new separate project? Or where? If it's not moving anywhere, then the unity-lens-applications package will still have to remain on the touch image, meaning this change would introduce duplication, unless the alluded to 6K LoC are actually removed and that scope no longer provides this functionality at all.

Revision history for this message
Michal Hruby (mhr3) wrote :

> This is installed on my workstation, but when running unity8 (from trunk with
> ./build and ./run), it is not showing all the apps I have installed. It's only
> showing 5 apps (and I have no idea why exactly it's showing only those), and
> won't let me launch them.

It's showing only X-Ubuntu-Touch apps, that's by design.

> I also see that unity-lens-applications provides the "running apps" scope. Is
> that scope being moved into here as well? A new separate project? Or where? If
> it's not moving anywhere, then the unity-lens-applications package will still
> have to remain on the touch image, meaning this change would introduce
> duplication, unless the alluded to 6K LoC are actually removed and that scope
> no longer provides this functionality at all.

running-apps scope was supposed to be used with MIR, but it's not going to be used for 13.10, there are a couple of bits missing. Still, it's a standalone scope that's not in any way tied to the rest of apps scopes, so could be very easily moved here if when we need to.

Revision history for this message
dobey (dobey) :
review: Abstain
Revision history for this message
Alejandro J. Cura (alecu) wrote :

445 + apps_model.append ("application://" + desktop_file_id,

I think this needs to be changed to "application:///" (triple slashes) because of bug 1231393.
I don't think this merits blocking the branch, since all relevant .desktop files are already lowercase.

Otherwise, the branch looks fine.

I've tested it on my Nexus 4 by copying non-click.scope to /usr/share/unity/scopes/applications, and removing applications.scope and scopes.scope from said folder, and then manually running from a compiled checkout in the device; and I did not find anything broken.

+1

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile.am'
2--- Makefile.am 2013-08-15 22:56:14 +0000
3+++ Makefile.am 2013-10-07 16:27:29 +0000
4@@ -3,10 +3,8 @@
5 #
6 # Install the click.scope
7 #
8-scope_in_files = \
9- click.scope
10 scopedir = $(SCOPESDIR)/applications
11-scope_DATA = $(scope_in_files)
12+scope_DATA = click.scope non-click.scope
13
14 icondir = $(datadir)/unity/themes
15
16@@ -29,7 +27,7 @@
17
18 EXTRA_DIST = \
19 autogen.sh \
20- $(scope_in_files) \
21+ $(scope_DATA) \
22 AUTHORS \
23 COPYING \
24 MAINTAINERS \
25
26=== modified file 'configure.ac'
27--- configure.ac 2013-09-23 18:26:19 +0000
28+++ configure.ac 2013-10-07 16:27:29 +0000
29@@ -42,6 +42,7 @@
30 GLIB_REQUIRED=2.32
31 PKG_CHECK_MODULES(SCOPE_DAEMON,
32 unity >= 7.0.0
33+ unity-protocol-private >= 7.1.0
34 json-glib-1.0
35 gio-unix-2.0
36 libsoup-2.4
37@@ -102,12 +103,19 @@
38 AC_SUBST(SCOPESDIR)
39
40 #####################################################
41+# Look for protocol-private lib dir
42+#####################################################
43+PROTOCOLPRIVATELIBDIR="`$PKG_CONFIG --variable=libdir unity-protocol-private`/libunity"
44+AC_SUBST(PROTOCOLPRIVATELIBDIR)
45+
46+#####################################################
47 # Create the Makefiles
48 #####################################################
49 AC_CONFIG_FILES([
50 Makefile
51 data/Makefile
52 src/Makefile
53+ src/config.vala
54 ])
55 AC_OUTPUT
56
57
58=== added file 'non-click.scope'
59--- non-click.scope 1970-01-01 00:00:00 +0000
60+++ non-click.scope 2013-10-07 16:27:29 +0000
61@@ -0,0 +1,13 @@
62+[Scope]
63+GroupName=com.canonical.Unity.Scope.Applications.Click
64+UniqueName=/com/canonical/unity/scope/applications/non_click
65+Icon=/usr/share/unity/icons/lens-nav-app.svg
66+Keywords=
67+RemoteContent=false
68+Type=applications
69+Name=Click Packages
70+Description=Find Click Packages
71+SearchHint=Search Click Packages
72+
73+[Desktop Entry]
74+X-Ubuntu-Gettext-Domain=unity-scope-click
75
76=== modified file 'src/Makefile.am'
77--- src/Makefile.am 2013-09-24 20:14:43 +0000
78+++ src/Makefile.am 2013-10-07 16:27:29 +0000
79@@ -10,7 +10,7 @@
80 # we're using Vala, C warnings are useless
81 AM_CFLAGS = -w
82 AM_CPPFLAGS = $(COVERAGE_CFLAGS)
83-AM_LDFLAGS = $(COVERAGE_LDFLAGS)
84+AM_LDFLAGS = $(COVERAGE_LDFLAGS) -R $(PROTOCOLPRIVATELIBDIR)
85
86 pkglibexec_PROGRAMS = \
87 click-scope \
88@@ -19,6 +19,7 @@
89 click_scope_CPPFLAGS = \
90 -DDATADIR=\"$(DATADIR)\" \
91 -DPKGDATADIR=\"$(PKGDATADIR)\" \
92+ -DGETTEXT_PACKAGE=\"$(GETTEXT_PACKAGE)\" \
93 -DG_LOG_DOMAIN=\"unity-scope-click\" \
94 $(SCOPE_DAEMON_CFLAGS) \
95 $(MAINTAINER_CFLAGS) \
96@@ -28,6 +29,7 @@
97 click_scope_VALAFLAGS = \
98 -C \
99 --pkg unity \
100+ --pkg unity-protocol \
101 --pkg gee-1.0 \
102 --pkg gio-unix-2.0 \
103 --pkg json-glib-1.0 \
104@@ -44,12 +46,15 @@
105 $(NULL)
106
107 click_scope_VALASOURCES = \
108+ config.vala \
109 click-scope.vala \
110 click-webservice.vala \
111 fake-data.vala \
112 download-manager.vala \
113 click-interface.vala \
114 ubuntuone-credentials.vala \
115+ non-click-scope.vala \
116+ utils.vala \
117 $(NULL)
118
119 click_scope_SOURCES = \
120
121=== modified file 'src/click-scope.vala'
122--- src/click-scope.vala 2013-10-03 22:35:56 +0000
123+++ src/click-scope.vala 2013-10-07 16:27:29 +0000
124@@ -495,6 +495,7 @@
125 {
126 var scope = new ClickScope();
127 var exporter = new Unity.ScopeDBusConnector (scope);
128+ var exporter2 = new Unity.ScopeDBusConnector (new NonClickScope ());
129 var cache_dir = Environment.get_user_cache_dir ();
130 if (FileUtils.test (cache_dir, FileTest.EXISTS | FileTest.IS_DIR)) {
131 var log_path = Path.build_filename (cache_dir,
132@@ -506,6 +507,7 @@
133
134 try {
135 exporter.export ();
136+ exporter2.export ();
137 } catch (GLib.Error e) {
138 error ("Cannot export scope to DBus: %s", e.message);
139 }
140
141=== added file 'src/config.vala.in'
142--- src/config.vala.in 1970-01-01 00:00:00 +0000
143+++ src/config.vala.in 2013-10-07 16:27:29 +0000
144@@ -0,0 +1,12 @@
145+namespace Config {
146+
147+ const string PREFIX = "@prefix@";
148+
149+ const string DATADIR = "@DATADIR@";
150+
151+ const string LOCALEDIR = "@DATADIR@/locale";
152+
153+ const string PACKAGE = "@PACKAGE@";
154+
155+ const string VERSION = "@VERSION@";
156+}
157
158=== added file 'src/non-click-scope.vala'
159--- src/non-click-scope.vala 1970-01-01 00:00:00 +0000
160+++ src/non-click-scope.vala 2013-10-07 16:27:29 +0000
161@@ -0,0 +1,582 @@
162+/*
163+ * Copyright (C) 2013 Canonical, Ltd.
164+ *
165+ * This program is free software; you can redistribute it and/or modify
166+ * it under the terms of the GNU General Public License as published by
167+ * the Free Software Foundation; version 3.
168+ *
169+ * This program is distributed in the hope that it will be useful,
170+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
171+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
172+ * GNU General Public License for more details.
173+ *
174+ * You should have received a copy of the GNU General Public License
175+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
176+ */
177+
178+using Unity;
179+
180+class NonClickScope: Unity.SimpleScope
181+{
182+ /* List of app IDs that aren't proper click packages yet */
183+ const string[] NON_CLICK_DESKTOPS =
184+ {
185+ "address-book-app.desktop",
186+ "calendar-app.desktop",
187+ "camera-app.desktop",
188+ "click-update-manager.desktop",
189+ "dialer-app.desktop",
190+ "friends-app.desktop",
191+ "gallery-app.desktop",
192+ "mediaplayer-app.desktop",
193+ "messaging-app.desktop",
194+ "music-app.desktop",
195+ "notes-app.desktop",
196+ "rssreader-app.desktop",
197+ "ubuntu-calculator-app.desktop",
198+ "ubuntu-clock-app.desktop",
199+ "ubuntu-filemanager-app.desktop",
200+ "ubuntu-system-settings.desktop",
201+ "ubuntu-terminal-app.desktop",
202+ "ubuntu-weather-app.desktop",
203+ "webbrowser-app.desktop"
204+ };
205+
206+ const string[] invisible_scope_ids =
207+ {
208+ "home.scope",
209+ "applications-scopes.scope",
210+ "applications-non-click.scope"
211+ };
212+
213+ const string LIBUNITY_SCHEMA = "com.canonical.Unity.Lenses";
214+ const string ICON_PATH = Config.DATADIR + "/icons/unity-icon-theme/places/svg/";
215+ const string GENERIC_SCOPE_ICON = ICON_PATH + "service-generic.svg";
216+ const string SCOPES_MODEL = "com.canonical.Unity.SmartScopes.RemoteScopesModel";
217+
218+ private enum RemoteScopesColumn
219+ {
220+ SCOPE_ID,
221+ NAME,
222+ DESCRIPTION,
223+ ICON_HINT,
224+ SCREENSHOT_URL,
225+ KEYWORDS
226+ }
227+
228+ private enum AppsColumn
229+ {
230+ APP_URI,
231+ NAME,
232+ ICON_HINT,
233+ DESCRIPTION,
234+ SCREENSHOT_URL
235+ }
236+
237+ private enum ResultCategory
238+ {
239+ INSTALLED,
240+ SCOPES
241+ }
242+
243+ private Dee.Model remote_scopes_model;
244+ private Dee.Model local_scopes_model;
245+ private Dee.Index remote_scopes_index;
246+ private Dee.Index local_scopes_index;
247+ private Dee.Model apps_model;
248+ private Dee.Index apps_index;
249+
250+ private bool scopes_loaded = false;
251+
252+ private HashTable<unowned string, unowned string> disabled_scope_ids;
253+ private HashTable<string, bool> locked_scope_ids;
254+
255+ public NonClickScope ()
256+ {
257+ Object ();
258+ }
259+
260+ construct
261+ {
262+ this.group_name = "com.canonical.Unity.Scope.Applications.Click";
263+ this.unique_name = "/com/canonical/unity/scope/applications/non_click";
264+
265+ /* set schema */
266+ var schema = new Schema ();
267+ schema.add_field ("scope_disabled", "u", Schema.FieldType.OPTIONAL);
268+ this.schema = schema;
269+
270+ Category cat;
271+ var icon_dir = File.new_for_path (ICON_PATH);
272+
273+ /* set categories */
274+ var categories = new CategorySet ();
275+ cat = new Unity.Category ("installed", _("Installed"),
276+ new FileIcon (icon_dir.get_child ("group-installed.svg")));
277+ categories.add (cat);
278+
279+ cat = new Unity.Category ("scopes", _("Dash plugins"),
280+ new FileIcon (icon_dir.get_child ("group-installed.svg")));
281+ categories.add (cat);
282+ this.category_set = categories;
283+
284+ /* set filters */
285+ var filters = new FilterSet ();
286+ {
287+ var filter = new CheckOptionFilter ("type", _("Type"));
288+ filter.sort_type = Unity.OptionsFilter.SortType.DISPLAY_NAME;
289+
290+ filter.add_option ("accessories", _("Accessories"));
291+ filter.add_option ("education", _("Education"));
292+ filter.add_option ("game", _("Games"));
293+ filter.add_option ("graphics", _("Graphics"));
294+ filter.add_option ("internet", _("Internet"));
295+ filter.add_option ("fonts", _("Fonts"));
296+ filter.add_option ("office", _("Office"));
297+ filter.add_option ("media", _("Media"));
298+ filter.add_option ("customization", _("Customization"));
299+ filter.add_option ("accessibility", _("Accessibility"));
300+ filter.add_option ("developer", _("Developer"));
301+ filter.add_option ("science-and-engineering", _("Science & engineering"));
302+ filter.add_option ("scopes", _("Dash plugins"));
303+ filter.add_option ("system", _("System"));
304+
305+ filters.add (filter);
306+ }
307+ this.filter_set = filters;
308+
309+ /* get all the remote scopes */
310+ var shared_model = new Dee.SharedModel (SCOPES_MODEL);
311+ shared_model.set_schema ("s", "s", "s", "s", "s", "as");
312+ shared_model.end_transaction.connect (this.remote_scopes_changed);
313+ remote_scopes_model = shared_model;
314+
315+ remote_scopes_index = Utils.prepare_index (remote_scopes_model,
316+ RemoteScopesColumn.NAME,
317+ (model, iter) =>
318+ {
319+ unowned string name = model.get_string (iter, RemoteScopesColumn.NAME);
320+ return "%s\n%s".printf (_("scope"), name);
321+ }, null);
322+
323+ local_scopes_model = new Dee.SequenceModel ();
324+ // use similar schema as remote_scopes_model, so we can share code
325+ local_scopes_model.set_schema ("s", "s", "s", "s", "s");
326+
327+ local_scopes_index = Utils.prepare_index (local_scopes_model,
328+ RemoteScopesColumn.NAME,
329+ (model, iter) =>
330+ {
331+ unowned string name = model.get_string (iter, RemoteScopesColumn.NAME);
332+ return "%s\n%s".printf (_("scope"), name);
333+ }, null);
334+
335+ /* monitor scopes dconf keys */
336+ disabled_scope_ids = new HashTable<unowned string, unowned string> (str_hash, str_equal);
337+ var pref_man = PreferencesManager.get_default ();
338+ pref_man.notify["disabled-scopes"].connect (update_disabled_scopes_hash);
339+ update_disabled_scopes_hash ();
340+
341+ locked_scope_ids = new HashTable<string, bool> (str_hash, str_equal);
342+ var settings = new Settings (LIBUNITY_SCHEMA);
343+ foreach (unowned string scope_id in settings.get_strv ("locked-scopes"))
344+ {
345+ locked_scope_ids[scope_id] = true;
346+ }
347+
348+ apps_model = new Dee.SequenceModel ();
349+ // uri, app title, description, screenshot?
350+ apps_model.set_schema ("s", "s", "s", "s", "s");
351+ apps_model.set_column_names ("uri", "name", "icon", "description", "screenshot");
352+
353+ apps_index = Utils.prepare_index (apps_model, AppsColumn.NAME, (model, iter) =>
354+ {
355+ return model.get_string (iter, AppsColumn.NAME);
356+ }, null);
357+
358+ build_scope_index.begin ();
359+ build_apps_data ();
360+
361+ /* set the search functions */
362+ set_search_async_func (this.dispatch_search);
363+ set_preview_async_func (this.dispatch_preview);
364+ set_activate_func (this.activate);
365+ }
366+
367+ private void update_disabled_scopes_hash ()
368+ {
369+ disabled_scope_ids.remove_all ();
370+ var pref_man = PreferencesManager.get_default ();
371+ foreach (unowned string scope_id in pref_man.disabled_scopes)
372+ {
373+ // using HashTable as a set (optimized in glib when done like this)
374+ disabled_scope_ids[scope_id] = scope_id;
375+ }
376+ }
377+
378+ private async void build_scope_index ()
379+ {
380+ try
381+ {
382+ var registry = yield Unity.Protocol.ScopeRegistry.find_scopes (null);
383+ var flattened = new SList<Protocol.ScopeRegistry.ScopeMetadata> ();
384+ foreach (var node in registry.scopes)
385+ {
386+ flattened.prepend (node.scope_info);
387+ foreach (var subscope_info in node.sub_scopes)
388+ flattened.prepend (subscope_info);
389+ }
390+ flattened.reverse ();
391+ foreach (var info in flattened)
392+ {
393+ if (info.is_master) continue;
394+ if (info.id in invisible_scope_ids) continue;
395+
396+ local_scopes_model.append (info.id, info.name, info.description,
397+ info.icon, "");
398+ }
399+ }
400+ catch (Error err)
401+ {
402+ warning ("Unable to find scopes: %s", err.message);
403+ }
404+ scopes_loaded = true;
405+ }
406+
407+ private void build_apps_data ()
408+ {
409+ var keyfile = new KeyFile ();
410+ foreach (unowned string desktop_file_id in NON_CLICK_DESKTOPS)
411+ {
412+ bool loaded = false;
413+ try
414+ {
415+ loaded = keyfile.load_from_dirs ("applications/" + desktop_file_id,
416+ Environment.get_system_data_dirs (),
417+ null,
418+ KeyFileFlags.KEEP_TRANSLATIONS);
419+ }
420+ catch (Error err)
421+ {
422+ loaded = false;
423+ }
424+ if (!loaded)
425+ {
426+ continue;
427+ }
428+ var app_info = new DesktopAppInfo.from_keyfile (keyfile);
429+ if (app_info == null) continue;
430+
431+ string? screenshot = null;
432+ bool is_touch_app = false;
433+ try
434+ {
435+ is_touch_app = keyfile.get_boolean (KeyFileDesktop.GROUP, "X-Ubuntu-Touch");
436+ screenshot = keyfile.get_string (KeyFileDesktop.GROUP, "X-Screenshot");
437+ }
438+ catch (Error err)
439+ {
440+ // ignore
441+ }
442+
443+ if (!is_touch_app) continue;
444+
445+ apps_model.append ("application://" + desktop_file_id,
446+ app_info.get_display_name (),
447+ app_info.get_icon ().to_string (),
448+ app_info.get_description (),
449+ screenshot);
450+ }
451+ }
452+
453+ private void remote_scopes_changed ()
454+ {
455+ this.results_invalidated (SearchType.DEFAULT);
456+ }
457+
458+ private void disable_scope (string scope_id)
459+ {
460+ if (scope_id in disabled_scope_ids) return;
461+
462+ var pref_man = PreferencesManager.get_default ();
463+ var disabled_scopes = pref_man.disabled_scopes;
464+ disabled_scopes += scope_id;
465+
466+ var settings = new Settings (LIBUNITY_SCHEMA);
467+ settings.set_strv ("disabled-scopes", disabled_scopes);
468+ }
469+
470+ private void enable_scope (string scope_id)
471+ {
472+ if (!(scope_id in disabled_scope_ids)) return;
473+
474+ var pref_man = PreferencesManager.get_default ();
475+ string[] disabled_scopes = {};
476+ foreach (unowned string disabled_scope_id in pref_man.disabled_scopes)
477+ {
478+ if (disabled_scope_id != scope_id)
479+ disabled_scopes += disabled_scope_id;
480+ }
481+
482+ var settings = new Settings (LIBUNITY_SCHEMA);
483+ settings.set_strv ("disabled-scopes", disabled_scopes);
484+ }
485+
486+ private string get_scope_icon (string icon_hint, bool is_disabled)
487+ {
488+ try
489+ {
490+ Icon base_icon = icon_hint == null || icon_hint == "" ?
491+ Icon.new_for_string (GENERIC_SCOPE_ICON) :
492+ Icon.new_for_string (icon_hint);
493+ var anno_icon = new AnnotatedIcon (base_icon);
494+ anno_icon.size_hint = IconSizeHint.SMALL;
495+ // dim disabled icons by decreasing their alpha
496+ if (is_disabled)
497+ anno_icon.set_colorize_rgba (1.0, 1.0, 1.0, 0.5);
498+ return anno_icon.to_string ();
499+ }
500+ catch (Error err)
501+ {
502+ return "";
503+ }
504+ }
505+
506+ private void add_apps_results (string query, ResultSet result_set)
507+ {
508+ var model = apps_index.model;
509+ var analyzer = apps_index.analyzer;
510+ var results = Utils.search_index (apps_index, analyzer, query);
511+ foreach (var iter in results)
512+ {
513+ unowned string app_id = model.get_string (iter, AppsColumn.APP_URI);
514+ unowned string icon_hint = model.get_string (iter, AppsColumn.ICON_HINT);
515+ unowned string name = model.get_string (iter, AppsColumn.NAME);
516+
517+ var result = ScopeResult ();
518+ result.uri = app_id;
519+ result.category = ResultCategory.INSTALLED;
520+ result.icon_hint = icon_hint;
521+ result.result_type = Unity.ResultType.PERSONAL;
522+ result.mimetype = "application/x-desktop";
523+ result.title = name;
524+ result.comment = "";
525+ result.dnd_uri = app_id;
526+
527+ result_set.add_result (result);
528+ }
529+ }
530+
531+ /* Models backing the index need to have similar enough schema! */
532+ private void add_scope_results (Dee.Index index, string query,
533+ ResultSet result_set)
534+ {
535+ var model = index.model;
536+ var analyzer = index.analyzer;
537+ var results = Utils.search_index (index, analyzer, query);
538+ foreach (var iter in results)
539+ {
540+ unowned string scope_id =
541+ model.get_string (iter, RemoteScopesColumn.SCOPE_ID);
542+ unowned string icon_hint =
543+ model.get_string (iter, RemoteScopesColumn.ICON_HINT);
544+ bool is_disabled = scope_id in disabled_scope_ids;
545+
546+ var result = Unity.ScopeResult ();
547+ result.uri = @"scope://$(scope_id)";
548+ result.category = ResultCategory.SCOPES;
549+ result.icon_hint = get_scope_icon (icon_hint, is_disabled);
550+ result.result_type = Unity.ResultType.DEFAULT;
551+ result.mimetype = "application/x-unity-scope";
552+ result.title = model.get_string (iter, RemoteScopesColumn.NAME);
553+ result.comment = "";
554+ result.dnd_uri = result.uri;
555+ result.metadata = new HashTable<string, Variant> (str_hash, str_equal);
556+ result.metadata["scope_disabled"] = new Variant.uint32 (is_disabled ? 1 : 0);
557+
558+ result_set.add_result (result);
559+ }
560+
561+ }
562+
563+ private void dispatch_search (ScopeSearchBase search, ScopeSearchBaseCallback cb)
564+ {
565+ perform_search.begin (search, (obj, res) =>
566+ {
567+ perform_search.end (res);
568+ cb (search);
569+ });
570+ }
571+
572+ private void dispatch_preview (ResultPreviewer previewer, AbstractPreviewCallback cb)
573+ {
574+ perform_preview.begin (previewer, (obj, res) =>
575+ {
576+ var preview = perform_preview.end (res);
577+ cb (previewer, preview);
578+ });
579+ }
580+
581+ private async void perform_search (ScopeSearchBase search)
582+ {
583+ // are we properly initialized?
584+ while (!scopes_loaded)
585+ {
586+ Timeout.add (100, perform_search.callback);
587+ yield;
588+ }
589+ var search_type = search.search_context.search_type;
590+ bool include_scope_results = true;
591+ if (search.search_context.filter_state != null)
592+ {
593+ var filter = search.search_context.filter_state.get_filter_by_id ("type");
594+ if (filter != null && filter.filtering)
595+ {
596+ var of = filter as OptionsFilter;
597+ include_scope_results = of.get_option ("scopes").active;
598+ }
599+ }
600+
601+ add_apps_results (search.search_context.search_query,
602+ search.search_context.result_set);
603+
604+ if (include_scope_results && search_type == SearchType.DEFAULT)
605+ {
606+ add_scope_results (local_scopes_index, search.search_context.search_query,
607+ search.search_context.result_set);
608+ add_scope_results (remote_scopes_index, search.search_context.search_query,
609+ search.search_context.result_set);
610+ }
611+ }
612+
613+ private async Preview? perform_preview (ResultPreviewer previewer)
614+ {
615+ unowned string uri = previewer.result.uri;
616+ if (uri.has_prefix ("scope://"))
617+ {
618+ // linear search, "yey"!
619+ string scope_id = uri.substring (8);
620+ var model = local_scopes_model;
621+ var iter = model.get_first_iter ();
622+ var end_iter = model.get_last_iter ();
623+ var found_iter = end_iter;
624+ while (iter != end_iter)
625+ {
626+ if (model.get_string (iter, RemoteScopesColumn.SCOPE_ID) == scope_id)
627+ {
628+ found_iter = iter;
629+ break;
630+ }
631+ iter = model.next (iter);
632+ }
633+
634+ if (found_iter == end_iter)
635+ {
636+ // try in the remote_scopes_model
637+ model = remote_scopes_model;
638+ iter = model.get_first_iter ();
639+ end_iter = model.get_last_iter ();
640+ while (iter != end_iter)
641+ {
642+ if (model.get_string (iter, RemoteScopesColumn.SCOPE_ID) == scope_id)
643+ {
644+ found_iter = iter;
645+ break;
646+ }
647+ iter = model.next (iter);
648+ }
649+ }
650+
651+ if (found_iter == end_iter) return null;
652+
653+ unowned string name = model.get_string (found_iter, RemoteScopesColumn.NAME);
654+ unowned string description = model.get_string (found_iter, RemoteScopesColumn.DESCRIPTION);
655+ unowned string icon = model.get_string (found_iter, RemoteScopesColumn.ICON_HINT);
656+ unowned string screenshot = model.get_string (found_iter, RemoteScopesColumn.SCREENSHOT_URL);
657+ Icon? app_icon = null;
658+ Icon? screenshot_icon = null;
659+ try
660+ {
661+ if (icon != "") app_icon = Icon.new_for_string (icon);
662+ if (screenshot != "") screenshot_icon = Icon.new_for_string (screenshot);
663+ }
664+ catch (Error err) {}
665+
666+ var preview = new ApplicationPreview (name, "", description,
667+ app_icon, screenshot_icon);
668+ PreviewAction action;
669+ bool scope_disabled = scope_id in disabled_scope_ids;
670+ if (scope_disabled)
671+ {
672+ action = new Unity.PreviewAction ("enable-scope", _("Enable"), null);
673+ }
674+ else
675+ {
676+ action = new Unity.PreviewAction ("disable-scope", _("Disable"), null);
677+ }
678+ if (!(scope_id in locked_scope_ids))
679+ {
680+ preview.add_action (action);
681+ }
682+ return preview;
683+ }
684+ else if (uri.has_prefix ("application:"))
685+ {
686+ // linear search, "yey"!
687+ var iter = apps_model.get_first_iter ();
688+ var end_iter = apps_model.get_last_iter ();
689+ var found_iter = end_iter;
690+ while (iter != end_iter)
691+ {
692+ if (apps_model.get_string (iter, AppsColumn.APP_URI) == uri)
693+ {
694+ found_iter = iter;
695+ break;
696+ }
697+ iter = apps_model.next (iter);
698+ }
699+ if (found_iter == end_iter) return null; // uh oh
700+
701+ unowned string name = apps_model.get_string (found_iter, AppsColumn.NAME);
702+ unowned string description = apps_model.get_string (found_iter, AppsColumn.DESCRIPTION);
703+ unowned string icon = apps_model.get_string (found_iter, AppsColumn.ICON_HINT);
704+ unowned string screenshot = apps_model.get_string (found_iter, AppsColumn.SCREENSHOT_URL);
705+ Icon? app_icon = null;
706+ Icon? screenshot_icon = null;
707+ try
708+ {
709+ if (icon != "") app_icon = Icon.new_for_string (icon);
710+ if (screenshot != "") screenshot_icon = Icon.new_for_string (screenshot);
711+ }
712+ catch (Error err) {}
713+ var preview = new ApplicationPreview (name, "", description,
714+ app_icon, screenshot_icon);
715+ var launch_action = new Unity.PreviewAction ("launch", _("Launch"), null);
716+ preview.add_action (launch_action);
717+ return preview;
718+ }
719+ return null;
720+ }
721+
722+ private ActivationResponse? activate (ScopeResult result,
723+ SearchMetadata metadata,
724+ string? action_id)
725+ {
726+ if (result.uri.has_prefix ("scope://"))
727+ {
728+ var scope_id = result.uri.substring (8);
729+ if (action_id == "enable-scope")
730+ {
731+ enable_scope (scope_id);
732+ }
733+ else if (action_id == "disable-scope")
734+ {
735+ disable_scope (scope_id);
736+ }
737+ return new Unity.ActivationResponse (HandledType.SHOW_PREVIEW, result.uri);
738+ }
739+
740+ return null;
741+ }
742+}
743+
744
745=== added file 'src/utils.vala'
746--- src/utils.vala 1970-01-01 00:00:00 +0000
747+++ src/utils.vala 2013-10-07 16:27:29 +0000
748@@ -0,0 +1,116 @@
749+/*
750+ * Copyright (C) 2010-2013 Canonical Ltd
751+ *
752+ * This program is free software: you can redistribute it and/or modify
753+ * it under the terms of the GNU General Public License version 3 as
754+ * published by the Free Software Foundation.
755+ *
756+ * This program is distributed in the hope that it will be useful,
757+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
758+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
759+ * GNU General Public License for more details.
760+ *
761+ * You should have received a copy of the GNU General Public License
762+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
763+ *
764+ * Authored by Michal Hruby <michal.hruby@canonical.com>
765+ *
766+ */
767+
768+namespace Utils
769+{
770+ private static Dee.ICUTermFilter icu_filter;
771+
772+ public Dee.Index prepare_index (Dee.Model model,
773+ uint sort_column,
774+ owned Dee.ModelReaderFunc reader_func,
775+ out Dee.Analyzer out_analyzer)
776+ {
777+ // reuse the icu_filter
778+ if (icu_filter == null)
779+ {
780+ icu_filter = new Dee.ICUTermFilter.ascii_folder ();
781+ }
782+
783+ var sort_filter = Dee.Filter.new_collator (sort_column);
784+ var filter_model = new Dee.FilterModel (model, sort_filter);
785+
786+ var analyzer = new Dee.TextAnalyzer ();
787+ analyzer.add_term_filter ((terms_in, terms_out) =>
788+ {
789+ for (uint i = 0; i < terms_in.num_terms (); i++)
790+ {
791+ unowned string term = terms_in.get_term (i);
792+ var folded = icu_filter.apply (term);
793+ terms_out.add_term (term);
794+ if (folded != term) terms_out.add_term (folded);
795+ }
796+ });
797+ out_analyzer = analyzer;
798+
799+ var reader = Dee.ModelReader.new ((owned) reader_func);
800+ return new Dee.TreeIndex (filter_model, analyzer, reader);
801+ }
802+
803+ public SList<Dee.ModelIter> search_index (Dee.Index index,
804+ Dee.Analyzer analyzer,
805+ string query)
806+ {
807+ if (query.strip ().length == 0)
808+ {
809+ var model = index.get_model ();
810+ var iter = model.get_first_iter ();
811+ var end_iter = model.get_last_iter ();
812+
813+ var result = new SList<Dee.ModelIter> ();
814+ while (iter != end_iter)
815+ {
816+ result.prepend (iter);
817+ iter = model.next (iter);
818+ }
819+ result.reverse ();
820+ return result;
821+ }
822+
823+ var term_list = Object.new (typeof (Dee.TermList)) as Dee.TermList;
824+ analyzer.tokenize (query, term_list);
825+ var matches = new Sequence<Dee.ModelIter> ();
826+
827+ uint num_terms = term_list.num_terms ();
828+ for (uint i = 0; i < num_terms; i++)
829+ {
830+ var rs = index.lookup (term_list.get_term (i),
831+ i < num_terms - 1 ? Dee.TermMatchFlag.EXACT : Dee.TermMatchFlag.PREFIX);
832+
833+ bool first_pass = i == 0;
834+ CompareDataFunc<Dee.ModelIter> cmp_func = (a, b) =>
835+ {
836+ return a == b ? 0 : ((void*) a > (void*) b ? 1 : -1);
837+ };
838+ // intersect the results (cause we want to AND the terms)
839+ var remaining = new Sequence<Dee.ModelIter> ();
840+ foreach (var item in rs)
841+ {
842+ if (first_pass)
843+ matches.insert_sorted (item, cmp_func);
844+ else if (matches.lookup (item, cmp_func) != null)
845+ remaining.insert_sorted (item, cmp_func);
846+ }
847+ if (!first_pass) matches = (owned) remaining;
848+ // final result set empty already?
849+ if (matches.get_begin_iter () == matches.get_end_iter ()) break;
850+ }
851+
852+ var result = new SList<Dee.ModelIter> ();
853+ var iter = matches.get_begin_iter ();
854+ var end_iter = matches.get_end_iter ();
855+ while (iter != end_iter)
856+ {
857+ result.prepend (iter.get ());
858+ iter = iter.next ();
859+ }
860+
861+ result.reverse ();
862+ return result;
863+ }
864+}

Subscribers

People subscribed via source and target branches

to all changes: