2
* Copyright (C) 2013 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/>.
19
class NonClickScope: Unity.SimpleScope
21
/* List of app IDs that aren't proper click packages yet */
22
const string[] NON_CLICK_DESKTOPS =
24
"address-book-app.desktop",
25
"calendar-app.desktop",
27
"click-update-manager.desktop",
29
"friends-app.desktop",
30
"gallery-app.desktop",
31
"mediaplayer-app.desktop",
32
"messaging-app.desktop",
35
"rssreader-app.desktop",
36
"ubuntu-calculator-app.desktop",
37
"ubuntu-clock-app.desktop",
38
"ubuntu-filemanager-app.desktop",
39
"ubuntu-system-settings.desktop",
40
"ubuntu-terminal-app.desktop",
41
"ubuntu-weather-app.desktop",
42
"webbrowser-app.desktop"
45
const string[] invisible_scope_ids =
48
"applications-scopes.scope",
49
"applications-non-click.scope"
52
const string LIBUNITY_SCHEMA = "com.canonical.Unity.Lenses";
53
const string ICON_PATH = Config.DATADIR + "/icons/unity-icon-theme/places/svg/";
54
const string GENERIC_SCOPE_ICON = ICON_PATH + "service-generic.svg";
55
const string SCOPES_MODEL = "com.canonical.Unity.SmartScopes.RemoteScopesModel";
57
private enum RemoteScopesColumn
67
private enum AppsColumn
76
private enum ResultCategory
82
private Dee.Model remote_scopes_model;
83
private Dee.Model local_scopes_model;
84
private Dee.Index remote_scopes_index;
85
private Dee.Index local_scopes_index;
86
private Dee.Model apps_model;
87
private Dee.Index apps_index;
89
private bool scopes_loaded = false;
91
private HashTable<unowned string, unowned string> disabled_scope_ids;
92
private HashTable<string, bool> locked_scope_ids;
94
public NonClickScope ()
101
this.group_name = "com.canonical.Unity.Scope.Applications.Click";
102
this.unique_name = "/com/canonical/unity/scope/applications/non_click";
105
var schema = new Schema ();
106
schema.add_field ("scope_disabled", "u", Schema.FieldType.OPTIONAL);
107
this.schema = schema;
110
var icon_dir = File.new_for_path (ICON_PATH);
113
var categories = new CategorySet ();
114
cat = new Unity.Category ("installed", _("Installed"),
115
new FileIcon (icon_dir.get_child ("group-installed.svg")));
116
categories.add (cat);
118
cat = new Unity.Category ("scopes", _("Dash plugins"),
119
new FileIcon (icon_dir.get_child ("group-installed.svg")));
120
categories.add (cat);
121
this.category_set = categories;
124
var filters = new FilterSet ();
126
var filter = new CheckOptionFilter ("type", _("Type"));
127
filter.sort_type = Unity.OptionsFilter.SortType.DISPLAY_NAME;
129
filter.add_option ("accessories", _("Accessories"));
130
filter.add_option ("education", _("Education"));
131
filter.add_option ("game", _("Games"));
132
filter.add_option ("graphics", _("Graphics"));
133
filter.add_option ("internet", _("Internet"));
134
filter.add_option ("fonts", _("Fonts"));
135
filter.add_option ("office", _("Office"));
136
filter.add_option ("media", _("Media"));
137
filter.add_option ("customization", _("Customization"));
138
filter.add_option ("accessibility", _("Accessibility"));
139
filter.add_option ("developer", _("Developer"));
140
filter.add_option ("science-and-engineering", _("Science & engineering"));
141
filter.add_option ("scopes", _("Dash plugins"));
142
filter.add_option ("system", _("System"));
144
filters.add (filter);
146
this.filter_set = filters;
148
/* get all the remote scopes */
149
var shared_model = new Dee.SharedModel (SCOPES_MODEL);
150
shared_model.set_schema ("s", "s", "s", "s", "s", "as");
151
shared_model.end_transaction.connect (this.remote_scopes_changed);
152
remote_scopes_model = shared_model;
154
remote_scopes_index = Utils.prepare_index (remote_scopes_model,
155
RemoteScopesColumn.NAME,
158
unowned string name = model.get_string (iter, RemoteScopesColumn.NAME);
159
return "%s\n%s".printf (_("scope"), name);
162
local_scopes_model = new Dee.SequenceModel ();
163
// use similar schema as remote_scopes_model, so we can share code
164
local_scopes_model.set_schema ("s", "s", "s", "s", "s");
166
local_scopes_index = Utils.prepare_index (local_scopes_model,
167
RemoteScopesColumn.NAME,
170
unowned string name = model.get_string (iter, RemoteScopesColumn.NAME);
171
return "%s\n%s".printf (_("scope"), name);
174
/* monitor scopes dconf keys */
175
disabled_scope_ids = new HashTable<unowned string, unowned string> (str_hash, str_equal);
176
var pref_man = PreferencesManager.get_default ();
177
pref_man.notify["disabled-scopes"].connect (update_disabled_scopes_hash);
178
update_disabled_scopes_hash ();
180
locked_scope_ids = new HashTable<string, bool> (str_hash, str_equal);
181
var settings = new Settings (LIBUNITY_SCHEMA);
182
foreach (unowned string scope_id in settings.get_strv ("locked-scopes"))
184
locked_scope_ids[scope_id] = true;
187
apps_model = new Dee.SequenceModel ();
188
// uri, app title, description, screenshot?
189
apps_model.set_schema ("s", "s", "s", "s", "s");
190
apps_model.set_column_names ("uri", "name", "icon", "description", "screenshot");
192
apps_index = Utils.prepare_index (apps_model, AppsColumn.NAME, (model, iter) =>
194
return model.get_string (iter, AppsColumn.NAME);
197
build_scope_index.begin ();
200
/* set the search functions */
201
set_search_async_func (this.dispatch_search);
202
set_preview_async_func (this.dispatch_preview);
203
set_activate_func (this.activate);
206
private void update_disabled_scopes_hash ()
208
disabled_scope_ids.remove_all ();
209
var pref_man = PreferencesManager.get_default ();
210
foreach (unowned string scope_id in pref_man.disabled_scopes)
212
// using HashTable as a set (optimized in glib when done like this)
213
disabled_scope_ids[scope_id] = scope_id;
217
private async void build_scope_index ()
221
var registry = yield Unity.Protocol.ScopeRegistry.find_scopes (null);
222
var flattened = new SList<Protocol.ScopeRegistry.ScopeMetadata> ();
223
foreach (var node in registry.scopes)
225
flattened.prepend (node.scope_info);
226
foreach (var subscope_info in node.sub_scopes)
227
flattened.prepend (subscope_info);
229
flattened.reverse ();
230
foreach (var info in flattened)
232
if (info.is_master) continue;
233
if (info.id in invisible_scope_ids) continue;
235
local_scopes_model.append (info.id, info.name, info.description,
241
warning ("Unable to find scopes: %s", err.message);
243
scopes_loaded = true;
246
private void build_apps_data ()
248
var keyfile = new KeyFile ();
249
foreach (unowned string desktop_file_id in NON_CLICK_DESKTOPS)
254
loaded = keyfile.load_from_dirs ("applications/" + desktop_file_id,
255
Environment.get_system_data_dirs (),
257
KeyFileFlags.KEEP_TRANSLATIONS);
267
var app_info = new DesktopAppInfo.from_keyfile (keyfile);
268
if (app_info == null) continue;
270
string? screenshot = null;
271
bool is_touch_app = false;
274
is_touch_app = keyfile.get_boolean (KeyFileDesktop.GROUP, "X-Ubuntu-Touch");
275
screenshot = keyfile.get_string (KeyFileDesktop.GROUP, "X-Screenshot");
282
if (!is_touch_app) continue;
284
apps_model.append ("application://" + desktop_file_id,
285
app_info.get_display_name (),
286
app_info.get_icon ().to_string (),
287
app_info.get_description (),
292
private void remote_scopes_changed ()
294
this.results_invalidated (SearchType.DEFAULT);
297
private void disable_scope (string scope_id)
299
if (scope_id in disabled_scope_ids) return;
301
var pref_man = PreferencesManager.get_default ();
302
var disabled_scopes = pref_man.disabled_scopes;
303
disabled_scopes += scope_id;
305
var settings = new Settings (LIBUNITY_SCHEMA);
306
settings.set_strv ("disabled-scopes", disabled_scopes);
309
private void enable_scope (string scope_id)
311
if (!(scope_id in disabled_scope_ids)) return;
313
var pref_man = PreferencesManager.get_default ();
314
string[] disabled_scopes = {};
315
foreach (unowned string disabled_scope_id in pref_man.disabled_scopes)
317
if (disabled_scope_id != scope_id)
318
disabled_scopes += disabled_scope_id;
321
var settings = new Settings (LIBUNITY_SCHEMA);
322
settings.set_strv ("disabled-scopes", disabled_scopes);
325
private string get_scope_icon (string icon_hint, bool is_disabled)
329
Icon base_icon = icon_hint == null || icon_hint == "" ?
330
Icon.new_for_string (GENERIC_SCOPE_ICON) :
331
Icon.new_for_string (icon_hint);
332
var anno_icon = new AnnotatedIcon (base_icon);
333
anno_icon.size_hint = IconSizeHint.SMALL;
334
// dim disabled icons by decreasing their alpha
336
anno_icon.set_colorize_rgba (1.0, 1.0, 1.0, 0.5);
337
return anno_icon.to_string ();
345
private void add_apps_results (string query, ResultSet result_set)
347
var model = apps_index.model;
348
var analyzer = apps_index.analyzer;
349
var results = Utils.search_index (apps_index, analyzer, query);
350
foreach (var iter in results)
352
unowned string app_id = model.get_string (iter, AppsColumn.APP_URI);
353
unowned string icon_hint = model.get_string (iter, AppsColumn.ICON_HINT);
354
unowned string name = model.get_string (iter, AppsColumn.NAME);
356
var result = ScopeResult ();
358
result.category = ResultCategory.INSTALLED;
359
result.icon_hint = icon_hint;
360
result.result_type = Unity.ResultType.PERSONAL;
361
result.mimetype = "application/x-desktop";
364
result.dnd_uri = app_id;
366
result_set.add_result (result);
370
/* Models backing the index need to have similar enough schema! */
371
private void add_scope_results (Dee.Index index, string query,
372
ResultSet result_set)
374
var model = index.model;
375
var analyzer = index.analyzer;
376
var results = Utils.search_index (index, analyzer, query);
377
foreach (var iter in results)
379
unowned string scope_id =
380
model.get_string (iter, RemoteScopesColumn.SCOPE_ID);
381
unowned string icon_hint =
382
model.get_string (iter, RemoteScopesColumn.ICON_HINT);
383
bool is_disabled = scope_id in disabled_scope_ids;
385
var result = Unity.ScopeResult ();
386
result.uri = @"scope://$(scope_id)";
387
result.category = ResultCategory.SCOPES;
388
result.icon_hint = get_scope_icon (icon_hint, is_disabled);
389
result.result_type = Unity.ResultType.DEFAULT;
390
result.mimetype = "application/x-unity-scope";
391
result.title = model.get_string (iter, RemoteScopesColumn.NAME);
393
result.dnd_uri = result.uri;
394
result.metadata = new HashTable<string, Variant> (str_hash, str_equal);
395
result.metadata["scope_disabled"] = new Variant.uint32 (is_disabled ? 1 : 0);
397
result_set.add_result (result);
402
private void dispatch_search (ScopeSearchBase search, ScopeSearchBaseCallback cb)
404
perform_search.begin (search, (obj, res) =>
406
perform_search.end (res);
411
private void dispatch_preview (ResultPreviewer previewer, AbstractPreviewCallback cb)
413
perform_preview.begin (previewer, (obj, res) =>
415
var preview = perform_preview.end (res);
416
cb (previewer, preview);
420
private async void perform_search (ScopeSearchBase search)
422
// are we properly initialized?
423
while (!scopes_loaded)
425
Timeout.add (100, perform_search.callback);
428
var search_type = search.search_context.search_type;
429
bool include_scope_results = true;
430
if (search.search_context.filter_state != null)
432
var filter = search.search_context.filter_state.get_filter_by_id ("type");
433
if (filter != null && filter.filtering)
435
var of = filter as OptionsFilter;
436
include_scope_results = of.get_option ("scopes").active;
440
add_apps_results (search.search_context.search_query,
441
search.search_context.result_set);
443
if (include_scope_results && search_type == SearchType.DEFAULT)
445
add_scope_results (local_scopes_index, search.search_context.search_query,
446
search.search_context.result_set);
447
add_scope_results (remote_scopes_index, search.search_context.search_query,
448
search.search_context.result_set);
452
private async Preview? perform_preview (ResultPreviewer previewer)
454
unowned string uri = previewer.result.uri;
455
if (uri.has_prefix ("scope://"))
457
// linear search, "yey"!
458
string scope_id = uri.substring (8);
459
var model = local_scopes_model;
460
var iter = model.get_first_iter ();
461
var end_iter = model.get_last_iter ();
462
var found_iter = end_iter;
463
while (iter != end_iter)
465
if (model.get_string (iter, RemoteScopesColumn.SCOPE_ID) == scope_id)
470
iter = model.next (iter);
473
if (found_iter == end_iter)
475
// try in the remote_scopes_model
476
model = remote_scopes_model;
477
iter = model.get_first_iter ();
478
end_iter = model.get_last_iter ();
479
while (iter != end_iter)
481
if (model.get_string (iter, RemoteScopesColumn.SCOPE_ID) == scope_id)
486
iter = model.next (iter);
490
if (found_iter == end_iter) return null;
492
unowned string name = model.get_string (found_iter, RemoteScopesColumn.NAME);
493
unowned string description = model.get_string (found_iter, RemoteScopesColumn.DESCRIPTION);
494
unowned string icon = model.get_string (found_iter, RemoteScopesColumn.ICON_HINT);
495
unowned string screenshot = model.get_string (found_iter, RemoteScopesColumn.SCREENSHOT_URL);
496
Icon? app_icon = null;
497
Icon? screenshot_icon = null;
500
if (icon != "") app_icon = Icon.new_for_string (icon);
501
if (screenshot != "") screenshot_icon = Icon.new_for_string (screenshot);
505
var preview = new ApplicationPreview (name, "", description,
506
app_icon, screenshot_icon);
507
PreviewAction action;
508
bool scope_disabled = scope_id in disabled_scope_ids;
511
action = new Unity.PreviewAction ("enable-scope", _("Enable"), null);
515
action = new Unity.PreviewAction ("disable-scope", _("Disable"), null);
517
if (!(scope_id in locked_scope_ids))
519
preview.add_action (action);
523
else if (uri.has_prefix ("application:"))
525
// linear search, "yey"!
526
var iter = apps_model.get_first_iter ();
527
var end_iter = apps_model.get_last_iter ();
528
var found_iter = end_iter;
529
while (iter != end_iter)
531
if (apps_model.get_string (iter, AppsColumn.APP_URI) == uri)
536
iter = apps_model.next (iter);
538
if (found_iter == end_iter) return null; // uh oh
540
unowned string name = apps_model.get_string (found_iter, AppsColumn.NAME);
541
unowned string description = apps_model.get_string (found_iter, AppsColumn.DESCRIPTION);
542
unowned string icon = apps_model.get_string (found_iter, AppsColumn.ICON_HINT);
543
unowned string screenshot = apps_model.get_string (found_iter, AppsColumn.SCREENSHOT_URL);
544
Icon? app_icon = null;
545
Icon? screenshot_icon = null;
548
if (icon != "") app_icon = Icon.new_for_string (icon);
549
if (screenshot != "") screenshot_icon = Icon.new_for_string (screenshot);
552
var preview = new ApplicationPreview (name, "", description,
553
app_icon, screenshot_icon);
554
var launch_action = new Unity.PreviewAction ("launch", _("Launch"), null);
555
preview.add_action (launch_action);
561
private ActivationResponse? activate (ScopeResult result,
562
SearchMetadata metadata,
565
if (result.uri.has_prefix ("scope://"))
567
var scope_id = result.uri.substring (8);
568
if (action_id == "enable-scope")
570
enable_scope (scope_id);
572
else if (action_id == "disable-scope")
574
disable_scope (scope_id);
576
return new Unity.ActivationResponse (HandledType.SHOW_PREVIEW, result.uri);