~ubuntu-desktop/unity-lens-files/ubuntu

« back to all changes in this revision

Viewing changes to src/daemon.vala

  • Committer: Didier Roche
  • Date: 2011-08-11 08:52:55 UTC
  • mfrom: (14.4.24 upstream)
  • Revision ID: didier.roche@canonical.com-20110811085255-jiqd2j7uf549mj24
* New upstream release.
* Fix some conflicts in src/config.vala and src/daemon.vala after
  merge-upstream. Get it back to sane values (no home dir)

Show diffs side-by-side

added added

removed removed

Lines of Context:
22
22
using Config;
23
23
using Gee;
24
24
 
25
 
namespace Unity.FilesPlace {
26
 
  
27
 
  const string ACTIVE_SECTION_HINT = "UnityActiveSection";
28
 
  const string EXTRA_ACTION_HINT = "UnityExtraAction";
 
25
namespace Unity.FilesLens {
29
26
  
30
27
  const string ICON_PATH = Config.DATADIR + "/icons/unity-icon-theme/places/svg/";
31
28
  
32
 
  /* Helper class used to encapsulate a state in a navigation sequence,
33
 
   * used as generic for our Unity.Place.Browser instance */
34
 
  private class BrowsingState
35
 
  {
36
 
    public Section section;
37
 
    public PlaceSearch? search;
38
 
    public string uri;    
39
 
  }
40
 
 
41
 
  public class Daemon : GLib.Object, Unity.Activation
 
29
  public class Daemon : GLib.Object
42
30
  {
43
31
    private Zeitgeist.Log log;
44
32
    private Zeitgeist.Index index;
47
35
    private Bookmarks bookmarks;
48
36
    private UrlChecker urls;
49
37
 
50
 
    private Unity.PlaceController control;
51
 
    private Unity.PlaceEntryInfo files;
52
 
    private Unity.Browser<BrowsingState> browser;
53
 
    
54
 
    /* We use the sections_model in normal operation, but when changing
55
 
     * to browsing mode we switch over to use pathbar_model as the sections
56
 
     * model. Magic in libunity will ensure us that the Unity Shell is notified
57
 
     * (over dbus) that we changed the sections model*/
58
 
    private Dee.SharedModel sections_model;
59
 
    private Dee.SharedModel pathbar_model;
60
 
    private string? browsing_uri = null;
61
 
    private Section browsing_root;
62
 
    
 
38
    private Unity.Lens lens;
 
39
    private Unity.Scope scope;
 
40
 
63
41
    /* For each section we have a set of Zeitgeist.Event templates that
64
42
     * we use to query Zeitgeist */
65
 
    private Gee.List<PtrArray> section_templates;
 
43
    private HashTable<string, Event> type_templates;
66
44
 
67
 
    /* Store a maping of DateMonth to Dee.ModelIter. We map to the iter and
68
 
     * not simply the offset, because in theory anyone on the bus could
69
 
     * update the Dee.SharedModel we use for the groups changing the row
70
 
     * offsets*/
71
 
    private Gee.List<unowned Dee.ModelIter?> months;
72
 
    
73
45
    /* Keep track of the previous search, so we can determine when to
74
46
     * filter down the result set instead of rebuilding it */
75
 
    private PlaceSearch? previous_search;
76
 
    
77
 
    /** Keep track of the previously active section and don't update
78
 
     * state if the section is in fact unchanged */
79
 
    private uint previous_active_section;
 
47
    private LensSearch? previous_search;
80
48
    
81
49
    private bool is_dirty;
82
 
    private bool all_models_synced;
83
50
    
84
 
    private Dee.Index entry_results_by_group;
85
 
 
86
51
    construct
87
52
    {
88
 
      sections_model = new Dee.SharedModel("com.canonical.Unity.FilesPlace.SectionsModel");
89
 
      sections_model.set_schema ("s", "s");
90
 
 
91
 
      pathbar_model = new Dee.SharedModel("com.canonical.Unity.FilesPlace.PathBarModel");
92
 
      pathbar_model.set_schema ("s", "s");
93
 
 
94
 
      var groups_model = new Dee.SharedModel("com.canonical.Unity.FilesPlace.GroupsModel");
95
 
      groups_model.set_schema ("s", "s", "s");
96
 
 
97
 
      var global_groups_model = new Dee.SharedModel("com.canonical.Unity.FilesPlace.GlobalGroupsModel");
98
 
      global_groups_model.set_schema ("s", "s", "s");
99
 
 
100
 
      var results_model = new Dee.SharedModel("com.canonical.Unity.FilesPlace.ResultsModel");
101
 
      results_model.set_schema ("s", "s", "u", "s", "s", "s");
102
 
 
103
 
      var global_results_model = new Dee.SharedModel("com.canonical.Unity.FilesPlace.GlobalResultsModel");
104
 
      global_results_model.set_schema ("s", "s", "u", "s", "s", "s");
105
 
 
106
 
      section_templates = new Gee.ArrayList<PtrArray> ();
107
 
      prepare_section_templates();
108
 
 
109
 
      files = new PlaceEntryInfo ("/com/canonical/unity/filesplace/files");
110
 
      files.sections_model = sections_model;
111
 
      files.entry_renderer_info.groups_model = groups_model;
112
 
      files.entry_renderer_info.results_model = results_model;
113
 
      files.global_renderer_info.groups_model = global_groups_model;
114
 
      files.global_renderer_info.results_model = global_results_model;      
115
 
 
116
 
      files.icon = @"$(Config.PREFIX)/share/unity/themes/files.png";
 
53
      prepare_type_templates();
 
54
 
 
55
      scope = new Unity.Scope ("/com/canonical/unity/scope/files");
 
56
      scope.search_in_global = true;
 
57
      scope.activate_uri.connect (activate);
 
58
 
 
59
      lens = new Unity.Lens("/com/canonical/unity/lens/files", "files");
 
60
      lens.search_in_global = true;
 
61
      lens.search_hint = _("Search Files & Folders");
 
62
      lens.visible = true;
 
63
      populate_categories ();
 
64
      populate_filters();
 
65
      lens.add_local_scope (scope);
117
66
      
118
67
      previous_search = null;
119
 
      previous_active_section = Section.LAST_SECTION; /* Must be an illegal section! */
120
68
      
121
69
      is_dirty = true;
122
70
      
123
 
      var analyzer = new Dee.Analyzer.for_uint32_column (ResultsColumn.GROUP_ID);
124
 
      entry_results_by_group = new Dee.HashIndex (results_model,
125
 
                                                  analyzer);
126
 
 
127
71
      /* Bring up Zeitgeist interfaces */
128
72
      log = new Zeitgeist.Log();
129
73
      index = new Zeitgeist.Index();
144
88
      bookmarks = new Bookmarks ();
145
89
      urls = new UrlChecker ();
146
90
 
147
 
      /* Listen for section changes */
148
 
      files.notify["active-section"].connect (
149
 
        (obj, pspec) => {
150
 
          if (!files.active)
151
 
            return;
152
 
          
153
 
          if (!all_models_synced)
154
 
            return;
155
 
          
156
 
          var section = (Section)files.active_section;
157
 
          var _results_model = files.entry_renderer_info.results_model;
158
 
          var _groups_model = files.entry_renderer_info.groups_model;
159
 
          
160
 
          if (!(is_dirty || previous_active_section != files.active_section))
161
 
            {
162
 
              return;
163
 
            }
164
 
          
165
 
          is_dirty = false;
166
 
          
167
 
          if (search_is_invalid (files.active_search))
168
 
            {
169
 
              int group_override = section == Section.ALL_FILES ?
170
 
                                                           Group.RECENT : -1;
171
 
              update_without_search_async.begin(section,
172
 
                                                _results_model, _groups_model,
173
 
                                                group_override);
174
 
            }
175
 
          else
176
 
            {
177
 
              update_search_async.begin (files.active_search, section,
178
 
                                         _results_model, _groups_model,
179
 
                                         false, entry_results_by_group);
180
 
            }
181
 
          
182
 
          previous_active_section = files.active_section;
 
91
      /* Listen for filter changes */
 
92
      scope.filters_changed.connect (
 
93
        () => {          
 
94
          if (scope.active_search != null)
 
95
            {
 
96
              is_dirty = true;
 
97
              scope.notify_property ("active-search");
 
98
            }
183
99
        }
184
100
      );
185
101
 
186
 
      /* Listen for changes to the place entry search */
187
 
      files.notify["active-search"].connect (
 
102
      /* Listen for changes to the lens entry search */
 
103
      scope.notify["active-search"].connect (
188
104
        (obj, pspec) => {
189
 
          var search = files.active_search;
190
 
          
191
 
          if (!all_models_synced)
192
 
            return;
193
 
          
194
 
          if (!files.active)
195
 
            return;
196
 
          
197
 
          var _results_model = files.entry_renderer_info.results_model;
198
 
          var _groups_model = files.entry_renderer_info.groups_model;
199
 
          var section = (Section) files.active_section;
 
105
          var search = scope.active_search;
200
106
          
201
107
          if (!(Utils.search_has_really_changed (previous_search, search) || is_dirty))
202
108
            return;
203
109
          
204
110
          is_dirty = false;
205
111
          
206
 
          if (search_is_invalid (files.active_search))
 
112
          if (search_is_invalid (search))
207
113
            {
208
 
              int group_override = section == Section.ALL_FILES ?
209
 
                                                           Group.RECENT : -1;
210
 
              update_without_search_async.begin(section,
211
 
                                                _results_model, _groups_model,
212
 
                                                group_override);
 
114
              update_without_search_async.begin(search);
213
115
            }
214
116
          else
215
117
            {
216
 
              update_search_async.begin (search, section,
217
 
                                         _results_model, _groups_model,
218
 
                                         Utils.check_is_filter_search(search,
219
 
                                                                      previous_search),
220
 
                                         entry_results_by_group);
 
118
              update_search_async.begin (search);
221
119
             }
222
120
          previous_search = search;
223
121
        }
224
122
      );
225
123
 
226
124
      /* Listen for changes to the global search */
227
 
      files.notify["active-global-search"].connect (
 
125
      scope.notify["active-global-search"].connect (
228
126
        (obj, pspec) => {
229
 
          if (!all_models_synced)
230
 
            return;
231
127
 
232
 
          var search = files.active_global_search;
233
 
          var _results_model = files.global_renderer_info.results_model;
234
 
          var _groups_model = files.global_renderer_info.groups_model;
 
128
          var search = scope.active_global_search;
235
129
          
236
130
          if (search_is_invalid (search))
237
131
            return;
239
133
          if (!Utils.search_has_really_changed (previous_search, search))
240
134
            return;
241
135
          
242
 
          update_global_search_async.begin (search,
243
 
                                            _results_model,
244
 
                                            _groups_model,
245
 
                                            Utils.check_is_filter_search(search, previous_search));
 
136
          update_global_search_async.begin (search);
246
137
          previous_search = search;
247
138
        }
248
139
      );
249
 
      
250
 
      /* Listen for when the place is hidden by the Unity Shell, and reset
251
 
       * all state when we are deactivated */
252
 
      files.notify["active"].connect (
253
 
        (obj, pspec) => {
254
 
          debug (@"Activated: $(files.active)");
255
 
          if (files.active && is_dirty)
256
 
            {
257
 
              files.notify_property ("active-section");
258
 
            }
259
 
          else
260
 
            {
261
 
              reset_browsing_state ();
262
 
            }
263
 
        }
264
 
      );           
265
 
 
266
 
      /* The last thing we do is export the controller. Once that is up,
267
 
       * clients will expect the SharedModels to work */
268
 
      control = new Unity.PlaceController ("/com/canonical/unity/filesplace");
269
 
      control.add_entry (files);
270
 
      // control.activation = this; // <-- folder browsing disabled
271
 
 
272
 
      try {
273
 
        control.export ();
274
 
      } catch (IOError error) {
275
 
        critical ("Failed to export DBus service for '%s': %s",
276
 
                  control.dbus_path, error.message);
277
 
      }
278
 
      
279
 
      /* The browser will automagically be exported/unexported on the bus
280
 
       * when we set/unset the 'browser' property on the 'files' EntryInfo.
281
 
       * Likewise, setting the browser also sets the UnityPlaceBrowserPath
282
 
       * and UnitySectionStyle hints accordingly.
283
 
       * Such works the magic of the libunity API :-) */
284
 
      browser = new Unity.Browser<BrowsingState> (
285
 
                                     "/com/canonical/unity/filesplace/browser");
286
 
      browser.back.connect (
287
 
        (browser, state, comment) => {
288
 
          debug ("Go back to: %s", (state as BrowsingState).uri);
289
 
          var f = File.new_for_uri ((state as BrowsingState).uri);
290
 
          browse_folder.begin (f);
291
 
        }
292
 
      );
293
 
      
294
 
      browser.forward.connect (
295
 
        (browser, state, comment) => {
296
 
          debug ("Go forward to: %s", (state as BrowsingState).uri);
297
 
          var f = File.new_for_uri ((state as BrowsingState).uri);
298
 
          browse_folder.begin (f);
299
 
        }
300
 
      );
301
 
      
302
 
      /* We should not start manipulating any of our models before they are
303
 
       * all synchronized. When they are we set all_models_synced = true */
304
 
      sections_model.notify["synchronized"].connect (check_models_synced);
305
 
      pathbar_model.notify["synchronized"].connect (check_models_synced);
306
 
      groups_model.notify["synchronized"].connect (check_models_synced);
307
 
      global_groups_model.notify["synchronized"].connect (check_models_synced);
308
 
      results_model.notify["synchronized"].connect (check_models_synced);
309
 
      global_results_model.notify["synchronized"].connect (check_models_synced);
310
 
      all_models_synced = false;
311
 
    }
312
 
 
313
 
    /* The check_models_synced() method acts like a latch - once all models
314
 
     * have reported themselves to be synchronized we set
315
 
     * all_models_synced = true and tell the searches to re-check their state
316
 
     * as they should refuse to run when all_models_synced == false */
317
 
    private void check_models_synced (Object obj, ParamSpec pspec)
318
 
    {
319
 
      if ((sections_model as Dee.SharedModel).synchronized &&
320
 
          (pathbar_model as Dee.SharedModel).synchronized &&
321
 
          (files.entry_renderer_info.groups_model as Dee.SharedModel).synchronized &&
322
 
          (files.entry_renderer_info.results_model as Dee.SharedModel).synchronized &&
323
 
          (files.global_renderer_info.groups_model as Dee.SharedModel).synchronized &&
324
 
          (files.global_renderer_info.results_model as Dee.SharedModel).synchronized) {
325
 
        if (all_models_synced == false)
326
 
          {
327
 
            all_models_synced = true;
328
 
            
329
 
            populate_sections ();
330
 
            populate_groups ();
331
 
            
332
 
            /* Emitting notify here will make us recheck of the search results
333
 
             * need update. In the negative case this is a noop */
334
 
            files.notify_property ("active-search");
335
 
            files.notify_property ("active-global-search");
336
 
          }
337
 
      }
338
 
    }
339
 
 
340
 
    private void populate_sections ()
341
 
    {
342
 
      var sections = files.sections_model;
343
 
 
344
 
      if (sections.get_n_rows() != 0)
345
 
        {
346
 
          debug ("Sections model already populated. We probably cloned it off Unity. Resetting it.");
347
 
          sections.clear ();
348
 
        }
349
 
 
350
 
      sections.append (_("All Files"), "");
351
 
      sections.append ( _("Documents"), "");
352
 
      sections.append (_("Folders"), "");
353
 
      sections.append (_("Images"), "");
354
 
      sections.append (_("Audio"), "");
355
 
      sections.append (_("Videos"), "");
356
 
      sections.append (_("Presentations"), "");
357
 
      sections.append (_("Other"), "");
358
 
    }
359
 
 
360
 
    private void populate_groups ()
361
 
    {
362
 
      var groups = files.entry_renderer_info.groups_model;
363
 
 
364
 
      if (groups.get_n_rows() != 0)
365
 
        {
366
 
          debug ("Groups model already populated. We probably cloned it off Unity. Resetting it.");
367
 
          groups.clear ();
368
 
        }
369
 
 
370
 
      /* Always expand the Favorite Folders group */
371
 
      files.entry_renderer_info.set_hint ("ExpandedGroups",
372
 
                                          @"$((uint)Group.FAVORITE_FOLDERS)");
373
 
 
374
 
      groups.append ("UnityFileInfoRenderer",
375
 
                     _("Top Results"),
376
 
                     ICON_PATH + "group-mostused.svg");
377
 
      groups.append ("UnityDefaultRenderer",
378
 
                     _("Recent"),
379
 
                     ICON_PATH + "group-recent.svg");
380
 
      groups.append ("UnityDefaultRenderer",
381
 
                     _("Downloads"),
382
 
                     ICON_PATH + "group-downloads.svg");
383
 
      groups.append ("UnityDefaultRenderer",
384
 
                     _("Favorite Folders"),
385
 
                     ICON_PATH + "group-favoritefolders.svg");
386
 
      groups.append ("UnityDefaultRenderer",
387
 
                     _("Files"),
388
 
                     ICON_PATH + "group-recent.svg");
389
 
      groups.append ("UnityEmptySearchRenderer",
390
 
                     "No search results", /* No i18n. Should never be rendered */
391
 
                     "");
392
 
      groups.append ("UnityEmptySectionRenderer",
393
 
                     "Empty section", /* No i18n. Should never be rendered */
394
 
                     "");
395
 
      groups.append ("UnityFileInfoRenderer",
396
 
                     _("Today"),
397
 
                     ICON_PATH + "group-daterange.svg");
398
 
      groups.append ("UnityFileInfoRenderer",
399
 
                     _("Yesterday"),
400
 
                     ICON_PATH + "group-daterange.svg");
401
 
      groups.append ("UnityFileInfoRenderer",
402
 
                     _("This week"),
403
 
                     ICON_PATH + "group-daterange.svg");
404
 
      groups.append ("UnityFileInfoRenderer",
405
 
                     _("Last Week"),
406
 
                     ICON_PATH + "group-daterange.svg");
407
 
      groups.append ("UnityFileInfoRenderer",
408
 
                     _("This Month"),
409
 
                     ICON_PATH + "group-daterange.svg");
410
 
      groups.append ("UnityFileInfoRenderer",
411
 
                     _("Past Six Months"),
412
 
                     ICON_PATH + "group-daterange.svg");
413
 
      groups.append ("UnityFileInfoRenderer",
414
 
                     _("This Year"),
415
 
                     ICON_PATH + "group-daterange.svg");
416
 
      groups.append ("UnityFileInfoRenderer",
417
 
                     _("Last Year"),
418
 
                     ICON_PATH + "group-daterange.svg");
419
 
      // FIXME: For prehistoric items use actual year, eg "2009"
420
 
 
421
 
 
422
 
      months = new Gee.ArrayList<unowned Dee.ModelIter?>();
423
 
      months.add(null);
424
 
      for (uint i = 1; i <= DateMonth.DECEMBER; i++)
425
 
      {
426
 
        unowned Dee.ModelIter iter = groups.append ("UnityFileInfoRenderer",
427
 
                                                    Utils.get_month_name ((DateMonth)i),
428
 
                                                    ICON_PATH + "group-daterange.svg");
429
 
        months.add(iter);
430
 
 
431
 
      }
432
 
      
433
 
      /* We only need the global groups up to Group.FILES */
434
 
      var global_groups = files.global_renderer_info.groups_model;
435
 
      
436
 
      if (global_groups.get_n_rows() != 0)
437
 
        {
438
 
          debug ("Global groups model already populated. We probably cloned it off Unity. Resetting it.");
439
 
          global_groups.clear ();
440
 
        }
441
 
      
442
 
      global_groups.append ("UnityFileInfoRenderer",
443
 
                            _("Top Results"),
444
 
                            ICON_PATH + "group-mostused.svg");
445
 
      global_groups.append ("UnityDefaultRenderer",
446
 
                            _("Recent"),
447
 
                            ICON_PATH + "group-recent.svg");
448
 
      global_groups.append ("UnityDefaultRenderer",
449
 
                            _("Downloads"),
450
 
                            ICON_PATH + "group-downloads.svg");
451
 
      global_groups.append ("UnityDefaultRenderer",
452
 
                            _("Folders"),
453
 
                            ICON_PATH + "group-favoritefolders.svg");
454
 
      global_groups.append ("UnityDefaultRenderer",
455
 
                            _("Files"),
456
 
                            ICON_PATH + "group-recent.svg");
457
 
    }
458
 
 
459
 
    private void prepare_section_templates ()
460
 
    {
461
 
      PtrArray templates;
 
140
     
 
141
      lens.export ();
 
142
    }
 
143
 
 
144
    private void populate_filters ()
 
145
    {
 
146
      Unity.Filter[] filters = {};
 
147
 
 
148
      /* Last modified */
 
149
      {
 
150
        var filter = new RadioOptionFilter ("modified", _("Last modified"));
 
151
 
 
152
        filter.add_option ("last-7-days", _("Last 7 days"));
 
153
        filter.add_option ("last-30-days", _("Last 30 days"));
 
154
        filter.add_option ("last-year", _("Last year"));
 
155
 
 
156
        filters += filter;
 
157
      }
 
158
 
 
159
      /* Type filter */
 
160
      {
 
161
        var filter = new RadioOptionFilter ("type", _("Type"));
 
162
 
 
163
        filter.add_option ("documents", _("Documents"));
 
164
        filter.add_option ("folders", _("Folders"));
 
165
        filter.add_option ("images", _("Images"));
 
166
        filter.add_option ("audio", _("Audio"));
 
167
        filter.add_option ("videos", _("Videos"));
 
168
        filter.add_option ("presentations", _("Presentations"));
 
169
        filter.add_option ("other", _("Other"));
 
170
 
 
171
        filters += filter;
 
172
      }
 
173
 
 
174
      /* Size filter */
 
175
      {
 
176
        var filter = new MultiRangeFilter ("size", _("Size"));
 
177
 
 
178
        filter.add_option ("1KB", _("1KB"));
 
179
        filter.add_option ("100KB", _("100KB"));
 
180
        filter.add_option ("1MB", _("1MB"));
 
181
        filter.add_option ("10MB", _("10MB"));
 
182
        filter.add_option ("100MB", _("100MB"));
 
183
        filter.add_option ("1GB", _("1GB"));
 
184
        filter.add_option (">1GB", _(">1GB"));
 
185
 
 
186
        filters += filter;
 
187
      }
 
188
 
 
189
      lens.filters = filters;
 
190
    }
 
191
 
 
192
    private void populate_categories ()
 
193
    {
 
194
      Unity.Category[] categories = {};
 
195
 
 
196
      var cat = new Unity.Category (_("Recent"), ICON_PATH + "category-recent.svg");
 
197
      categories += cat;
 
198
 
 
199
      cat =  new Unity.Category (_("Downloads"), ICON_PATH + "category-downloads.svg");
 
200
      categories += cat;
 
201
 
 
202
      cat = new Unity.Category (_("Folders"), ICON_PATH + "category-favoritefolders.svg");
 
203
      categories += cat;
 
204
 
 
205
      lens.categories = categories;
 
206
    }
 
207
 
 
208
    private void prepare_type_templates ()
 
209
    {
 
210
      type_templates = new HashTable<string, Event> (str_hash, str_equal);
462
211
      Event event;
463
212
 
464
213
      /* HACK ALERT: All the (event as GLib.Object).ref() are needed because
465
214
       *             GPtrArray doesn't grab a ref to the event objects */
466
215
 
467
216
      /* Section.ALL_FILES */
468
 
      templates = new PtrArray.sized(1);
469
217
      event = new Event.full("", ZG_USER_ACTIVITY, "",
470
218
                             new Subject.full ("file:*",
471
219
                                               "", "", "", "", "", ""));
472
 
      templates.add ((event as GLib.Object).ref());
473
 
      section_templates.add (templates);
474
 
 
 
220
      type_templates.insert ("all", (event as GLib.Object).ref() as Event);
 
221
      
475
222
      /* Section.DOCUMENTS
476
223
       * FIXME: Filter out presentations: https://bugs.launchpad.net/zeitgeist/+bug/592599 */
477
 
      templates = new PtrArray.sized(1);
478
224
      event = new Event.full("", ZG_USER_ACTIVITY, "",
479
225
                             new Subject.full ("file:*",
480
226
                                               NFO_DOCUMENT,
481
227
                                               "", "", "", "", ""));
482
 
      templates.add ((event as GLib.Object).ref());
483
 
      section_templates.add (templates);
 
228
      type_templates.insert ("documents", (event as GLib.Object).ref() as Event);
484
229
 
485
230
      /* Section.FOLDERS.
486
231
       * FIXME: We probably need to be clever here and use something
487
 
       *       like subject.origin in stead of NFO_FOLDER */
488
 
      templates = new PtrArray.sized(1);
 
232
       *       like subject.origin in stead of NFO_FOLDERS */
489
233
      event = new Event.full("", ZG_USER_ACTIVITY, "",
490
234
                             new Subject.full ("file:*",
491
235
                                               "", "", "", "", "", ""));
492
 
      templates.add ((event as GLib.Object).ref());
493
 
      section_templates.add (templates);
 
236
      type_templates.insert ("folders", (event as GLib.Object).ref() as Event);
494
237
 
495
238
      /* Section.IMAGES */
496
 
      templates = new PtrArray.sized(1);
497
239
      event = new Event.full("", ZG_USER_ACTIVITY, "",
498
240
                             new Subject.full ("file:*",
499
241
                                               NFO_IMAGE, "", "", "", "", ""));
500
 
      templates.add ((event as GLib.Object).ref());
501
 
      section_templates.add (templates);
 
242
      type_templates.insert ("images", (event as GLib.Object).ref() as Event);
502
243
      
503
244
      /* Section.AUDIO */
504
 
      templates = new PtrArray.sized(1);
505
245
      event = new Event.full("", ZG_USER_ACTIVITY, "",
506
246
                             new Subject.full ("file:*",
507
247
                                               NFO_AUDIO, "", "", "", "", ""));
508
 
      templates.add ((event as GLib.Object).ref());
509
 
      section_templates.add (templates);
 
248
      type_templates.insert ("audio", (event as GLib.Object).ref() as Event);
510
249
 
511
250
      /* Section.VIDEOS */
512
 
      templates = new PtrArray.sized(1);
513
251
      event = new Event.full("", ZG_USER_ACTIVITY, "",
514
252
                             new Subject.full ("file:*",
515
253
                                               NFO_VIDEO, "", "", "", "", ""));
516
 
      templates.add ((event as GLib.Object).ref());
517
 
      section_templates.add (templates);
 
254
      type_templates.insert ("videos", (event as GLib.Object).ref() as Event);
518
255
 
519
256
      /* Section.PRESENTATIONS
520
257
       * FIXME: Zeitgeist logger needs to user finer granularity
521
258
       *        on classification as I am not sure it uses
522
259
       *        NFO_PRESENTATION yet */
523
 
      templates = new PtrArray.sized(1);
524
260
      event = new Event.full("", ZG_USER_ACTIVITY, "",
525
261
                             new Subject.full ("file:*",
526
262
                                               NFO_PRESENTATION, "", "", "", "", ""));
527
 
      templates.add ((event as GLib.Object).ref());
528
 
      section_templates.add (templates);
 
263
      type_templates.insert ("presentations", (event as GLib.Object).ref() as Event);
529
264
 
530
265
      /* Section.OTHER 
531
266
       * Note that subject templates are joined with logical AND */
532
 
      templates = new PtrArray.sized(1);
533
267
      event = new Event.full("", ZG_USER_ACTIVITY, "");
534
268
      event.add_subject (new Subject.full ("file:*",
535
269
                                           "!"+NFO_DOCUMENT, "", "", "", "", ""));
549
283
                                           "!"+NFO_PRESENTATION,
550
284
                                           "",
551
285
                                           "", "", "", ""));
552
 
      templates.add ((event as GLib.Object).ref());
553
 
      section_templates.add (templates);
 
286
      type_templates.insert ("other", (event as GLib.Object).ref() as Event);
554
287
    }
555
288
 
556
 
    private bool search_is_invalid (PlaceSearch? search)
 
289
    private bool search_is_invalid (LensSearch? search)
557
290
    {
558
291
      /* This boolean expression is unfolded as we seem to get
559
292
       * some null dereference if we join them in a big || expression */
560
293
      if (search == null)
561
294
        return true;
562
 
      else if (search.get_search_string () == null)
 
295
      else if (search.search_string == null)
563
296
        return true;
564
297
      
565
 
      return search.get_search_string ().strip() == "";
 
298
      return search.search_string.strip() == "";
566
299
    }
567
300
 
568
 
    private string prepare_search_string (PlaceSearch? search)
 
301
    private string prepare_search_string (LensSearch? search)
569
302
    {
570
 
      var s = search.get_search_string ();
 
303
      var s = search.search_string;
571
304
 
572
305
      if (s.has_suffix (" "))
573
306
        s = s.strip ();
585
318
      return s;
586
319
    }
587
320
    
588
 
    private async void update_global_search_async (PlaceSearch search,
589
 
                                                   Dee.Model results_model,
590
 
                                                   Dee.Model groups_model,
591
 
                                                   bool is_filter_search)
 
321
    private async void update_global_search_async (LensSearch search)
592
322
    {
 
323
      var results_model = scope.global_results_model;
 
324
 
593
325
      if (search_is_invalid (search))
594
326
        {
595
327
          results_model.clear ();
599
331
      /* Prevent concurrent searches and concurrent updates of our models,
600
332
       * by preventing any notify signals from propagating to us.
601
333
       * Important: Remeber to thaw the notifys again! */
602
 
      files.freeze_notify ();
 
334
      scope.freeze_notify ();
603
335
      
604
336
      var search_string = prepare_search_string (search);
605
 
      var templates = section_templates.get((int)Section.ALL_FILES);
606
 
      
 
337
 
 
338
      var templates = new PtrArray.sized(1);
 
339
      templates.add (type_templates.lookup ("all"));
 
340
 
607
341
      try {
608
342
        /* Get results ranked by recency */
609
343
        var timer = new Timer ();
614
348
                                          20,
615
349
                                          ResultType.MOST_RECENT_SUBJECTS,
616
350
                                          null);
617
 
        
 
351
 
618
352
        timer.stop ();
619
353
        debug ("Found %u/%u global results for search '%s' in %fms",
620
354
               results.size (), results.estimated_matches (),
621
355
               search_string, timer.elapsed()*1000);
622
356
 
623
357
 
624
 
        var bookmark_matches = bookmarks.prefix_search (search.get_search_string());
625
 
        var checked_url = urls.check_url (search.get_search_string());
626
 
 
627
 
        /* Clean up results model */
628
 
        if (!is_filter_search)
629
 
          {
630
 
            /* Don't clear the model before the first results are ready */
631
 
            results_model.clear ();
632
 
            
633
 
            if (checked_url != null)
634
 
              {
635
 
                results_model.append (checked_url, urls.icon, Group.FILES,
636
 
                                      "text/html", search.get_search_string(),
637
 
                                      checked_url);
638
 
              }
639
 
            
640
 
            append_bookmarks (bookmark_matches, results_model, Group.FILES);
641
 
            
642
 
            Unity.FilesPlace.append_events_sorted (results,
643
 
                                                   results_model,
644
 
                                                   groups_model,
645
 
                                                   Section.ALL_FILES,
646
 
                                                   Group.FILES);
647
 
          }
648
 
        else
649
 
          {
650
 
            /* Remove all rows from the results_model that are not
651
 
             * in the new result set or matching bookmarks */
652
 
            Set<string> valid_uris = Utils.get_uri_set (results);
653
 
            
654
 
            foreach (var bookmark in bookmark_matches)
655
 
              {
656
 
                valid_uris.add (bookmark.uri);
657
 
              }
658
 
            
659
 
            unowned ModelIter iter = results_model.get_first_iter();
660
 
            unowned ModelIter end = results_model.get_last_iter ();
661
 
            unowned ModelIter current_row;
662
 
            string result_uri;
663
 
            while (iter != end)
664
 
              {
665
 
                result_uri = results_model.get_string (iter, ResultsColumn.URI);
666
 
                current_row = iter;
667
 
                iter = results_model.next (iter);
668
 
                if (!(result_uri in valid_uris))
669
 
                  results_model.remove (current_row);
670
 
              }
671
 
            
672
 
            /* A possibly checked URL will be filtered out by now,
673
 
             * So we *prepend* it, to add it on the first spot
674
 
             * if we have one */
675
 
            if (checked_url != null)
676
 
              {
677
 
                results_model.prepend (checked_url, urls.icon, Group.FILES,
678
 
                                       "text/html", search.get_search_string(),
679
 
                                       checked_url);
680
 
              }
681
 
          }
 
358
        results_model.clear ();
 
359
 
 
360
        var checked_url = urls.check_url (search.search_string);
 
361
        if (checked_url != null)
 
362
          {
 
363
            results_model.append (checked_url, urls.icon, Categories.RECENT,
 
364
                                  "text/html", search.search_string,
 
365
                                  checked_url);
 
366
          }
 
367
        var bookmark_matches = bookmarks.prefix_search (search.search_string);
 
368
        append_bookmarks (bookmark_matches, results_model, Categories.FOLDERS);
 
369
        
 
370
        Unity.FilesLens.append_events_sorted (results, results_model);
 
371
 
682
372
      } catch (GLib.Error e) {
683
373
        warning ("Error performing global search '%s': %s",
684
 
                 search.get_search_string (), e.message);
 
374
                 search.search_string, e.message);
685
375
      }
686
376
      
687
377
      /* Allow new searches once we enter an idle again.
689
379
       * changing the model even before we had flushed out current changes
690
380
       */
691
381
      Idle.add (() => {
692
 
        files.thaw_notify ();
 
382
        scope.thaw_notify ();
693
383
        return false;
694
384
      });
695
385
      
696
386
      search.finished ();
697
387
    }
698
388
    
699
 
    private async void update_search_async  (PlaceSearch search,
700
 
                                             Section section,
701
 
                                             Dee.Model results_model,
702
 
                                             Dee.Model groups_model,
703
 
                                             bool is_filter_search,
704
 
                                             Dee.Index results_by_group,
705
 
                                             int group_override = -1)
 
389
    private async void update_search_async  (LensSearch search)
706
390
    {
707
 
      Timer full_timer = new Timer ();
708
 
      
709
 
      // FIXME: Implement in-folder searching
710
 
      if (files.browser != null)
711
 
        {
712
 
          warning ("In-folder searching not implemented yet");
713
 
          return;
714
 
        }
715
 
      
716
391
      if (search_is_invalid (search))
717
392
        {
718
 
          update_without_search_async.begin (section,
719
 
                                             results_model,
720
 
                                             groups_model);
 
393
          update_without_search_async.begin (search);
721
394
          return;
722
395
        }
723
 
      
 
396
 
 
397
      var results_model = scope.results_model;
 
398
 
724
399
      /* Prevent concurrent searches and concurrent updates of our models,
725
400
       * by preventing any notify signals from propagating to us.
726
401
       * Important: Remeber to thaw the notifys again! */
727
 
      files.freeze_notify ();
 
402
      scope.freeze_notify ();
728
403
      
729
404
      var search_string = prepare_search_string (search);
730
 
      var templates = section_templates.get((int)section);  
731
 
      
 
405
 
 
406
      string type_id = get_current_type ();
 
407
 
 
408
      var result_type = type_id == "folder" ? ResultType.MOST_RECENT_ORIGIN
 
409
                                            : ResultType.MOST_RECENT_SUBJECTS;
 
410
 
 
411
      /* Grab the pre-compiled template we're going to use */
 
412
      var templates = new PtrArray.sized(1);
 
413
      templates.add (type_templates.lookup (type_id));
 
414
 
 
415
 
732
416
      try {
733
 
        /* Get relevancy ranked results for the "Top Results" group */
 
417
        /* Get results ranked by recency */
734
418
        var timer = new Timer ();
735
419
        var results = yield index.search (search_string,
736
 
                                          new Zeitgeist.TimeRange.anytime(),
 
420
                                          get_current_timerange (),
737
421
                                          templates,
738
422
                                          Zeitgeist.StorageState.ANY,
739
423
                                          50,
740
 
                                          Zeitgeist.ResultType.RELEVANCY,
 
424
                                          result_type,
741
425
                                          null);
742
 
        
743
 
        timer.stop ();
744
 
        debug ("Found %u/%u Top Results for search '%s' in %fms",
745
 
               results.size (), results.estimated_matches (),
746
 
               search_string, timer.elapsed()*1000);
747
 
 
748
 
        /* Clean up results model */
749
 
        if (!is_filter_search)
750
 
          {
751
 
            /* Don't clear the results before the first ones are ready */
752
 
            results_model.clear ();
753
 
          }
754
 
        else
755
 
          {
756
 
            /* Remove everything in the Top Results group. We can not do filter
757
 
             * searches here as it's sorted by relevancy alone */
758
 
            uint group = Group.TOP_RESULTS;
759
 
            var top_results = results_by_group.lookup (@"$group",
760
 
                                                       TermMatchFlag.EXACT);
761
 
            foreach (var row in top_results)
762
 
              {
763
 
                results_model.remove (row);
764
 
              }
765
 
          }
766
 
        
767
 
        /* First add any matching folder bookmarks.
768
 
         * Note: This only works without dupes on incremental search
769
 
         *       because the folders are in the Top Results group which we
770
 
         *       always clean out on each update */
771
 
        var bookmark_matches = bookmarks.prefix_search (search.get_search_string());
772
 
        append_bookmarks (bookmark_matches, results_model, Group.TOP_RESULTS);
773
 
        
774
 
        var seen_uris = Unity.FilesPlace.append_events_sorted (results,
775
 
                                                               results_model,
776
 
                                                               groups_model,
777
 
                                                               section,
778
 
                                                               Group.TOP_RESULTS);
779
 
        
780
 
        /* Get time-grouped results */
781
 
        var result_type = section == Section.FOLDERS ?
782
 
                               ResultType.MOST_RECENT_ORIGIN : 
783
 
                               ResultType.MOST_RECENT_SUBJECTS;
784
 
        timer.start ();
785
 
        results = yield index.search (search_string,
786
 
                                      new Zeitgeist.TimeRange.anytime(),
787
 
                                      templates,
788
 
                                      Zeitgeist.StorageState.ANY,
789
 
                                      100,
790
 
                                      result_type,
791
 
                                      null);
792
 
        
793
 
        timer.stop ();
794
 
        debug ("Found %u/%u time grouped results for search '%s' in %fms",
795
 
               results.size (), results.estimated_matches (),
796
 
               search_string, timer.elapsed()*1000);
797
 
        
798
 
        if (!is_filter_search)
799
 
          {
800
 
            Unity.FilesPlace.append_events_sorted (results,
801
 
                                                   results_model, groups_model,
802
 
                                                   section,
803
 
                                                   group_override,
804
 
                                                   seen_uris);
805
 
          }
806
 
        else
807
 
          {
808
 
            /* We must filter all groups except Top Results and we should
809
 
             * not include results that are already in Top Results
810
 
             * IMPORTANT: This code assumes Group.TOP_RESULTS is the very first
811
 
             *            and that Group.RECENT is the second group! */
812
 
            Set<string> timegrouped_uris = Utils.get_uri_set (results);
813
 
            timegrouped_uris.remove_all (seen_uris);
814
 
            uint n_groups = groups_model.get_n_rows ();
815
 
            for (uint group_ = Group.RECENT; group_ < n_groups; group_++)
816
 
              Utils.apply_uri_filter (timegrouped_uris,
817
 
                                      results_by_group.lookup (@"$group_",
818
 
                                                               TermMatchFlag.EXACT));
819
 
          }
 
426
 
 
427
        timer.stop ();
 
428
        debug ("Found %u/%u results for search '%s' in %fms",
 
429
               results.size (), results.estimated_matches (),
 
430
               search_string, timer.elapsed()*1000);
 
431
 
 
432
        results_model.clear ();
 
433
 
 
434
        var checked_url = urls.check_url (search.search_string);
 
435
        if (checked_url != null)
 
436
          {
 
437
            results_model.append (checked_url, urls.icon, Categories.RECENT,
 
438
                                  "text/html", search.search_string,
 
439
                                  checked_url);
 
440
          }
 
441
 
 
442
        var bookmark_matches = bookmarks.prefix_search (search.search_string);
 
443
        append_bookmarks (bookmark_matches, results_model, Categories.FOLDERS);
 
444
 
 
445
        /* FIXME: Add downloads */
 
446
        
 
447
        Unity.FilesLens.append_events_sorted (results, results_model);
820
448
 
821
449
      } catch (GLib.Error e) {
822
 
        warning ("Error performing search '%s': %s",
823
 
                 search.get_search_string (), e.message);
 
450
        warning ("Error performing global search '%s': %s",
 
451
                 search.search_string, e.message);
824
452
      }
825
453
      
826
 
      check_empty_search (search, results_model);            
827
 
      
828
 
      full_timer.stop ();
829
 
      debug ("FULL SEARCH TIME FOR '%s': %fms",
830
 
             search.get_search_string(), full_timer.elapsed()*1000);
831
 
      
832
454
      /* Allow new searches once we enter an idle again.
833
455
       * We don't do it directly from here as that could mean we start
834
456
       * changing the model even before we had flushed out current changes
835
457
       */
836
458
      Idle.add (() => {
837
 
        files.thaw_notify ();
 
459
        scope.thaw_notify ();
838
460
        return false;
839
461
      });
840
462
      
841
463
      search.finished ();
842
464
    }
843
465
 
844
 
    private async void update_without_search_async (Section section,
845
 
                                                    Dee.Model results_model,
846
 
                                                    Dee.Model groups_model,
847
 
                                                    int group_override = -1)
848
 
    {
849
 
      bool active_section_hint_new = false;
850
 
    
851
 
      if (files.browser != null)
852
 
        {
853
 
          if (section != 0)
854
 
            {
855
 
              File folder = get_folder_for_pathbar_section (section);
856
 
              try {                 
857
 
                yield activate (folder.get_uri ());
858
 
              } catch (IOError ee) {
859
 
                    warning ("Failed to activate URI '%s': %s",
860
 
                             folder.get_uri (), ee.message);
861
 
              }
862
 
              debug ("Browsed %s from path bar", folder.get_uri ());
863
 
              return;
864
 
            }
865
 
          else
866
 
            {
867
 
              /* The root section of the pathbar was clicked.
868
 
               * Leave folder browsing mode */
869
 
              debug ("Root section of pathbar activated. Leaving browsing mode");
870
 
              reset_browsing_state ();
871
 
              files.active_section = browsing_root;
872
 
              files.set_hint (ACTIVE_SECTION_HINT, @"$browsing_root");
873
 
              section = browsing_root;
874
 
              active_section_hint_new = true;
875
 
            }          
876
 
        }
877
 
      
878
 
      /* If we have the UnityActiveSection hint and we didn't just set it,
879
 
       * then clear it */
880
 
      if (files.get_hint (ACTIVE_SECTION_HINT) != null &&
881
 
          !active_section_hint_new)
882
 
        {
883
 
          // Hack alert: We do the notify() because we need to trigger a DBus
884
 
          //             signal that the entry has changed
885
 
          files.clear_hint (ACTIVE_SECTION_HINT);
886
 
          files.notify_property ("active-section");
887
 
          debug ("Clearing active section hint");
888
 
        }
889
 
      
890
 
      var sections_model = files.sections_model;
891
 
 
892
 
      if (Section.LAST_SECTION != sections_model.get_n_rows())
893
 
        {
894
 
          critical ("Section model malformed");
895
 
          return;
896
 
        }
897
 
 
898
 
      if (section > Section.LAST_SECTION)
899
 
        {
900
 
          critical ("Active section out of bounds: %u", section);
901
 
          return;
902
 
        }
 
466
    private string get_current_type ()
 
467
    {
 
468
      /* Get the current type to filter by */
 
469
      var filter = scope.get_filter ("type") as RadioOptionFilter;
 
470
      Unity.FilterOption? option =  filter.get_active_option ();
 
471
      return option == null ? "all" : option.id;
 
472
    }
 
473
 
 
474
    private TimeRange get_current_timerange ()
 
475
    {
 
476
      var filter = scope.get_filter ("modified") as RadioOptionFilter;
 
477
      Unity.FilterOption? option = filter.get_active_option ();
 
478
 
 
479
      string date = option == null ? "all" : option.id;
 
480
 
 
481
      if (date == "last-7-days")
 
482
        return new TimeRange (Timestamp.now() - Timestamp.WEEK, Timestamp.now ());
 
483
      else if (date == "last-30-days")
 
484
        return new TimeRange (Timestamp.now() - (Timestamp.WEEK * 4), Timestamp.now());
 
485
      else if (date == "last-year")
 
486
        return new TimeRange (Timestamp.now() - Timestamp.YEAR, Timestamp.now ());
 
487
      else
 
488
        return new TimeRange.anytime ();
 
489
    }
 
490
 
 
491
    private async void update_without_search_async (LensSearch search)
 
492
    {
 
493
      var results_model = scope.results_model;
903
494
 
904
495
      /* Prevent concurrent searches and concurrent updates of our models,
905
496
       * by preventing any notify signals from propagating to us.
906
497
       * Important: Remeber to thaw the notifys again! */
907
 
      files.freeze_notify ();
908
 
 
909
 
      var templates = section_templates.get((int)section);
910
 
      var result_type = section == Section.FOLDERS ?
911
 
                               ResultType.MOST_RECENT_ORIGIN : 
912
 
                               ResultType.MOST_RECENT_SUBJECTS;
 
498
      scope.freeze_notify ();
 
499
      
 
500
      string type_id = get_current_type ();
 
501
 
 
502
      var result_type = type_id == "folder" ? ResultType.MOST_RECENT_ORIGIN
 
503
                                            : ResultType.MOST_RECENT_SUBJECTS;
 
504
 
 
505
      /* Grab the pre-compiled template we're going to use */
 
506
      var templates = new PtrArray.sized(1);
 
507
      templates.add (type_templates.lookup (type_id));
913
508
 
914
509
      try {
915
 
        var timer = new Timer();
916
 
        var events = yield log.find_events (
917
 
                                        new Zeitgeist.TimeRange.anytime(),
918
 
                                        templates,
919
 
                                        Zeitgeist.StorageState.ANY,
920
 
                                        100,
921
 
                                        result_type,
922
 
                                        null);
 
510
        /* Get results ranked by recency */
 
511
        var timer = new Timer ();
 
512
        var results = yield log.find_events (get_current_timerange (),
 
513
                                             templates,
 
514
                                             Zeitgeist.StorageState.ANY,
 
515
                                             100,
 
516
                                             result_type,
 
517
                                             null);
 
518
 
923
519
        timer.stop ();
924
 
        debug ("Got %u events for section %u in %fms",
925
 
               events.size(), section, timer.elapsed()*1000);
926
 
 
927
 
        /* Don't clear the model before we have the results ready */
928
 
        results_model.clear();
929
 
        
930
 
        if (section == Section.ALL_FILES)
931
 
          {
932
 
            append_bookmarks (bookmarks.list(), results_model);
933
 
            yield update_downloads_async (results_model, groups_model);
934
 
          }
935
 
 
936
 
        Unity.FilesPlace.append_events_sorted (events,
937
 
                                               results_model, groups_model,
938
 
                                               section,
939
 
                                               group_override);
 
520
        debug ("Found %u/%u no search results in %fms",
 
521
               results.size (), results.estimated_matches (),
 
522
               timer.elapsed()*1000);
 
523
 
 
524
        results_model.clear ();
 
525
 
 
526
        if (type_id == "all" || type_id == "folder")
 
527
          append_bookmarks (bookmarks.list(), results_model);
 
528
 
 
529
        yield update_downloads_async (results_model);
 
530
       
 
531
        Unity.FilesLens.append_events_sorted (results, results_model);
 
532
 
940
533
      } catch (GLib.Error e) {
941
 
        warning ("Error fetching recetnly used files: %s", e.message);
 
534
        warning ("Error performing empty search: %s",
 
535
                 e.message);
942
536
      }
943
537
      
944
 
      check_empty_section (section, results_model);
945
 
      
946
538
      /* Allow new searches once we enter an idle again.
947
539
       * We don't do it directly from here as that could mean we start
948
540
       * changing the model even before we had flushed out current changes
949
541
       */
950
542
      Idle.add (() => {
951
 
        files.thaw_notify ();
 
543
        scope.thaw_notify ();
952
544
        return false;
953
545
      });
 
546
      
 
547
      search.finished ();
954
548
    }
955
549
 
956
550
    private void append_bookmarks (GLib.List<Bookmark> bookmarks,
957
551
                                   Dee.Model results_model,
958
 
                                   Group group = Group.FAVORITE_FOLDERS)
 
552
                                   Categories category = Categories.FOLDERS)
959
553
    {
960
554
      foreach (var bookmark in bookmarks)
961
555
      {
962
 
        results_model.append (bookmark.uri, bookmark.icon, group,
 
556
        results_model.append (bookmark.uri, bookmark.icon, category,
963
557
                              bookmark.mimetype, bookmark.display_name,
964
558
                              bookmark.uri);
965
559
      }
966
560
    }
967
561
 
968
 
    private async void update_downloads_async (Dee.Model results_model,
969
 
                                               Dee.Model groups_model)
 
562
    private async void update_downloads_async (Dee.Model results_model)
970
563
    {
971
 
      // FIXME: The Downloads folder and update on changes
 
564
      // FIXME: Store the Downloads folder and update on changes
972
565
      unowned string download_path =
973
566
                 Environment.get_user_special_dir (UserDirectory.DOWNLOAD);
974
567
      var download_dir = File.new_for_path (download_path);
991
584
        var uri = download_dir.get_child (info.get_name ()).get_uri ();
992
585
        var mimetype = info.get_content_type ();
993
586
        var icon_hint = Utils.check_icon_string (uri, mimetype, info);
994
 
        results_model.append (uri, icon_hint, Group.DOWNLOADS,
995
 
                              mimetype, info.get_display_name (), uri);
996
 
      }
997
 
    }
998
 
 
999
 
    private void reset_browsing_state ()
1000
 
    {
1001
 
      /* We check for actual changes before resetting the properties, in order
1002
 
       * to avoid spurious PlaceEntryInfoChanged signals which Unity doesn't
1003
 
       * like that much */
1004
 
      if (files.browser != null)
1005
 
        {
1006
 
          files.browser = null;
1007
 
        }
1008
 
      
1009
 
      if (files.sections_model != sections_model)
1010
 
        {
1011
 
          files.sections_model = sections_model;
1012
 
        }
1013
 
      
1014
 
      files.clear_hints ();
1015
 
      is_dirty = true;
1016
 
    }
1017
 
 
1018
 
    /**
1019
 
     * Override of the default activation handler. The files place daemon
1020
 
     * can handle activation of folders which puts it into "folder browsing mode"
1021
 
     */
1022
 
    public async uint32 activate (string uri)
1023
 
    {
1024
 
      /* When Unity asks us to activate "." it's a special request for
1025
 
       * activating our UnityExtraAction hint. Which for the files' place
1026
 
       * is  "browse current folder in Nautilus" */
1027
 
      if ("." == uri)
1028
 
        {
1029
 
          browse_current_folder_in_nautilus ();
1030
 
          return ActivationStatus.ACTIVATED_HIDE_DASH;
1031
 
        }
1032
 
      
1033
 
      var f = File.new_for_uri (uri);
1034
 
      
1035
 
      if (f.query_file_type (0, null) != FileType.DIRECTORY)
1036
 
        {
1037
 
          debug ("Declined activation of URI '%s': Not a directory", uri);
1038
 
          return ActivationStatus.NOT_ACTIVATED;
1039
 
        }
1040
 
      
1041
 
      debug ("Browsing folder: %s", uri);
1042
 
 
1043
 
      /* Record the URI in the browser */
1044
 
      var state = new BrowsingState ();
1045
 
      state.search = files.active_search;
1046
 
      state.section = (Section)files.active_section;
1047
 
      state.uri = uri;
1048
 
      browser.record_state (state, uri); // FIXME: Nicer comment than just the URI
1049
 
      browsing_uri = uri;
1050
 
      
1051
 
      /* We need to mark us as dirty - otherwise we'll discard the section
1052
 
       * change signal when changed back to the section of the pathbar root */
1053
 
      is_dirty = true;
1054
 
      
1055
 
      /* If we are entering browsing mode record the active section,
1056
 
       * and set the UnityExtraAction hint to "Browse current folder
1057
 
       * in Nautilus" */
1058
 
          if (files.browser == null)    
1059
 
        {
1060
 
          browsing_root = (Section) files.active_section;
1061
 
          
1062
 
          /* The UnityExtraIcon hint must contain a serialized, GIcon,
1063
 
           * but a path to an image qualifies as such */
1064
 
          files.set_hint (EXTRA_ACTION_HINT, ICON_PATH + "open-folder.svg");
1065
 
        }      
1066
 
 
1067
 
      /* Setting the browser property does all the necessary dbus magic,
1068
 
       * it is also used to indicate to our selves that we are in browsing
1069
 
       * mode */          
1070
 
      files.browser = browser;      
1071
 
      
1072
 
      /* Change our files.sections_model over to a special model
1073
 
       * we use to render the breadcrumbs/path bar. Changing the
1074
 
       * files.sections_model property will automatically notify Unity
1075
 
       * over DBus with the PlaceEntryInfoChanged signal */
1076
 
      files.sections_model = pathbar_model;
1077
 
       
1078
 
      browse_folder.begin (f);
1079
 
 
1080
 
      return ActivationStatus.ACTIVATED_SHOW_DASH;
1081
 
    }
1082
 
    
1083
 
    private async void browse_folder (File folder)
1084
 
    {         
1085
 
      var results_model = files.entry_renderer_info.results_model;
1086
 
      SList<FileInfo> file_infos;
1087
 
      
1088
 
      // FIXME: Alphabetic sorting of folder contents
1089
 
      /* Populate the results_model with folder contents */
1090
 
      try {
1091
 
        file_infos = yield Utils.list_dir (folder);
1092
 
      } catch (GLib.Error err) {
1093
 
        warning ("Failed to browse folder '%s': %s",
1094
 
                 folder.get_uri (), err.message);
1095
 
        return;
1096
 
      }
1097
 
 
1098
 
      results_model.clear ();
1099
 
      foreach (var info in file_infos) {
1100
 
            if (info.get_is_hidden() || info.get_is_backup ())
1101
 
              continue;
1102
 
            
1103
 
        var uri = folder.get_child (info.get_name ()).get_uri ();
1104
 
        var mimetype = info.get_content_type ();
1105
 
        var icon_hint = Utils.check_icon_string (uri, mimetype, info);
1106
 
        debug ("Found child: %s", uri);
1107
 
        results_model.append (uri, icon_hint, 0, // FIXME: which group for folder browsing?
1108
 
                              mimetype, info.get_display_name (), uri);
1109
 
      }
1110
 
      
1111
 
      yield update_pathbar_model (folder);
1112
 
    }
1113
 
    
1114
 
    private async void update_pathbar_model (File folder)
1115
 
    {
1116
 
      /* Update the pathbar model with path relative to the home dir
1117
 
       * Unity should be showing the pathbar instead of the normal sections */
1118
 
      // FIXME: Don't .clear() the model, but compute the diff and update in stead
1119
 
      pathbar_model.clear ();
1120
 
      string home_path = Environment.get_home_dir ();
1121
 
      File parent = folder;
1122
 
      FileInfo finfo;      
1123
 
      do {
1124
 
        try {
1125
 
          if (parent.get_path () == home_path)
1126
 
            break;
1127
 
 
1128
 
          finfo = parent.query_info (Utils.file_attribs,
1129
 
                                           FileQueryInfoFlags.NONE);
1130
 
        } catch (GLib.Error e) {
1131
 
          warning ("Failed to compute breadcrumb path: %s", e.message);  
1132
 
                break;
1133
 
        }
1134
 
        pathbar_model.prepend (finfo.get_display_name (), "");
1135
 
      } while ((parent = parent.get_parent ()) != null);
1136
 
      
1137
 
      string section_name = sections_model.get_string (sections_model.get_iter_at_row (browsing_root), 0);
1138
 
      pathbar_model.prepend (section_name, "");
1139
 
    }
1140
 
    
1141
 
    /* Calculates the file URI of the path currently being browsed,
1142
 
     * up to the path element number @path_element */
1143
 
    private File get_folder_for_pathbar_section (uint path_element)
1144
 
    {
1145
 
      uint n_path_sections = pathbar_model.get_n_rows ();
1146
 
      if (path_element >= n_path_sections - 1)
1147
 
        {
1148
 
                      warning ("Path section out of bounds: %u (of %u)",
1149
 
                               path_element, n_path_sections);
1150
 
                      return File.new_for_path (Environment.get_home_dir ());
1151
 
        }
1152
 
                  
1153
 
      uint to_remove = n_path_sections - path_element - 1;
1154
 
      File uri = File.new_for_uri (browsing_uri);
1155
 
      
1156
 
      while (to_remove > 0)
1157
 
        {
1158
 
          uri = uri.get_parent ();
1159
 
          to_remove--;
1160
 
        }
1161
 
          
1162
 
      debug ("For section %u found: %s", path_element, uri.get_uri ());
1163
 
      return uri;
1164
 
    }
1165
 
    
1166
 
    /* Launch default file manager (Nautilus) on the currently browsed folder */
1167
 
    private void browse_current_folder_in_nautilus ()
1168
 
    {
1169
 
      if (browsing_uri == null || browsing_uri == "")
1170
 
        {
1171
 
        warning ("Unable to open current folder in file manager. " +
1172
 
                 "We don't have a current folder!");
1173
 
          return;
1174
 
        }
1175
 
      
1176
 
      debug ("Opening folder current folder '%s' in file manager",
1177
 
             browsing_uri);
1178
 
      
1179
 
      try {
1180
 
        AppInfo.launch_default_for_uri (browsing_uri, null);
1181
 
      } catch (GLib.Error e) {
1182
 
        warning ("Failed to open current folder '%s' in file manager: %s",
1183
 
                 browsing_uri, e.message);
1184
 
      }
1185
 
    }
1186
 
    
1187
 
    public void check_empty_search (PlaceSearch? search,
1188
 
                                    Dee.Model results_model)
1189
 
    {
1190
 
      if (results_model.get_n_rows () > 0)
1191
 
        return;
1192
 
      
1193
 
      if (search_is_invalid(search))
1194
 
        return;
1195
 
      
1196
 
      results_model.append ("", "", Group.EMPTY_SEARCH,
1197
 
                            "", _("Your search did not match any files"), "");      
1198
 
      
1199
 
      // FIXME: Use prefered browser
1200
 
      // FIXME: URL escape search string
1201
 
      results_model.append (@"http://google.com/#q=$(search.get_search_string())",
1202
 
                            "", Group.EMPTY_SEARCH,
1203
 
                            "", _("Search the web"), "");
1204
 
    }
1205
 
    
1206
 
    public void check_empty_section (Section section,
1207
 
                                     Dee.Model results_model)
1208
 
    {
1209
 
      if (results_model.get_n_rows () > 0)
1210
 
        return;
1211
 
            
1212
 
      string msg;
1213
 
      
1214
 
      switch (section)
1215
 
      {
1216
 
        case Section.ALL_FILES:
1217
 
          msg = _("There are no files in your Home folder");
1218
 
          break;
1219
 
        case Section.DOCUMENTS:
1220
 
          msg = _("There are no documents in your Home folder");
1221
 
          break;
1222
 
        case Section.FOLDERS:
1223
 
          msg = _("There are no folders in your Home folder");
1224
 
          break;
1225
 
        case Section.IMAGES:
1226
 
          msg = _("There are no images in your Home folder");
1227
 
          break;
1228
 
        case Section.AUDIO:
1229
 
          msg = _("There are no audio files in your Home folder");
1230
 
          break;
1231
 
        case Section.VIDEOS:
1232
 
          msg = _("There are no videos in your Home folder");
1233
 
          break;
1234
 
        case Section.PRESENTATIONS:
1235
 
          msg = _("There are no presentations in your Home folder");
1236
 
          break;
1237
 
        case Section.OTHER:
1238
 
          msg = _("There are no other files in your Home folder");
1239
 
          break;
1240
 
        default:
1241
 
          warning ("Unknown section number: %u", section);
1242
 
          msg = _("There are no files in your Home folder");
1243
 
          break;
1244
 
      }
1245
 
      
1246
 
      results_model.append ("", "", Group.EMPTY_SECTION, "", msg, "");
1247
 
    }
1248
 
    
 
587
        results_model.append (uri, icon_hint, Categories.DOWNLOADS,
 
588
                              mimetype, info.get_display_name (), uri);
 
589
      }
 
590
    }
 
591
 
 
592
    public Unity.ActivationResponse activate (string uri)
 
593
    {
 
594
      debug (@"Activating: $uri");
 
595
      try {
 
596
        AppInfo.launch_default_for_uri (uri, null);
 
597
        return new Unity.ActivationResponse(Unity.HandledType.HIDE_DASH);
 
598
      } catch (GLib.Error error) {
 
599
        warning ("Failed to launch URI %s", uri);
 
600
        return new Unity.ActivationResponse(Unity.HandledType.NOT_HANDLED);
 
601
      }
 
602
    }
 
603
 
1249
604
    private void on_zeitgeist_changed ()
1250
605
    {
1251
606
      /* Emitting notify here will make us recheck if the search results
1252
607
       * need update. In the negative case this is a noop */
1253
608
      is_dirty = true;
1254
 
      files.notify_property ("active-section");
 
609
      scope.notify_property ("active-search");
1255
610
    }
1256
611
    
1257
 
  } /* End: Daemon class */
1258
 
  
 
612
  }  
 
613
 
1259
614
  /* Appends a set of Zeitgeist.Events to our Dee.Model assuming that
1260
615
   * these events are already sorted with descending timestamps */
1261
 
  public Gee.Set<string> append_events_sorted (Zeitgeist.ResultSet events,
1262
 
                                               Dee.Model results,
1263
 
                                               Dee.Model groups,
1264
 
                                               Section section,
1265
 
                                               int group_override = -1,
1266
 
                                               Gee.Set<string>? seen_uris = null)
1267
 
    {
1268
 
      Gee.Set<string> _seen_uris;
1269
 
    
1270
 
      if (seen_uris == null)
1271
 
        _seen_uris = new Gee.HashSet<string>(str_hash);
1272
 
      else
1273
 
        _seen_uris = seen_uris;
1274
 
    
1275
 
      foreach (var ev in events)
1276
 
        {
1277
 
          if (ev.num_subjects() > 0)
1278
 
            {
1279
 
              // FIXME: We only use the first subject...
1280
 
              Zeitgeist.Subject su = ev.get_subject(0);
1281
 
              //var timer = new Timer();
1282
 
              
1283
 
              string uri = su.get_uri ();
1284
 
                  string display_name = su.get_text ();
1285
 
                  string mimetype = su.get_mimetype () != null ?
1286
 
                                su.get_mimetype () : "application/octet-stream";
1287
 
                  File file = File.new_for_uri (uri);
1288
 
                  
1289
 
                  /* De-dup the results keyed on the subject URIs */
1290
 
              if (uri in _seen_uris)
 
616
  public void append_events_sorted (Zeitgeist.ResultSet events,
 
617
                                    Dee.Model results)
 
618
  {
 
619
     foreach (var ev in events)
 
620
      {
 
621
        if (ev.num_subjects() > 0)
 
622
          {
 
623
            // FIXME: We only use the first subject...
 
624
            Zeitgeist.Subject su = ev.get_subject(0);
 
625
            
 
626
            string uri = su.get_uri ();
 
627
            string display_name = su.get_text ();
 
628
            string mimetype = su.get_mimetype () != null ?
 
629
                          su.get_mimetype () : "application/octet-stream";
 
630
            File file = File.new_for_uri (uri);
 
631
            
 
632
            /* Don't check existence on non-native files as http:// and
 
633
             * friends are *very* expensive to query */
 
634
            if (file.is_native()) {
 
635
              // hidden files should be ignored
 
636
              try {
 
637
                FileInfo info = file.query_info(FILE_ATTRIBUTE_STANDARD_IS_HIDDEN, 0, null);
 
638
                if (info.get_is_hidden())
 
639
                  continue;
 
640
              } catch (GLib.Error e) {
 
641
                // as error occurred file must be missing therefore ignoring it
1291
642
                continue;
1292
 
              _seen_uris.add (uri);
1293
 
              
1294
 
              /* Don't check existence on non-native files as http:// and
1295
 
               * friends are *very* expensive to query */
1296
 
              if (file.is_native()) {
1297
 
                // hidden files should be ignored
1298
 
                try {
1299
 
                  FileInfo info = file.query_info(FILE_ATTRIBUTE_STANDARD_IS_HIDDEN, 0, null);
1300
 
                  if (info.get_is_hidden())
1301
 
                    continue;
1302
 
                } catch (GLib.Error e) {
1303
 
                  // as error occurred file must be missing therefore ignoring it
1304
 
                  continue;
1305
 
                }
1306
 
              }
1307
 
                  
1308
 
              if (section == Section.FOLDERS)
1309
 
                {
1310
 
                  File dir = File.new_for_uri(uri).get_parent();
1311
 
                  uri = dir.get_uri ();
1312
 
                  try{
1313
 
                    FileInfo info = dir.query_info (FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
1314
 
                                                        FileQueryInfoFlags.NONE);
1315
 
                        display_name = info.get_display_name ();
1316
 
                      } catch (GLib.Error e) {
1317
 
                        /* Bugger, we fall back to basename, which might not be
1318
 
                         * valid UTF8... */
1319
 
                        warning ("Unable to get display name for %s", uri);
1320
 
                        display_name = dir.get_basename ();
1321
 
                      }
1322
 
                      mimetype = "inode/directory";
1323
 
                }
1324
 
                  
1325
 
              string icon = Utils.get_icon_for_uri (uri, mimetype);
1326
 
              
1327
 
              uint group_id;
1328
 
              string comment = "";
1329
 
              
1330
 
              /* We call this as we want the comment string */
1331
 
              group_id= Utils.get_time_group (ev, groups, out comment);
1332
 
              
1333
 
              if (group_override >= 0)
1334
 
                {
1335
 
                  group_id = (uint) group_override;
1336
 
                }
1337
 
              
1338
 
              results.append (uri, icon, group_id, mimetype,
1339
 
                              display_name, comment);
1340
 
              
1341
 
              //timer.stop();
1342
 
              //debug("  + %s: %fms", uri, timer.elapsed()*1000);
1343
 
            }          
1344
 
        }
1345
 
      
1346
 
      return _seen_uris;
1347
 
    }
 
643
              }
 
644
            }
 
645
#if 0    
 
646
            if (section == Section.FOLDERS)
 
647
              {
 
648
                File dir = File.new_for_uri(uri).get_parent();
 
649
                uri = dir.get_uri ();
 
650
                try{
 
651
                  FileInfo info = dir.query_info (FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
 
652
                                                FileQueryInfoFlags.NONE);
 
653
                display_name = info.get_display_name ();
 
654
              } catch (GLib.Error e) {
 
655
                /* Bugger, we fall back to basename, which might not be
 
656
                 * valid UTF8... */
 
657
                warning ("Unable to get display name for %s", uri);
 
658
                display_name = dir.get_basename ();
 
659
              }
 
660
              mimetype = "inode/directory";
 
661
              }
 
662
#endif           
 
663
            string icon = Utils.get_icon_for_uri (uri, mimetype);
 
664
            
 
665
            uint category_id;
 
666
            string comment = "";
 
667
            
 
668
            category_id = file.query_file_type (0, null) == FileType.DIRECTORY ? Categories.FOLDERS
 
669
                                                                          : Categories.RECENT;
 
670
            results.append (uri, icon, category_id, mimetype,
 
671
                            display_name, comment);
 
672
            
 
673
          }          
 
674
      }
 
675
   }
1348
676
} /* namespace */
1349
677