~davidmhewitt/slingshot/fix-1665931

« back to all changes in this revision

Viewing changes to lib/synapse-core/desktop-file-service.vala

  • Committer: RabbitBot
  • Author(s): Fabio Zaramella
  • Date: 2017-01-31 00:02:45 UTC
  • mfrom: (724.1.3 code-style)
  • Revision ID: rabbitbot-20170131000245-ef6klohh85zvrib5
Synapse-core and synapse-plugins:
* Code style

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
/*
2
 
 * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com>
3
 
 *
4
 
 * This library is free software; you can redistribute it and/or
5
 
 * modify it under the terms of the GNU Lesser General Public
6
 
 * License as published by the Free Software Foundation; either
7
 
 * version 2.1 of the License, or (at your option) any later version.
8
 
 *
9
 
 * This library is distributed in the hope that it will be useful,
10
 
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12
 
 * Lesser General Public License for more details.
13
 
 *
14
 
 * You should have received a copy of the GNU Lesser General Public License
15
 
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
 
 *
17
 
 * Authored by Michal Hruby <michal.mhr@gmail.com>
18
 
 *             Alberto Aldegheri <albyrock87+dev@gmail.com>
19
 
 *
20
 
 */
21
 
 
22
 
namespace Synapse
23
 
{
24
 
  errordomain DesktopFileError
25
 
  {
26
 
    UNINTERESTING_ENTRY
27
 
  }
28
 
 
29
 
  public class DesktopFileInfo: Object
30
 
  {
31
 
    // registered environments from http://standards.freedesktop.org/menu-spec/latest
32
 
    // (and pantheon)
33
 
    [Flags]
34
 
    public enum EnvironmentType
35
 
    {
36
 
      GNOME     = 1 << 0,
37
 
      KDE       = 1 << 1,
38
 
      LXDE      = 1 << 2,
39
 
      MATE      = 1 << 3,
40
 
      RAZOR     = 1 << 4,
41
 
      ROX       = 1 << 5,
42
 
      TDE       = 1 << 6,
43
 
      UNITY     = 1 << 7,
44
 
      XFCE      = 1 << 8,
45
 
      PANTHEON  = 1 << 9,
46
 
      OLD       = 1 << 10,
47
 
 
48
 
      ALL       = 0x3FF
49
 
    }
50
 
   
51
 
    public string desktop_id { get; construct set; } 
52
 
    public string name { get; construct set; }
53
 
    public string generic_name { get; construct set; }
54
 
    public string comment { get; set; default = ""; }
55
 
    public string icon_name { get; construct set; default = ""; }
56
 
    public string gettext_domain { get; construct set; }
57
 
 
58
 
    public bool needs_terminal { get; set; default = false; }
59
 
    public string filename { get; construct set; }
60
 
 
61
 
    public string exec { get; set; }
62
 
 
63
 
    public bool is_hidden { get; private set; default = false; }
64
 
    public bool is_valid { get; private set; default = true; }
65
 
 
66
 
    public string[] mime_types = null;
67
 
    
68
 
    private string? name_folded = null;
69
 
    public unowned string get_name_folded ()
70
 
    {
71
 
      if (name_folded == null) name_folded = name.casefold ();
72
 
      return name_folded;
73
 
    }
74
 
    
75
 
    public EnvironmentType show_in { get; set; default = EnvironmentType.ALL; }
76
 
 
77
 
    private const string[] SUPPORTED_GETTEXT_DOMAINS_KEYS = {"X-Ubuntu-Gettext-Domain", "X-GNOME-Gettext-Domain"};
78
 
    private const string GROUP = "Desktop Entry";
79
 
 
80
 
    public DesktopFileInfo.for_keyfile (string path, KeyFile keyfile,
81
 
                                        string desktop_id)
82
 
    {
83
 
      Object (filename: path, desktop_id: desktop_id);
84
 
 
85
 
      init_from_keyfile (keyfile);
86
 
    }
87
 
 
88
 
    private EnvironmentType parse_environments (string[] environments)
89
 
    {
90
 
      EnvironmentType result = 0;
91
 
      foreach (unowned string env in environments)
92
 
      {
93
 
        string env_up = env.up ();
94
 
        switch (env_up)
95
 
        {
96
 
          case "GNOME": result |= EnvironmentType.GNOME; break;
97
 
          case "PANTHEON": result |= EnvironmentType.PANTHEON; break;
98
 
          case "KDE": result |= EnvironmentType.KDE; break;
99
 
          case "LXDE": result |= EnvironmentType.LXDE; break;
100
 
          case "MATE": result |= EnvironmentType.MATE; break;
101
 
          case "RAZOR": result |= EnvironmentType.RAZOR; break;
102
 
          case "ROX": result |= EnvironmentType.ROX; break;
103
 
          case "TDE": result |= EnvironmentType.TDE; break;
104
 
          case "UNITY": result |= EnvironmentType.UNITY; break;
105
 
          case "XFCE": result |= EnvironmentType.XFCE; break;
106
 
          case "OLD": result |= EnvironmentType.OLD; break;
107
 
          default: warning ("%s is not understood", env); break;
108
 
        }
109
 
      }
110
 
      return result;
111
 
    }
112
 
 
113
 
    private void init_from_keyfile (KeyFile keyfile)
114
 
    {
115
 
      try
116
 
      {
117
 
        if (keyfile.get_string (GROUP, "Type") != "Application")
118
 
        {
119
 
          throw new DesktopFileError.UNINTERESTING_ENTRY ("Not Application-type desktop entry");
120
 
        }
121
 
        
122
 
        if (keyfile.has_key (GROUP, "Categories"))
123
 
        {
124
 
          string[] categories = keyfile.get_string_list (GROUP, "Categories");
125
 
          if ("Screensaver" in categories)
126
 
          {
127
 
            throw new DesktopFileError.UNINTERESTING_ENTRY ("Screensaver desktop entry");
128
 
          }
129
 
        }
130
 
        foreach (var domain_key in SUPPORTED_GETTEXT_DOMAINS_KEYS) {
131
 
          if (keyfile.has_key (GROUP, domain_key)) {
132
 
            gettext_domain = keyfile.get_string (GROUP, domain_key);
133
 
            break;
134
 
          }
135
 
        }
136
 
 
137
 
        DesktopAppInfo app_info;
138
 
        app_info = new DesktopAppInfo.from_keyfile (keyfile);
139
 
 
140
 
        if (app_info == null)
141
 
        {
142
 
          throw new DesktopFileError.UNINTERESTING_ENTRY ("Unable to create AppInfo");
143
 
        }
144
 
 
145
 
        name = app_info.get_name ();
146
 
        generic_name = app_info.get_generic_name () ?? "";     
147
 
        exec = app_info.get_commandline ();
148
 
        if (exec == null)
149
 
        {
150
 
          throw new DesktopFileError.UNINTERESTING_ENTRY ("Unable to get exec for %s".printf (name));
151
 
        }
152
 
 
153
 
        // check for hidden desktop files
154
 
        if (keyfile.has_key (GROUP, "Hidden") &&
155
 
          keyfile.get_boolean (GROUP, "Hidden"))
156
 
        {
157
 
          is_hidden = true;
158
 
        }
159
 
        if (keyfile.has_key (GROUP, "NoDisplay") &&
160
 
          keyfile.get_boolean (GROUP, "NoDisplay"))
161
 
        {
162
 
          is_hidden = true;
163
 
        }
164
 
 
165
 
        comment = app_info.get_description () ?? "";
166
 
 
167
 
        var icon = app_info.get_icon () ??
168
 
          new ThemedIcon ("application-default-icon");
169
 
        icon_name = icon.to_string ();
170
 
 
171
 
        if (keyfile.has_key (GROUP, "MimeType"))
172
 
        {
173
 
          mime_types = keyfile.get_string_list (GROUP, "MimeType");
174
 
        }
175
 
        if (keyfile.has_key (GROUP, "Terminal"))
176
 
        {
177
 
          needs_terminal = keyfile.get_boolean (GROUP, "Terminal");
178
 
        }
179
 
        if (keyfile.has_key (GROUP, "OnlyShowIn"))
180
 
        {
181
 
          show_in = parse_environments (keyfile.get_string_list (GROUP, 
182
 
                                                                 "OnlyShowIn"));
183
 
        }
184
 
        else if (keyfile.has_key (GROUP, "NotShowIn"))
185
 
        {
186
 
          var not_show = parse_environments (keyfile.get_string_list (GROUP,
187
 
                                                                  "NotShowIn"));
188
 
          show_in = EnvironmentType.ALL ^ not_show;
189
 
        }
190
 
        
191
 
        // special case these, people are using them quite often and wonder
192
 
        // why they don't appear
193
 
        if (filename.has_suffix ("gconf-editor.desktop") ||
194
 
            filename.has_suffix ("dconf-editor.desktop"))
195
 
        {
196
 
          is_hidden = false;
197
 
        }
198
 
      }
199
 
      catch (Error err)
200
 
      {
201
 
        Utils.Logger.warning (this, "%s", err.message);
202
 
        is_valid = false;
203
 
      }
204
 
    }
205
 
  }
206
 
 
207
 
  public class DesktopFileService : Object
208
 
  {
209
 
    private static unowned DesktopFileService? instance;
210
 
    private Utils.AsyncOnce<bool> init_once;
211
 
 
212
 
    // singleton that can be easily destroyed
213
 
    public static DesktopFileService get_default ()
214
 
    {
215
 
      return instance ?? new DesktopFileService ();
216
 
    }
217
 
 
218
 
    private DesktopFileService ()
219
 
    {
220
 
    }
221
 
 
222
 
    private Gee.List<FileMonitor> directory_monitors;
223
 
    private Gee.List<DesktopFileInfo> all_desktop_files;
224
 
    private Gee.List<DesktopFileInfo> non_hidden_desktop_files;
225
 
    private Gee.Map<unowned string, Gee.List<DesktopFileInfo> > mimetype_map;
226
 
    private Gee.Map<string, Gee.List<DesktopFileInfo> > exec_map;
227
 
    private Gee.Map<string, DesktopFileInfo> desktop_id_map;
228
 
    private Gee.MultiMap<string, string> mimetype_parent_map;
229
 
    
230
 
    construct
231
 
    {
232
 
      instance = this;
233
 
 
234
 
      directory_monitors = new Gee.ArrayList<FileMonitor> ();
235
 
      all_desktop_files = new Gee.ArrayList<DesktopFileInfo> ();
236
 
      non_hidden_desktop_files = new Gee.ArrayList<DesktopFileInfo> ();
237
 
      mimetype_parent_map = new Gee.HashMultiMap<string, string> ();
238
 
      init_once = new Utils.AsyncOnce<bool> ();
239
 
 
240
 
      initialize.begin ();
241
 
    }
242
 
    
243
 
    ~DesktopFileService ()
244
 
    {
245
 
      instance = null;
246
 
    }
247
 
 
248
 
    public async void initialize ()
249
 
    {
250
 
      if (init_once.is_initialized ()) return;
251
 
      var is_locked = yield init_once.enter ();
252
 
      if (!is_locked) return;
253
 
 
254
 
      get_environment_type ();
255
 
      DesktopAppInfo.set_desktop_env (session_type_str);
256
 
 
257
 
      Idle.add_full (Priority.LOW, initialize.callback);
258
 
      yield;
259
 
 
260
 
      yield load_all_desktop_files ();
261
 
 
262
 
      init_once.leave (true);
263
 
    }
264
 
    
265
 
    private DesktopFileInfo.EnvironmentType session_type =
266
 
      DesktopFileInfo.EnvironmentType.GNOME;
267
 
    private string session_type_str = "GNOME";
268
 
    
269
 
    public DesktopFileInfo.EnvironmentType get_environment ()
270
 
    {
271
 
      return this.session_type;
272
 
    }
273
 
    
274
 
    private void get_environment_type ()
275
 
    {
276
 
      unowned string? session_var;
277
 
      session_var = Environment.get_variable ("XDG_CURRENT_DESKTOP");
278
 
      if (session_var == null)
279
 
      {
280
 
        session_var = Environment.get_variable ("DESKTOP_SESSION");
281
 
      }
282
 
 
283
 
      if (session_var == null) return;
284
 
 
285
 
      string session = session_var.down ();
286
 
 
287
 
      if (session.has_prefix ("unity") || session.has_prefix ("ubuntu"))
288
 
      {
289
 
        session_type = DesktopFileInfo.EnvironmentType.UNITY;
290
 
        session_type_str = "Unity";
291
 
      }
292
 
      else if (session.has_prefix ("kde"))
293
 
      {
294
 
        session_type = DesktopFileInfo.EnvironmentType.KDE;
295
 
        session_type_str = "KDE";
296
 
      }
297
 
      else if (session.has_prefix ("gnome"))
298
 
      {
299
 
        session_type = DesktopFileInfo.EnvironmentType.GNOME;
300
 
        session_type_str = "GNOME";
301
 
      }
302
 
      else if (session.has_prefix ("lx"))
303
 
      {
304
 
        session_type = DesktopFileInfo.EnvironmentType.LXDE;
305
 
        session_type_str = "LXDE";
306
 
      }
307
 
      else if (session.has_prefix ("xfce"))
308
 
      {
309
 
        session_type = DesktopFileInfo.EnvironmentType.XFCE;
310
 
        session_type_str = "XFCE";
311
 
      }
312
 
      else if (session.has_prefix ("mate"))
313
 
      {
314
 
        session_type = DesktopFileInfo.EnvironmentType.MATE;
315
 
        session_type_str = "MATE";
316
 
      }
317
 
      else if (session.has_prefix ("razor"))
318
 
      {
319
 
        session_type = DesktopFileInfo.EnvironmentType.RAZOR;
320
 
        session_type_str = "Razor";
321
 
      }
322
 
      else if (session.has_prefix ("tde"))
323
 
      {
324
 
        session_type = DesktopFileInfo.EnvironmentType.TDE;
325
 
        session_type_str = "TDE";
326
 
      }
327
 
      else if (session.has_prefix ("rox"))
328
 
      {
329
 
        session_type = DesktopFileInfo.EnvironmentType.ROX;
330
 
        session_type_str = "ROX";
331
 
      }
332
 
      else if (session.has_prefix ("pantheon"))
333
 
      {
334
 
        session_type = DesktopFileInfo.EnvironmentType.PANTHEON;
335
 
        session_type_str = "Pantheon";
336
 
      }
337
 
      else
338
 
      {
339
 
        warning ("Desktop session type is not recognized, assuming GNOME.");
340
 
      }
341
 
    }
342
 
 
343
 
    private async void process_directory (File directory,
344
 
                                          string id_prefix,
345
 
                                          Gee.Set<File> monitored_dirs)
346
 
    {
347
 
      try
348
 
      {
349
 
        string path = directory.get_path ();
350
 
        // we need to skip menu-xdg directory, see lp:686624
351
 
        if (path != null && path.has_suffix ("menu-xdg")) return;
352
 
        // screensavers don't interest us, skip those
353
 
        if (path != null && path.has_suffix ("/screensavers")) return;
354
 
 
355
 
        Utils.Logger.debug (this, "Searching for desktop files in: %s", path);
356
 
        bool exists = yield Utils.query_exists_async (directory);
357
 
        if (!exists) return;
358
 
        /* Check if we already scanned this directory // lp:686624 */
359
 
        foreach (var scanned_dir in monitored_dirs)
360
 
        {
361
 
          if (path == scanned_dir.get_path ()) return;
362
 
        }
363
 
        monitored_dirs.add (directory);
364
 
        var enumerator = yield directory.enumerate_children_async (
365
 
          FileAttribute.STANDARD_NAME + "," + FileAttribute.STANDARD_TYPE,
366
 
          0, 0);
367
 
        var files = yield enumerator.next_files_async (1024, 0);
368
 
        foreach (var f in files)
369
 
        {
370
 
          unowned string name = f.get_name ();
371
 
          if (f.get_file_type () == FileType.DIRECTORY)
372
 
          {
373
 
            // FIXME: this could cause too many open files error, or?
374
 
            var subdir = directory.get_child (name);
375
 
            var new_prefix = "%s%s-".printf (id_prefix, subdir.get_basename ());
376
 
            yield process_directory (subdir, new_prefix, monitored_dirs);
377
 
          }
378
 
          else
379
 
          {
380
 
            // ignore ourselves
381
 
            if (name.has_suffix ("synapse.desktop")) continue;
382
 
            if (name.has_suffix (".desktop"))
383
 
            {
384
 
              yield load_desktop_file (directory.get_child (name), id_prefix);
385
 
            }
386
 
          }
387
 
        }
388
 
      }
389
 
      catch (Error err)
390
 
      {
391
 
        warning ("%s", err.message);
392
 
      }
393
 
    }
394
 
 
395
 
    private async void load_all_desktop_files ()
396
 
    {
397
 
      string[] data_dirs = Environment.get_system_data_dirs ();
398
 
      data_dirs += Environment.get_user_data_dir ();
399
 
 
400
 
      Gee.Set<File> desktop_file_dirs = new Gee.HashSet<File> ();
401
 
 
402
 
      mimetype_parent_map.clear ();
403
 
 
404
 
      foreach (unowned string data_dir in data_dirs)
405
 
      {
406
 
        string dir_path = Path.build_filename (data_dir, "applications", null);
407
 
        var directory = File.new_for_path (dir_path);
408
 
        yield process_directory (directory, "", desktop_file_dirs);
409
 
        dir_path = Path.build_filename (data_dir, "mime", "subclasses");
410
 
        yield load_mime_parents_from_file (dir_path);
411
 
      }
412
 
      
413
 
      create_indices ();
414
 
 
415
 
      directory_monitors = new Gee.ArrayList<FileMonitor> ();
416
 
      foreach (File d in desktop_file_dirs)
417
 
      {
418
 
        try
419
 
        {
420
 
          FileMonitor monitor = d.monitor_directory (0, null);
421
 
          monitor.changed.connect (this.desktop_file_directory_changed);
422
 
          directory_monitors.add (monitor);
423
 
        }
424
 
        catch (Error err)
425
 
        {
426
 
          warning ("Unable to monitor directory: %s", err.message);
427
 
        }
428
 
      }
429
 
    }
430
 
    
431
 
    private uint timer_id = 0;
432
 
 
433
 
    public signal void reload_started ();
434
 
    public signal void reload_done ();
435
 
 
436
 
    private void desktop_file_directory_changed ()
437
 
    {
438
 
      reload_started ();
439
 
      if (timer_id != 0)
440
 
      {
441
 
        Source.remove (timer_id);
442
 
      }
443
 
      
444
 
      timer_id = Timeout.add (5000, () =>
445
 
      {
446
 
        timer_id = 0;
447
 
        reload_desktop_files.begin ();
448
 
        return false;
449
 
      });
450
 
    }
451
 
    
452
 
    private async void reload_desktop_files ()
453
 
    {
454
 
      debug ("Reloading desktop files...");
455
 
      all_desktop_files.clear ();
456
 
      non_hidden_desktop_files.clear ();
457
 
      yield load_all_desktop_files ();
458
 
 
459
 
      reload_done ();
460
 
    }
461
 
 
462
 
    private async void load_desktop_file (File file, string id_prefix)
463
 
    {
464
 
      try
465
 
      {
466
 
        uint8[] file_contents;
467
 
        bool success = yield file.load_contents_async (null, out file_contents,
468
 
                                                       null);
469
 
        if (success)
470
 
        {
471
 
          var keyfile = new KeyFile ();
472
 
          keyfile.load_from_data ((string) file_contents,
473
 
                                  file_contents.length, 0);
474
 
 
475
 
          var desktop_id = "%s%s".printf (id_prefix, file.get_basename ());
476
 
          var dfi = new DesktopFileInfo.for_keyfile (file.get_path (),
477
 
                                                     keyfile,
478
 
                                                     desktop_id);
479
 
          if (dfi.is_valid)
480
 
          {
481
 
            all_desktop_files.add (dfi);
482
 
            if (!dfi.is_hidden && session_type in dfi.show_in)
483
 
            {
484
 
              non_hidden_desktop_files.add (dfi);
485
 
            }
486
 
          }
487
 
        }
488
 
      }
489
 
      catch (Error err)
490
 
      {
491
 
        warning ("%s", err.message);
492
 
      }
493
 
    }
494
 
    
495
 
    private void create_indices ()
496
 
    {
497
 
      // create mimetype maps
498
 
      mimetype_map =
499
 
        new Gee.HashMap<unowned string, Gee.List<DesktopFileInfo> > ();
500
 
      // and exec map
501
 
      exec_map =
502
 
        new Gee.HashMap<string, Gee.List<DesktopFileInfo> > ();
503
 
      // and desktop id map
504
 
      desktop_id_map =
505
 
        new Gee.HashMap<string, DesktopFileInfo> ();
506
 
 
507
 
      Regex exec_re;
508
 
      try
509
 
      {
510
 
        exec_re = new Regex ("%[fFuU]");
511
 
      }
512
 
      catch (Error err)
513
 
      {
514
 
        critical ("%s", err.message);
515
 
        return;
516
 
      }
517
 
 
518
 
      foreach (var dfi in all_desktop_files)
519
 
      {
520
 
        string exec = "";
521
 
        try
522
 
        {
523
 
          exec = exec_re.replace_literal (dfi.exec, -1, 0, "");
524
 
        }
525
 
        catch (RegexError err)
526
 
        {
527
 
          Utils.Logger.error (this, "%s", err.message);
528
 
        }
529
 
        exec = exec.strip ();
530
 
        // update exec map
531
 
        Gee.List<DesktopFileInfo>? exec_list = exec_map[exec];
532
 
        if (exec_list == null)
533
 
        {
534
 
          exec_list = new Gee.ArrayList<DesktopFileInfo> ();
535
 
          exec_map[exec] = exec_list;
536
 
        }
537
 
        exec_list.add (dfi);
538
 
 
539
 
        // update desktop id map
540
 
        var desktop_id = dfi.desktop_id ?? Path.get_basename (dfi.filename);
541
 
        desktop_id_map[desktop_id] = dfi;
542
 
 
543
 
        // update mimetype map
544
 
        if (dfi.is_hidden || dfi.mime_types == null) continue;
545
 
        
546
 
        foreach (unowned string mime_type in dfi.mime_types)
547
 
        {
548
 
          Gee.List<DesktopFileInfo>? list = mimetype_map[mime_type];
549
 
          if (list == null)
550
 
          {
551
 
            list = new Gee.ArrayList<DesktopFileInfo> ();
552
 
            mimetype_map[mime_type] = list;
553
 
          }
554
 
          list.add (dfi);
555
 
        }
556
 
      }
557
 
    }
558
 
 
559
 
    private async void load_mime_parents_from_file (string fi)
560
 
    {
561
 
      var file = File.new_for_path (fi);
562
 
      bool exists = yield Utils.query_exists_async (file);
563
 
      if (!exists) return;
564
 
      try
565
 
      {
566
 
        var fis = yield file.read_async (GLib.Priority.DEFAULT);
567
 
        var dis = new DataInputStream (fis);
568
 
        string line = null;
569
 
        string[] mimes = null;
570
 
        int len = 0;
571
 
        // Read lines until end of file (null) is reached
572
 
        do {
573
 
          line = yield dis.read_line_async (GLib.Priority.DEFAULT);
574
 
          if (line == null) break;
575
 
          if (line.has_prefix ("#")) continue; //comment line
576
 
          mimes = line.split (" ");
577
 
          len = (int)GLib.strv_length (mimes);
578
 
          if (len != 2) continue;
579
 
          // cannot be parent of myself!
580
 
          if (mimes[0] == mimes[1]) continue;
581
 
          //debug ("Map %s -> %s", mimes[0], mimes[1]);
582
 
          mimetype_parent_map.set (mimes[0], mimes[1]);
583
 
        } while (true);
584
 
      } catch (GLib.Error err) { /* can't read file */ }
585
 
    }
586
 
    
587
 
    private void add_dfi_for_mime (string mime, Gee.Set<DesktopFileInfo> ret)
588
 
    {
589
 
      var dfis = mimetype_map[mime];
590
 
      if (dfis != null) ret.add_all (dfis);
591
 
 
592
 
      var parents = mimetype_parent_map[mime];
593
 
      if (parents == null) return;
594
 
      foreach (string parent in parents)
595
 
        add_dfi_for_mime (parent, ret);
596
 
    }
597
 
 
598
 
    // retuns desktop files available on the system (without hidden ones)
599
 
    public Gee.List<DesktopFileInfo> get_desktop_files ()
600
 
    {
601
 
      return non_hidden_desktop_files.read_only_view;
602
 
    }
603
 
    
604
 
    // returns all desktop files available on the system (even the ones which
605
 
    // are hidden by default)
606
 
    public Gee.List<DesktopFileInfo> get_all_desktop_files ()
607
 
    {
608
 
      return all_desktop_files.read_only_view;
609
 
    }
610
 
    
611
 
    public Gee.List<DesktopFileInfo> get_desktop_files_for_type (string mime_type)
612
 
    {
613
 
      var dfi_set = new Gee.HashSet<DesktopFileInfo> ();
614
 
      add_dfi_for_mime (mime_type, dfi_set);
615
 
      var ret = new Gee.ArrayList<DesktopFileInfo> ();
616
 
      ret.add_all (dfi_set);
617
 
      return ret;
618
 
    }
619
 
 
620
 
    public Gee.List<DesktopFileInfo> get_desktop_files_for_exec (string exec)
621
 
    {
622
 
      return exec_map[exec] ?? new Gee.ArrayList<DesktopFileInfo> ();
623
 
    }
624
 
    
625
 
    public DesktopFileInfo? get_desktop_file_for_id (string desktop_id)
626
 
    {
627
 
      return desktop_id_map[desktop_id];
628
 
    }
629
 
  }
 
2
* Copyright (c) 2010 Michal Hruby <michal.mhr@gmail.com>
 
3
*               2017 elementary LLC.
 
4
*
 
5
* This program is free software; you can redistribute it and/or
 
6
* modify it under the terms of the GNU General Public
 
7
* License as published by the Free Software Foundation; either
 
8
* version 2 of the License, or (at your option) any later version.
 
9
*
 
10
* This program is distributed in the hope that it will be useful,
 
11
* but WITHOUT ANY WARRANTY; without even the implied warranty of
 
12
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 
13
* General Public License for more details.
 
14
*
 
15
* You should have received a copy of the GNU General Public
 
16
* License along with this program; if not, write to the
 
17
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 
18
* Boston, MA 02110-1301 USA
 
19
*
 
20
* Authored by: Michal Hruby <michal.mhr@gmail.com>
 
21
*/
 
22
 
 
23
namespace Synapse {
 
24
    errordomain DesktopFileError {
 
25
        UNINTERESTING_ENTRY
 
26
    }
 
27
 
 
28
    public class DesktopFileInfo: Object {
 
29
        // registered environments from http://standards.freedesktop.org/menu-spec/latest
 
30
        // (and pantheon)
 
31
        [Flags]
 
32
        public enum EnvironmentType {
 
33
            GNOME     = 1 << 0,
 
34
            KDE       = 1 << 1,
 
35
            LXDE      = 1 << 2,
 
36
            MATE      = 1 << 3,
 
37
            RAZOR     = 1 << 4,
 
38
            ROX       = 1 << 5,
 
39
            TDE       = 1 << 6,
 
40
            UNITY     = 1 << 7,
 
41
            XFCE      = 1 << 8,
 
42
            PANTHEON  = 1 << 9,
 
43
            OLD       = 1 << 10,
 
44
 
 
45
            ALL       = 0x3FF
 
46
        }
 
47
 
 
48
        public string desktop_id { get; construct set; } 
 
49
        public string name { get; construct set; }
 
50
        public string generic_name { get; construct set; }
 
51
        public string comment { get; set; default = ""; }
 
52
        public string icon_name { get; construct set; default = ""; }
 
53
        public string gettext_domain { get; construct set; }
 
54
 
 
55
        public bool needs_terminal { get; set; default = false; }
 
56
        public string filename { get; construct set; }
 
57
 
 
58
        public string exec { get; set; }
 
59
 
 
60
        public bool is_hidden { get; private set; default = false; }
 
61
        public bool is_valid { get; private set; default = true; }
 
62
 
 
63
        public string[] mime_types = null;
 
64
 
 
65
        private string? name_folded = null;
 
66
        public unowned string get_name_folded () {
 
67
            if (name_folded == null) {
 
68
                name_folded = name.casefold ();
 
69
            }
 
70
 
 
71
            return name_folded;
 
72
        }
 
73
 
 
74
        public EnvironmentType show_in { get; set; default = EnvironmentType.ALL; }
 
75
 
 
76
        private const string[] SUPPORTED_GETTEXT_DOMAINS_KEYS = {"X-Ubuntu-Gettext-Domain", "X-GNOME-Gettext-Domain"};
 
77
        private const string GROUP = "Desktop Entry";
 
78
 
 
79
        public DesktopFileInfo.for_keyfile (string path, KeyFile keyfile, string desktop_id) {
 
80
            Object (filename: path, desktop_id: desktop_id);
 
81
 
 
82
            init_from_keyfile (keyfile);
 
83
        }
 
84
 
 
85
        private EnvironmentType parse_environments (string[] environments) {
 
86
            EnvironmentType result = 0;
 
87
            foreach (unowned string env in environments) {
 
88
                string env_up = env.up ();
 
89
                switch (env_up) {
 
90
                    case "GNOME":
 
91
                        result |= EnvironmentType.GNOME;
 
92
                        break;
 
93
                    case "PANTHEON":
 
94
                        result |= EnvironmentType.PANTHEON;
 
95
                        break;
 
96
                    case "KDE":
 
97
                        result |= EnvironmentType.KDE;
 
98
                        break;
 
99
                    case "LXDE":
 
100
                        result |= EnvironmentType.LXDE;
 
101
                        break;
 
102
                    case "MATE":
 
103
                        result |= EnvironmentType.MATE;
 
104
                        break;
 
105
                    case "RAZOR":
 
106
                        result |= EnvironmentType.RAZOR;
 
107
                        break;
 
108
                    case "ROX": result |= EnvironmentType.ROX; break;
 
109
                    case "TDE": result |= EnvironmentType.TDE; break;
 
110
                    case "UNITY": result |= EnvironmentType.UNITY; break;
 
111
                    case "XFCE": result |= EnvironmentType.XFCE; break;
 
112
                    case "OLD": result |= EnvironmentType.OLD; break;
 
113
                    default: warning ("%s is not understood", env); break;
 
114
                }
 
115
            }
 
116
            return result;
 
117
        }
 
118
 
 
119
        private void init_from_keyfile (KeyFile keyfile) {
 
120
            try {
 
121
                if (keyfile.get_string (GROUP, "Type") != "Application") {
 
122
                    throw new DesktopFileError.UNINTERESTING_ENTRY ("Not Application-type desktop entry");
 
123
                }
 
124
 
 
125
                if (keyfile.has_key (GROUP, "Categories")) {
 
126
                    string[] categories = keyfile.get_string_list (GROUP, "Categories");
 
127
                    if ("Screensaver" in categories) {
 
128
                        throw new DesktopFileError.UNINTERESTING_ENTRY ("Screensaver desktop entry");
 
129
                    }
 
130
                }
 
131
                foreach (var domain_key in SUPPORTED_GETTEXT_DOMAINS_KEYS) {
 
132
                    if (keyfile.has_key (GROUP, domain_key)) {
 
133
                        gettext_domain = keyfile.get_string (GROUP, domain_key);
 
134
                        break;
 
135
                    }
 
136
                }
 
137
 
 
138
                DesktopAppInfo app_info;
 
139
                app_info = new DesktopAppInfo.from_keyfile (keyfile);
 
140
 
 
141
                if (app_info == null) {
 
142
                    throw new DesktopFileError.UNINTERESTING_ENTRY ("Unable to create AppInfo");
 
143
                }
 
144
 
 
145
                name = app_info.get_name ();
 
146
                generic_name = app_info.get_generic_name () ?? "";     
 
147
                exec = app_info.get_commandline ();
 
148
                if (exec == null) {
 
149
                    throw new DesktopFileError.UNINTERESTING_ENTRY ("Unable to get exec for %s".printf (name));
 
150
                }
 
151
 
 
152
                // check for hidden desktop files
 
153
                if (keyfile.has_key (GROUP, "Hidden") && keyfile.get_boolean (GROUP, "Hidden")) {
 
154
                    is_hidden = true;
 
155
                }
 
156
                if (keyfile.has_key (GROUP, "NoDisplay") && keyfile.get_boolean (GROUP, "NoDisplay")) {
 
157
                    is_hidden = true;
 
158
                }
 
159
 
 
160
                comment = app_info.get_description () ?? "";
 
161
 
 
162
                var icon = app_info.get_icon () ??
 
163
                new ThemedIcon ("application-default-icon");
 
164
                icon_name = icon.to_string ();
 
165
 
 
166
                if (keyfile.has_key (GROUP, "MimeType")) {
 
167
                    mime_types = keyfile.get_string_list (GROUP, "MimeType");
 
168
                }
 
169
                if (keyfile.has_key (GROUP, "Terminal")) {
 
170
                    needs_terminal = keyfile.get_boolean (GROUP, "Terminal");
 
171
                }
 
172
                if (keyfile.has_key (GROUP, "OnlyShowIn")) {
 
173
                    show_in = parse_environments (keyfile.get_string_list (GROUP, "OnlyShowIn"));
 
174
                } else if (keyfile.has_key (GROUP, "NotShowIn")) {
 
175
                    var not_show = parse_environments (keyfile.get_string_list (GROUP, "NotShowIn"));
 
176
                    show_in = EnvironmentType.ALL ^ not_show;
 
177
                }
 
178
 
 
179
                // special case these, people are using them quite often and wonder
 
180
                // why they don't appear
 
181
                if (filename.has_suffix ("gconf-editor.desktop") || filename.has_suffix ("dconf-editor.desktop")) {
 
182
                    is_hidden = false;
 
183
                }
 
184
            } catch (Error err) {
 
185
                Utils.Logger.warning (this, "%s", err.message);
 
186
                is_valid = false;
 
187
            }
 
188
        }
 
189
    }
 
190
 
 
191
    public class DesktopFileService : Object {
 
192
        private static unowned DesktopFileService? instance;
 
193
        private Utils.AsyncOnce<bool> init_once;
 
194
 
 
195
        // singleton that can be easily destroyed
 
196
        public static DesktopFileService get_default () {
 
197
            return instance ?? new DesktopFileService ();
 
198
        }
 
199
 
 
200
        private DesktopFileService () { }
 
201
 
 
202
        private Gee.List<FileMonitor> directory_monitors;
 
203
        private Gee.List<DesktopFileInfo> all_desktop_files;
 
204
        private Gee.List<DesktopFileInfo> non_hidden_desktop_files;
 
205
        private Gee.Map<unowned string, Gee.List<DesktopFileInfo> > mimetype_map;
 
206
        private Gee.Map<string, Gee.List<DesktopFileInfo> > exec_map;
 
207
        private Gee.Map<string, DesktopFileInfo> desktop_id_map;
 
208
        private Gee.MultiMap<string, string> mimetype_parent_map;
 
209
 
 
210
        construct {
 
211
            instance = this;
 
212
 
 
213
            directory_monitors = new Gee.ArrayList<FileMonitor> ();
 
214
            all_desktop_files = new Gee.ArrayList<DesktopFileInfo> ();
 
215
            non_hidden_desktop_files = new Gee.ArrayList<DesktopFileInfo> ();
 
216
            mimetype_parent_map = new Gee.HashMultiMap<string, string> ();
 
217
            init_once = new Utils.AsyncOnce<bool> ();
 
218
 
 
219
            initialize.begin ();
 
220
        }
 
221
 
 
222
        ~DesktopFileService () {
 
223
            instance = null;
 
224
        }
 
225
 
 
226
        public async void initialize () {
 
227
            if (init_once.is_initialized ()) {
 
228
                return;
 
229
            }
 
230
            var is_locked = yield init_once.enter ();
 
231
            if (!is_locked) {
 
232
                return;
 
233
            }
 
234
 
 
235
            get_environment_type ();
 
236
            DesktopAppInfo.set_desktop_env (session_type_str);
 
237
 
 
238
            Idle.add_full (Priority.LOW, initialize.callback);
 
239
            yield;
 
240
            yield load_all_desktop_files ();
 
241
 
 
242
            init_once.leave (true);
 
243
        }
 
244
 
 
245
        private DesktopFileInfo.EnvironmentType session_type = DesktopFileInfo.EnvironmentType.GNOME;
 
246
        private string session_type_str = "GNOME";
 
247
 
 
248
        public DesktopFileInfo.EnvironmentType get_environment () {
 
249
            return this.session_type;
 
250
        }
 
251
 
 
252
        private void get_environment_type () {
 
253
            unowned string? session_var;
 
254
            session_var = Environment.get_variable ("XDG_CURRENT_DESKTOP");
 
255
            if (session_var == null) {
 
256
                session_var = Environment.get_variable ("DESKTOP_SESSION");
 
257
            }
 
258
 
 
259
            if (session_var == null) {
 
260
                return;
 
261
            }
 
262
 
 
263
            string session = session_var.down ();
 
264
 
 
265
            if (session.has_prefix ("unity") || session.has_prefix ("ubuntu")) {
 
266
                session_type = DesktopFileInfo.EnvironmentType.UNITY;
 
267
                session_type_str = "Unity";
 
268
            } else if (session.has_prefix ("kde")) {
 
269
                session_type = DesktopFileInfo.EnvironmentType.KDE;
 
270
                session_type_str = "KDE";
 
271
            } else if (session.has_prefix ("gnome")) {
 
272
                session_type = DesktopFileInfo.EnvironmentType.GNOME;
 
273
                session_type_str = "GNOME";
 
274
            } else if (session.has_prefix ("lx")) {
 
275
                session_type = DesktopFileInfo.EnvironmentType.LXDE;
 
276
                session_type_str = "LXDE";
 
277
            } else if (session.has_prefix ("xfce")) {
 
278
                session_type = DesktopFileInfo.EnvironmentType.XFCE;
 
279
                session_type_str = "XFCE";
 
280
            } else if (session.has_prefix ("mate")) {
 
281
                session_type = DesktopFileInfo.EnvironmentType.MATE;
 
282
                session_type_str = "MATE";
 
283
            } else if (session.has_prefix ("razor")) {
 
284
                session_type = DesktopFileInfo.EnvironmentType.RAZOR;
 
285
                session_type_str = "Razor";
 
286
            } else if (session.has_prefix ("tde")) {
 
287
                session_type = DesktopFileInfo.EnvironmentType.TDE;
 
288
                session_type_str = "TDE";
 
289
            } else if (session.has_prefix ("rox")) {
 
290
                session_type = DesktopFileInfo.EnvironmentType.ROX;
 
291
                session_type_str = "ROX";
 
292
            } else if (session.has_prefix ("pantheon")) {
 
293
                session_type = DesktopFileInfo.EnvironmentType.PANTHEON;
 
294
                session_type_str = "Pantheon";
 
295
            } else {
 
296
                warning ("Desktop session type is not recognized, assuming GNOME.");
 
297
            }
 
298
        }
 
299
 
 
300
        private async void process_directory (File directory, string id_prefix, Gee.Set<File> monitored_dirs) {
 
301
            try {
 
302
                string path = directory.get_path ();
 
303
                // we need to skip menu-xdg directory, see lp:686624
 
304
                if (path != null && path.has_suffix ("menu-xdg")) {
 
305
                    return;
 
306
                }
 
307
                // screensavers don't interest us, skip those
 
308
                if (path != null && path.has_suffix ("/screensavers")) {
 
309
                    return;
 
310
                }
 
311
 
 
312
                Utils.Logger.debug (this, "Searching for desktop files in: %s", path);
 
313
                bool exists = yield Utils.query_exists_async (directory);
 
314
                if (!exists) {
 
315
                    return;
 
316
                }
 
317
                /* Check if we already scanned this directory // lp:686624 */
 
318
                foreach (var scanned_dir in monitored_dirs) {
 
319
                    if (path == scanned_dir.get_path ()) {
 
320
                        return;
 
321
                    }
 
322
                }
 
323
                monitored_dirs.add (directory);
 
324
                var enumerator = yield directory.enumerate_children_async (FileAttribute.STANDARD_NAME + "," + FileAttribute.STANDARD_TYPE, 0, 0);
 
325
                var files = yield enumerator.next_files_async (1024, 0);
 
326
                foreach (var f in files) {
 
327
                    unowned string name = f.get_name ();
 
328
                    if (f.get_file_type () == FileType.DIRECTORY) {
 
329
                        // FIXME: this could cause too many open files error, or?
 
330
                        var subdir = directory.get_child (name);
 
331
                        var new_prefix = "%s%s-".printf (id_prefix, subdir.get_basename ());
 
332
                        yield process_directory (subdir, new_prefix, monitored_dirs);
 
333
                    } else {
 
334
                        // ignore ourselves
 
335
                        if (name.has_suffix ("synapse.desktop")) {
 
336
                            continue;
 
337
                        }
 
338
                        if (name.has_suffix (".desktop")) {
 
339
                            yield load_desktop_file (directory.get_child (name), id_prefix);
 
340
                        }
 
341
                    }
 
342
                }
 
343
            } catch (Error err) {
 
344
                warning ("%s", err.message);
 
345
            }
 
346
        }
 
347
 
 
348
        private async void load_all_desktop_files () {
 
349
            string[] data_dirs = Environment.get_system_data_dirs ();
 
350
            data_dirs += Environment.get_user_data_dir ();
 
351
 
 
352
            Gee.Set<File> desktop_file_dirs = new Gee.HashSet<File> ();
 
353
            mimetype_parent_map.clear ();
 
354
 
 
355
            foreach (unowned string data_dir in data_dirs) {
 
356
                string dir_path = Path.build_filename (data_dir, "applications", null);
 
357
                var directory = File.new_for_path (dir_path);
 
358
                yield process_directory (directory, "", desktop_file_dirs);
 
359
                dir_path = Path.build_filename (data_dir, "mime", "subclasses");
 
360
                yield load_mime_parents_from_file (dir_path);
 
361
            }
 
362
 
 
363
            create_indices ();
 
364
 
 
365
            directory_monitors = new Gee.ArrayList<FileMonitor> ();
 
366
            foreach (File d in desktop_file_dirs) {
 
367
                try {
 
368
                    FileMonitor monitor = d.monitor_directory (0, null);
 
369
                    monitor.changed.connect (this.desktop_file_directory_changed);
 
370
                    directory_monitors.add (monitor);
 
371
                } catch (Error err) {
 
372
                    warning ("Unable to monitor directory: %s", err.message);
 
373
                }
 
374
            }
 
375
        }
 
376
 
 
377
        private uint timer_id = 0;
 
378
 
 
379
        public signal void reload_started ();
 
380
        public signal void reload_done ();
 
381
 
 
382
        private void desktop_file_directory_changed () {
 
383
            reload_started ();
 
384
            if (timer_id != 0) {
 
385
                Source.remove (timer_id);
 
386
            }
 
387
 
 
388
            timer_id = Timeout.add (5000, () => {
 
389
                timer_id = 0;
 
390
                reload_desktop_files.begin ();
 
391
                return false;
 
392
            });
 
393
        }
 
394
 
 
395
        private async void reload_desktop_files () {
 
396
            debug ("Reloading desktop files...");
 
397
            all_desktop_files.clear ();
 
398
            non_hidden_desktop_files.clear ();
 
399
            yield load_all_desktop_files ();
 
400
 
 
401
            reload_done ();
 
402
        }
 
403
 
 
404
        private async void load_desktop_file (File file, string id_prefix) {
 
405
            try {
 
406
                uint8[] file_contents;
 
407
                bool success = yield file.load_contents_async (null, out file_contents, null);
 
408
                if (success) {
 
409
                    var keyfile = new KeyFile ();
 
410
                    keyfile.load_from_data ((string) file_contents,
 
411
                    file_contents.length, 0);
 
412
 
 
413
                    var desktop_id = "%s%s".printf (id_prefix, file.get_basename ());
 
414
                    var dfi = new DesktopFileInfo.for_keyfile (file.get_path (), keyfile, desktop_id);
 
415
                    if (dfi.is_valid) {
 
416
                        all_desktop_files.add (dfi);
 
417
                        if (!dfi.is_hidden && session_type in dfi.show_in) {
 
418
                            non_hidden_desktop_files.add (dfi);
 
419
                        }
 
420
                    }
 
421
                }
 
422
            } catch (Error err) {
 
423
                warning ("%s", err.message);
 
424
            }
 
425
        }
 
426
 
 
427
        private void create_indices () {
 
428
            // create mimetype maps
 
429
            mimetype_map = new Gee.HashMap<unowned string, Gee.List<DesktopFileInfo> > ();
 
430
            // and exec map
 
431
            exec_map = new Gee.HashMap<string, Gee.List<DesktopFileInfo> > ();
 
432
            // and desktop id map
 
433
            desktop_id_map = new Gee.HashMap<string, DesktopFileInfo> ();
 
434
 
 
435
            Regex exec_re;
 
436
            try {
 
437
                exec_re = new Regex ("%[fFuU]");
 
438
            } catch (Error err) {
 
439
                        critical ("%s", err.message);
 
440
                        return;
 
441
            }
 
442
 
 
443
            foreach (var dfi in all_desktop_files) {
 
444
                        string exec = "";
 
445
                        try {
 
446
                                exec = exec_re.replace_literal (dfi.exec, -1, 0, "");
 
447
                        } catch (RegexError err) {
 
448
                                Utils.Logger.error (this, "%s", err.message);
 
449
                        }
 
450
                        exec = exec.strip ();
 
451
                        // update exec map
 
452
                        Gee.List<DesktopFileInfo>? exec_list = exec_map[exec];
 
453
                        if (exec_list == null) {
 
454
                                    exec_list = new Gee.ArrayList<DesktopFileInfo> ();
 
455
                                    exec_map[exec] = exec_list;
 
456
                        }
 
457
                        exec_list.add (dfi);
 
458
 
 
459
                        // update desktop id map
 
460
                        var desktop_id = dfi.desktop_id ?? Path.get_basename (dfi.filename);
 
461
                        desktop_id_map[desktop_id] = dfi;
 
462
 
 
463
                        // update mimetype map
 
464
                        if (dfi.is_hidden || dfi.mime_types == null) {
 
465
                                        continue;
 
466
                                }
 
467
 
 
468
                        foreach (unowned string mime_type in dfi.mime_types) {
 
469
                            Gee.List<DesktopFileInfo>? list = mimetype_map[mime_type];
 
470
                            if (list == null) {
 
471
                                list = new Gee.ArrayList<DesktopFileInfo> ();
 
472
                                mimetype_map[mime_type] = list;
 
473
                            }
 
474
                            list.add (dfi);
 
475
                        }
 
476
            }
 
477
        }
 
478
 
 
479
        private async void load_mime_parents_from_file (string fi) {
 
480
            var file = File.new_for_path (fi);
 
481
            bool exists = yield Utils.query_exists_async (file);
 
482
            if (!exists) {
 
483
                return;
 
484
            }
 
485
 
 
486
            try {
 
487
                var fis = yield file.read_async (GLib.Priority.DEFAULT);
 
488
                var dis = new DataInputStream (fis);
 
489
                string line = null;
 
490
                string[] mimes = null;
 
491
                int len = 0;
 
492
                // Read lines until end of file (null) is reached
 
493
                do {
 
494
                    line = yield dis.read_line_async (GLib.Priority.DEFAULT);
 
495
                    if (line == null) {
 
496
                        break;
 
497
                    }
 
498
                    if (line.has_prefix ("#")) {
 
499
                        continue; //comment line
 
500
                    }
 
501
                    mimes = line.split (" ");
 
502
                    len = (int)GLib.strv_length (mimes);
 
503
                    if (len != 2) {
 
504
                        continue;
 
505
                    }
 
506
                    // cannot be parent of myself!
 
507
                    if (mimes[0] == mimes[1]) {
 
508
                        continue;
 
509
                    }
 
510
                    //debug ("Map %s -> %s", mimes[0], mimes[1]);
 
511
                    mimetype_parent_map.set (mimes[0], mimes[1]);
 
512
                } while (true);
 
513
            } catch (GLib.Error err) {
 
514
                warning ("Can't read file.");
 
515
            }
 
516
        }
 
517
 
 
518
        private void add_dfi_for_mime (string mime, Gee.Set<DesktopFileInfo> ret) {
 
519
            var dfis = mimetype_map[mime];
 
520
            if (dfis != null) {
 
521
                ret.add_all (dfis);
 
522
            }
 
523
 
 
524
            var parents = mimetype_parent_map[mime];
 
525
            if (parents == null) {
 
526
                return;
 
527
            }
 
528
            foreach (string parent in parents) {
 
529
                add_dfi_for_mime (parent, ret);
 
530
            }
 
531
        }
 
532
 
 
533
        // retuns desktop files available on the system (without hidden ones)
 
534
        public Gee.List<DesktopFileInfo> get_desktop_files () {
 
535
            return non_hidden_desktop_files.read_only_view;
 
536
        }
 
537
 
 
538
        // returns all desktop files available on the system (even the ones which
 
539
        // are hidden by default)
 
540
        public Gee.List<DesktopFileInfo> get_all_desktop_files () {
 
541
            return all_desktop_files.read_only_view;
 
542
        }
 
543
 
 
544
        public Gee.List<DesktopFileInfo> get_desktop_files_for_type (string mime_type) {
 
545
            var dfi_set = new Gee.HashSet<DesktopFileInfo> ();
 
546
            add_dfi_for_mime (mime_type, dfi_set);
 
547
            var ret = new Gee.ArrayList<DesktopFileInfo> ();
 
548
            ret.add_all (dfi_set);
 
549
            return ret;
 
550
        }
 
551
 
 
552
        public Gee.List<DesktopFileInfo> get_desktop_files_for_exec (string exec) {
 
553
            return exec_map[exec] ?? new Gee.ArrayList<DesktopFileInfo> ();
 
554
        }
 
555
 
 
556
        public DesktopFileInfo? get_desktop_file_for_id (string desktop_id) {
 
557
            return desktop_id_map[desktop_id];
 
558
        }
 
559
    }
630
560
}