~dobey/unity-scope-click/finite-search

« back to all changes in this revision

Viewing changes to src/non-click-scope.vala

  • Committer: Rodney Dawes
  • Date: 2013-10-08 20:25:26 UTC
  • mfrom: (60.2.12 trunk)
  • Revision ID: rodney.dawes@canonical.com-20131008202526-zxhoxi6nh3xmsijk
[ Alejandro J. Cura ]
* Pass the publisher field if available to the app preview. (LP:
  #1226265)
* Include the framework in the search query. (LP: #1234255)
* Catch more download errors, and ask for credentials on 401. (LP:
  #1234742)
[ Ubuntu daily release ]
* Automatic snapshot from revision 70
[ Alejandro J. Cura ]
* Do not show suggested apps when internet search is off. (LP:
  #1231378)
* Don't show Uninstall button if manifest has _removable:0, and remove
  pin-to-launcher. (LP: #1233641, #1233653)
* Show download manager failures. (LP: #1226021)
* Ask for confirmation before uninstall . (LP: #1233643)
[ Rodney Dawes ]
* Remove the fake ratings and reviews data from the app previews. (LP:
  #1209213)
* Remove the noauth=1 appendage to the URL.
* Refactor the error handling, to separate credentials errors from
  other errors. Show a "Go to Accounts" button in the credentials
  error preview. (LP: #1231517)
* Don't add the Install button, if there was an error. (LP: #1233169)
[ Ubuntu daily release ]
* Automatic snapshot from revision 66

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/*
 
2
 * Copyright (C) 2013 Canonical, Ltd.
 
3
 *
 
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.
 
7
 *
 
8
 * This program is distributed in the hope that it will be useful,
 
9
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
 * GNU General Public License for more details.
 
12
 *
 
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/>.
 
15
 */
 
16
 
 
17
using Unity;
 
18
 
 
19
class NonClickScope: Unity.SimpleScope
 
20
{
 
21
  /* List of app IDs that aren't proper click packages yet */
 
22
  const string[] NON_CLICK_DESKTOPS =
 
23
  {
 
24
    "address-book-app.desktop",
 
25
    "calendar-app.desktop",
 
26
    "camera-app.desktop",
 
27
    "click-update-manager.desktop",
 
28
    "dialer-app.desktop",
 
29
    "friends-app.desktop",
 
30
    "gallery-app.desktop",
 
31
    "mediaplayer-app.desktop",
 
32
    "messaging-app.desktop",
 
33
    "music-app.desktop",
 
34
    "notes-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"
 
43
  };
 
44
 
 
45
  const string[] invisible_scope_ids =
 
46
  {
 
47
    "home.scope",
 
48
    "applications-scopes.scope",
 
49
    "applications-non-click.scope"
 
50
  };
 
51
 
 
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";
 
56
 
 
57
  private enum RemoteScopesColumn
 
58
  {
 
59
    SCOPE_ID,
 
60
    NAME,
 
61
    DESCRIPTION,
 
62
    ICON_HINT,
 
63
    SCREENSHOT_URL,
 
64
    KEYWORDS
 
65
  }
 
66
 
 
67
  private enum AppsColumn
 
68
  {
 
69
    APP_URI,
 
70
    NAME,
 
71
    ICON_HINT,
 
72
    DESCRIPTION,
 
73
    SCREENSHOT_URL
 
74
  }
 
75
 
 
76
  private enum ResultCategory
 
77
  {
 
78
    INSTALLED,
 
79
    SCOPES
 
80
  }
 
81
 
 
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;
 
88
 
 
89
  private bool scopes_loaded = false;
 
90
 
 
91
  private HashTable<unowned string, unowned string> disabled_scope_ids;
 
92
  private HashTable<string, bool> locked_scope_ids;
 
93
 
 
94
  public NonClickScope ()
 
95
  {
 
96
    Object ();
 
97
  }
 
98
 
 
99
  construct
 
100
  {
 
101
    this.group_name = "com.canonical.Unity.Scope.Applications.Click";
 
102
    this.unique_name = "/com/canonical/unity/scope/applications/non_click";
 
103
 
 
104
    /* set schema */
 
105
    var schema = new Schema ();
 
106
    schema.add_field ("scope_disabled", "u", Schema.FieldType.OPTIONAL);
 
107
    this.schema = schema;
 
108
 
 
109
    Category cat;
 
110
    var icon_dir = File.new_for_path (ICON_PATH);
 
111
 
 
112
    /* set categories */
 
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);
 
117
 
 
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;
 
122
 
 
123
    /* set filters */
 
124
    var filters = new FilterSet ();
 
125
    {
 
126
      var filter = new CheckOptionFilter ("type", _("Type"));
 
127
      filter.sort_type = Unity.OptionsFilter.SortType.DISPLAY_NAME;
 
128
 
 
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"));
 
143
 
 
144
      filters.add (filter);
 
145
    }
 
146
    this.filter_set = filters;
 
147
 
 
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;
 
153
 
 
154
    remote_scopes_index = Utils.prepare_index (remote_scopes_model,
 
155
                                               RemoteScopesColumn.NAME,
 
156
                                               (model, iter) =>
 
157
    {
 
158
      unowned string name = model.get_string (iter, RemoteScopesColumn.NAME);
 
159
      return "%s\n%s".printf (_("scope"), name);
 
160
    }, null);
 
161
 
 
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");
 
165
 
 
166
    local_scopes_index = Utils.prepare_index (local_scopes_model,
 
167
                                              RemoteScopesColumn.NAME,
 
168
                                              (model, iter) =>
 
169
    {
 
170
      unowned string name = model.get_string (iter, RemoteScopesColumn.NAME);
 
171
      return "%s\n%s".printf (_("scope"), name);
 
172
    }, null);
 
173
 
 
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 ();
 
179
 
 
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"))
 
183
    {
 
184
      locked_scope_ids[scope_id] = true;
 
185
    }
 
186
 
 
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");
 
191
 
 
192
    apps_index = Utils.prepare_index (apps_model, AppsColumn.NAME, (model, iter) =>
 
193
    {
 
194
      return model.get_string (iter, AppsColumn.NAME);
 
195
    }, null);
 
196
 
 
197
    build_scope_index.begin ();
 
198
    build_apps_data ();
 
199
 
 
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);
 
204
  }
 
205
 
 
206
  private void update_disabled_scopes_hash ()
 
207
  {
 
208
    disabled_scope_ids.remove_all ();
 
209
    var pref_man = PreferencesManager.get_default ();
 
210
    foreach (unowned string scope_id in pref_man.disabled_scopes)
 
211
    {
 
212
      // using HashTable as a set (optimized in glib when done like this)
 
213
      disabled_scope_ids[scope_id] = scope_id;
 
214
    }
 
215
  }
 
216
 
 
217
  private async void build_scope_index ()
 
218
  {
 
219
    try
 
220
    {
 
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)
 
224
      {
 
225
        flattened.prepend (node.scope_info);
 
226
        foreach (var subscope_info in node.sub_scopes)
 
227
          flattened.prepend (subscope_info);
 
228
      }
 
229
      flattened.reverse ();
 
230
      foreach (var info in flattened)
 
231
      {
 
232
        if (info.is_master) continue;
 
233
        if (info.id in invisible_scope_ids) continue;
 
234
 
 
235
        local_scopes_model.append (info.id, info.name, info.description,
 
236
                                   info.icon, "");
 
237
      }
 
238
    }
 
239
    catch (Error err)
 
240
    {
 
241
      warning ("Unable to find scopes: %s", err.message);
 
242
    }
 
243
    scopes_loaded = true;
 
244
  }
 
245
 
 
246
  private void build_apps_data ()
 
247
  {
 
248
    var keyfile = new KeyFile ();
 
249
    foreach (unowned string desktop_file_id in NON_CLICK_DESKTOPS)
 
250
    {
 
251
      bool loaded = false;
 
252
      try
 
253
      {
 
254
        loaded = keyfile.load_from_dirs ("applications/" + desktop_file_id,
 
255
                                         Environment.get_system_data_dirs (),
 
256
                                         null,
 
257
                                         KeyFileFlags.KEEP_TRANSLATIONS);
 
258
      }
 
259
      catch (Error err)
 
260
      {
 
261
        loaded = false;
 
262
      }
 
263
      if (!loaded)
 
264
      {
 
265
        continue;
 
266
      }
 
267
      var app_info = new DesktopAppInfo.from_keyfile (keyfile);
 
268
      if (app_info == null) continue;
 
269
 
 
270
      string? screenshot = null;
 
271
      bool is_touch_app = false;
 
272
      try
 
273
      {
 
274
        is_touch_app = keyfile.get_boolean (KeyFileDesktop.GROUP, "X-Ubuntu-Touch");
 
275
        screenshot = keyfile.get_string (KeyFileDesktop.GROUP, "X-Screenshot");
 
276
      }
 
277
      catch (Error err)
 
278
      {
 
279
        // ignore
 
280
      }
 
281
 
 
282
      if (!is_touch_app) continue;
 
283
 
 
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 (),
 
288
                         screenshot);
 
289
    }
 
290
  }
 
291
 
 
292
  private void remote_scopes_changed ()
 
293
  {
 
294
    this.results_invalidated (SearchType.DEFAULT);
 
295
  }
 
296
 
 
297
  private void disable_scope (string scope_id)
 
298
  {
 
299
    if (scope_id in disabled_scope_ids) return;
 
300
 
 
301
    var pref_man = PreferencesManager.get_default ();
 
302
    var disabled_scopes = pref_man.disabled_scopes;
 
303
    disabled_scopes += scope_id;
 
304
 
 
305
    var settings = new Settings (LIBUNITY_SCHEMA);
 
306
    settings.set_strv ("disabled-scopes", disabled_scopes);
 
307
  }
 
308
 
 
309
  private void enable_scope (string scope_id)
 
310
  {
 
311
    if (!(scope_id in disabled_scope_ids)) return;
 
312
 
 
313
    var pref_man = PreferencesManager.get_default ();
 
314
    string[] disabled_scopes = {};
 
315
    foreach (unowned string disabled_scope_id in pref_man.disabled_scopes)
 
316
    {
 
317
      if (disabled_scope_id != scope_id)
 
318
        disabled_scopes += disabled_scope_id;
 
319
    }
 
320
 
 
321
    var settings = new Settings (LIBUNITY_SCHEMA);
 
322
    settings.set_strv ("disabled-scopes", disabled_scopes);
 
323
  }
 
324
 
 
325
  private string get_scope_icon (string icon_hint, bool is_disabled)
 
326
  {
 
327
    try
 
328
    {
 
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
 
335
      if (is_disabled)
 
336
        anno_icon.set_colorize_rgba (1.0, 1.0, 1.0, 0.5);
 
337
      return anno_icon.to_string ();
 
338
    }
 
339
    catch (Error err)
 
340
    {
 
341
      return "";
 
342
    }
 
343
  }
 
344
 
 
345
  private void add_apps_results (string query, ResultSet result_set)
 
346
  {
 
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)
 
351
    {
 
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);
 
355
 
 
356
      var result = ScopeResult ();
 
357
      result.uri = app_id;
 
358
      result.category = ResultCategory.INSTALLED;
 
359
      result.icon_hint = icon_hint;
 
360
      result.result_type = Unity.ResultType.PERSONAL;
 
361
      result.mimetype = "application/x-desktop";
 
362
      result.title = name;
 
363
      result.comment = "";
 
364
      result.dnd_uri = app_id;
 
365
 
 
366
      result_set.add_result (result);
 
367
    }
 
368
  }
 
369
 
 
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)
 
373
  {
 
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)
 
378
    {
 
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;
 
384
 
 
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);
 
392
      result.comment = "";
 
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);
 
396
 
 
397
      result_set.add_result (result);
 
398
    }
 
399
 
 
400
  }
 
401
 
 
402
  private void dispatch_search (ScopeSearchBase search, ScopeSearchBaseCallback cb)
 
403
  {
 
404
    perform_search.begin (search, (obj, res) =>
 
405
    {
 
406
      perform_search.end (res);
 
407
      cb (search);
 
408
    });
 
409
  }
 
410
 
 
411
  private void dispatch_preview (ResultPreviewer previewer, AbstractPreviewCallback cb)
 
412
  {
 
413
    perform_preview.begin (previewer, (obj, res) =>
 
414
    {
 
415
      var preview = perform_preview.end (res);
 
416
      cb (previewer, preview);
 
417
    });
 
418
  }
 
419
 
 
420
  private async void perform_search (ScopeSearchBase search)
 
421
  {
 
422
    // are we properly initialized?
 
423
    while (!scopes_loaded)
 
424
    {
 
425
      Timeout.add (100, perform_search.callback);
 
426
      yield;
 
427
    }
 
428
    var search_type = search.search_context.search_type;
 
429
    bool include_scope_results = true;
 
430
    if (search.search_context.filter_state != null)
 
431
    {
 
432
      var filter = search.search_context.filter_state.get_filter_by_id ("type");
 
433
      if (filter != null && filter.filtering)
 
434
      {
 
435
        var of = filter as OptionsFilter;
 
436
        include_scope_results = of.get_option ("scopes").active;
 
437
      }
 
438
    }
 
439
 
 
440
    add_apps_results (search.search_context.search_query,
 
441
                      search.search_context.result_set);
 
442
 
 
443
    if (include_scope_results && search_type == SearchType.DEFAULT)
 
444
    {
 
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);
 
449
    }
 
450
  }
 
451
 
 
452
  private async Preview? perform_preview (ResultPreviewer previewer)
 
453
  {
 
454
    unowned string uri = previewer.result.uri;
 
455
    if (uri.has_prefix ("scope://"))
 
456
    {
 
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)
 
464
      {
 
465
        if (model.get_string (iter, RemoteScopesColumn.SCOPE_ID) == scope_id)
 
466
        {
 
467
          found_iter = iter;
 
468
          break;
 
469
        }
 
470
        iter = model.next (iter);
 
471
      }
 
472
 
 
473
      if (found_iter == end_iter)
 
474
      {
 
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)
 
480
        {
 
481
          if (model.get_string (iter, RemoteScopesColumn.SCOPE_ID) == scope_id)
 
482
          {
 
483
            found_iter = iter;
 
484
            break;
 
485
          }
 
486
          iter = model.next (iter);
 
487
        }
 
488
      }
 
489
 
 
490
      if (found_iter == end_iter) return null;
 
491
 
 
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;
 
498
      try
 
499
      {
 
500
        if (icon != "") app_icon = Icon.new_for_string (icon);
 
501
        if (screenshot != "") screenshot_icon = Icon.new_for_string (screenshot);
 
502
      }
 
503
      catch (Error err) {}
 
504
 
 
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;
 
509
      if (scope_disabled)
 
510
      {
 
511
        action = new Unity.PreviewAction ("enable-scope", _("Enable"), null);
 
512
      }
 
513
      else
 
514
      {
 
515
        action = new Unity.PreviewAction ("disable-scope", _("Disable"), null);
 
516
      }
 
517
      if (!(scope_id in locked_scope_ids))
 
518
      {
 
519
        preview.add_action (action);
 
520
      }
 
521
      return preview;
 
522
    }
 
523
    else if (uri.has_prefix ("application:"))
 
524
    {
 
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)
 
530
      {
 
531
        if (apps_model.get_string (iter, AppsColumn.APP_URI) == uri)
 
532
        {
 
533
          found_iter = iter;
 
534
          break;
 
535
        }
 
536
        iter = apps_model.next (iter);
 
537
      }
 
538
      if (found_iter == end_iter) return null; // uh oh
 
539
 
 
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;
 
546
      try
 
547
      {
 
548
        if (icon != "") app_icon = Icon.new_for_string (icon);
 
549
        if (screenshot != "") screenshot_icon = Icon.new_for_string (screenshot);
 
550
      }
 
551
      catch (Error err) {}
 
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);
 
556
      return preview;
 
557
    }
 
558
    return null;
 
559
  }
 
560
 
 
561
  private ActivationResponse? activate (ScopeResult result,
 
562
                                        SearchMetadata metadata,
 
563
                                        string? action_id)
 
564
  {
 
565
    if (result.uri.has_prefix ("scope://"))
 
566
    {
 
567
      var scope_id = result.uri.substring (8);
 
568
      if (action_id == "enable-scope")
 
569
      {
 
570
        enable_scope (scope_id);
 
571
      }
 
572
      else if (action_id == "disable-scope")
 
573
      {
 
574
        disable_scope (scope_id);
 
575
      }
 
576
      return new Unity.ActivationResponse (HandledType.SHOW_PREVIEW, result.uri);
 
577
    }
 
578
 
 
579
    return null;
 
580
  }
 
581
}
 
582