47
35
private Bookmarks bookmarks;
48
36
private UrlChecker urls;
50
private Unity.PlaceController control;
51
private Unity.PlaceEntryInfo files;
52
private Unity.Browser<BrowsingState> browser;
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;
38
private Unity.Lens lens;
39
private Unity.Scope scope;
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;
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
71
private Gee.List<unowned Dee.ModelIter?> months;
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;
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;
81
49
private bool is_dirty;
82
private bool all_models_synced;
84
private Dee.Index entry_results_by_group;
88
sections_model = new Dee.SharedModel("com.canonical.Unity.FilesPlace.SectionsModel");
89
sections_model.set_schema ("s", "s");
91
pathbar_model = new Dee.SharedModel("com.canonical.Unity.FilesPlace.PathBarModel");
92
pathbar_model.set_schema ("s", "s");
94
var groups_model = new Dee.SharedModel("com.canonical.Unity.FilesPlace.GroupsModel");
95
groups_model.set_schema ("s", "s", "s");
97
var global_groups_model = new Dee.SharedModel("com.canonical.Unity.FilesPlace.GlobalGroupsModel");
98
global_groups_model.set_schema ("s", "s", "s");
100
var results_model = new Dee.SharedModel("com.canonical.Unity.FilesPlace.ResultsModel");
101
results_model.set_schema ("s", "s", "u", "s", "s", "s");
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");
106
section_templates = new Gee.ArrayList<PtrArray> ();
107
prepare_section_templates();
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;
116
files.icon = @"$(Config.PREFIX)/share/unity/themes/files.png";
53
prepare_type_templates();
55
scope = new Unity.Scope ("/com/canonical/unity/scope/files");
56
scope.search_in_global = true;
57
scope.activate_uri.connect (activate);
59
lens = new Unity.Lens("/com/canonical/unity/lens/files", "files");
60
lens.search_in_global = true;
61
lens.search_hint = _("Search Files & Folders");
63
populate_categories ();
65
lens.add_local_scope (scope);
118
67
previous_search = null;
119
previous_active_section = Section.LAST_SECTION; /* Must be an illegal section! */
123
var analyzer = new Dee.Analyzer.for_uint32_column (ResultsColumn.GROUP_ID);
124
entry_results_by_group = new Dee.HashIndex (results_model,
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 ();
147
/* Listen for section changes */
148
files.notify["active-section"].connect (
153
if (!all_models_synced)
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;
160
if (!(is_dirty || previous_active_section != files.active_section))
167
if (search_is_invalid (files.active_search))
169
int group_override = section == Section.ALL_FILES ?
171
update_without_search_async.begin(section,
172
_results_model, _groups_model,
177
update_search_async.begin (files.active_search, section,
178
_results_model, _groups_model,
179
false, entry_results_by_group);
182
previous_active_section = files.active_section;
91
/* Listen for filter changes */
92
scope.filters_changed.connect (
94
if (scope.active_search != null)
97
scope.notify_property ("active-search");
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;
191
if (!all_models_synced)
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;
201
107
if (!(Utils.search_has_really_changed (previous_search, search) || is_dirty))
204
110
is_dirty = false;
206
if (search_is_invalid (files.active_search))
112
if (search_is_invalid (search))
208
int group_override = section == Section.ALL_FILES ?
210
update_without_search_async.begin(section,
211
_results_model, _groups_model,
114
update_without_search_async.begin(search);
216
update_search_async.begin (search, section,
217
_results_model, _groups_model,
218
Utils.check_is_filter_search(search,
220
entry_results_by_group);
118
update_search_async.begin (search);
222
120
previous_search = search;
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)
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;
236
130
if (search_is_invalid (search))
239
133
if (!Utils.search_has_really_changed (previous_search, search))
242
update_global_search_async.begin (search,
245
Utils.check_is_filter_search(search, previous_search));
136
update_global_search_async.begin (search);
246
137
previous_search = search;
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 (
254
debug (@"Activated: $(files.active)");
255
if (files.active && is_dirty)
257
files.notify_property ("active-section");
261
reset_browsing_state ();
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
274
} catch (IOError error) {
275
critical ("Failed to export DBus service for '%s': %s",
276
control.dbus_path, error.message);
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);
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);
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;
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)
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)
327
all_models_synced = true;
329
populate_sections ();
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");
340
private void populate_sections ()
342
var sections = files.sections_model;
344
if (sections.get_n_rows() != 0)
346
debug ("Sections model already populated. We probably cloned it off Unity. Resetting it.");
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"), "");
360
private void populate_groups ()
362
var groups = files.entry_renderer_info.groups_model;
364
if (groups.get_n_rows() != 0)
366
debug ("Groups model already populated. We probably cloned it off Unity. Resetting it.");
370
/* Always expand the Favorite Folders group */
371
files.entry_renderer_info.set_hint ("ExpandedGroups",
372
@"$((uint)Group.FAVORITE_FOLDERS)");
374
groups.append ("UnityFileInfoRenderer",
376
ICON_PATH + "group-mostused.svg");
377
groups.append ("UnityDefaultRenderer",
379
ICON_PATH + "group-recent.svg");
380
groups.append ("UnityDefaultRenderer",
382
ICON_PATH + "group-downloads.svg");
383
groups.append ("UnityDefaultRenderer",
384
_("Favorite Folders"),
385
ICON_PATH + "group-favoritefolders.svg");
386
groups.append ("UnityDefaultRenderer",
388
ICON_PATH + "group-recent.svg");
389
groups.append ("UnityEmptySearchRenderer",
390
"No search results", /* No i18n. Should never be rendered */
392
groups.append ("UnityEmptySectionRenderer",
393
"Empty section", /* No i18n. Should never be rendered */
395
groups.append ("UnityFileInfoRenderer",
397
ICON_PATH + "group-daterange.svg");
398
groups.append ("UnityFileInfoRenderer",
400
ICON_PATH + "group-daterange.svg");
401
groups.append ("UnityFileInfoRenderer",
403
ICON_PATH + "group-daterange.svg");
404
groups.append ("UnityFileInfoRenderer",
406
ICON_PATH + "group-daterange.svg");
407
groups.append ("UnityFileInfoRenderer",
409
ICON_PATH + "group-daterange.svg");
410
groups.append ("UnityFileInfoRenderer",
411
_("Past Six Months"),
412
ICON_PATH + "group-daterange.svg");
413
groups.append ("UnityFileInfoRenderer",
415
ICON_PATH + "group-daterange.svg");
416
groups.append ("UnityFileInfoRenderer",
418
ICON_PATH + "group-daterange.svg");
419
// FIXME: For prehistoric items use actual year, eg "2009"
422
months = new Gee.ArrayList<unowned Dee.ModelIter?>();
424
for (uint i = 1; i <= DateMonth.DECEMBER; i++)
426
unowned Dee.ModelIter iter = groups.append ("UnityFileInfoRenderer",
427
Utils.get_month_name ((DateMonth)i),
428
ICON_PATH + "group-daterange.svg");
433
/* We only need the global groups up to Group.FILES */
434
var global_groups = files.global_renderer_info.groups_model;
436
if (global_groups.get_n_rows() != 0)
438
debug ("Global groups model already populated. We probably cloned it off Unity. Resetting it.");
439
global_groups.clear ();
442
global_groups.append ("UnityFileInfoRenderer",
444
ICON_PATH + "group-mostused.svg");
445
global_groups.append ("UnityDefaultRenderer",
447
ICON_PATH + "group-recent.svg");
448
global_groups.append ("UnityDefaultRenderer",
450
ICON_PATH + "group-downloads.svg");
451
global_groups.append ("UnityDefaultRenderer",
453
ICON_PATH + "group-favoritefolders.svg");
454
global_groups.append ("UnityDefaultRenderer",
456
ICON_PATH + "group-recent.svg");
459
private void prepare_section_templates ()
144
private void populate_filters ()
146
Unity.Filter[] filters = {};
150
var filter = new RadioOptionFilter ("modified", _("Last modified"));
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"));
161
var filter = new RadioOptionFilter ("type", _("Type"));
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"));
176
var filter = new MultiRangeFilter ("size", _("Size"));
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"));
189
lens.filters = filters;
192
private void populate_categories ()
194
Unity.Category[] categories = {};
196
var cat = new Unity.Category (_("Recent"), ICON_PATH + "category-recent.svg");
199
cat = new Unity.Category (_("Downloads"), ICON_PATH + "category-downloads.svg");
202
cat = new Unity.Category (_("Folders"), ICON_PATH + "category-favoritefolders.svg");
205
lens.categories = categories;
208
private void prepare_type_templates ()
210
type_templates = new HashTable<string, Event> (str_hash, str_equal);
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 */
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);
220
type_templates.insert ("all", (event as GLib.Object).ref() as Event);
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:*",
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);
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);
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);
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);
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);
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);
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, "", "", "", "", ""));
689
379
* changing the model even before we had flushed out current changes
691
381
Idle.add (() => {
692
files.thaw_notify ();
382
scope.thaw_notify ();
696
386
search.finished ();
699
private async void update_search_async (PlaceSearch search,
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)
707
Timer full_timer = new Timer ();
709
// FIXME: Implement in-folder searching
710
if (files.browser != null)
712
warning ("In-folder searching not implemented yet");
716
391
if (search_is_invalid (search))
718
update_without_search_async.begin (section,
393
update_without_search_async.begin (search);
397
var results_model = scope.results_model;
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 ();
729
404
var search_string = prepare_search_string (search);
730
var templates = section_templates.get((int)section);
406
string type_id = get_current_type ();
408
var result_type = type_id == "folder" ? ResultType.MOST_RECENT_ORIGIN
409
: ResultType.MOST_RECENT_SUBJECTS;
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));
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 (),
738
422
Zeitgeist.StorageState.ANY,
740
Zeitgeist.ResultType.RELEVANCY,
744
debug ("Found %u/%u Top Results for search '%s' in %fms",
745
results.size (), results.estimated_matches (),
746
search_string, timer.elapsed()*1000);
748
/* Clean up results model */
749
if (!is_filter_search)
751
/* Don't clear the results before the first ones are ready */
752
results_model.clear ();
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)
763
results_model.remove (row);
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);
774
var seen_uris = Unity.FilesPlace.append_events_sorted (results,
780
/* Get time-grouped results */
781
var result_type = section == Section.FOLDERS ?
782
ResultType.MOST_RECENT_ORIGIN :
783
ResultType.MOST_RECENT_SUBJECTS;
785
results = yield index.search (search_string,
786
new Zeitgeist.TimeRange.anytime(),
788
Zeitgeist.StorageState.ANY,
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);
798
if (!is_filter_search)
800
Unity.FilesPlace.append_events_sorted (results,
801
results_model, groups_model,
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));
428
debug ("Found %u/%u results for search '%s' in %fms",
429
results.size (), results.estimated_matches (),
430
search_string, timer.elapsed()*1000);
432
results_model.clear ();
434
var checked_url = urls.check_url (search.search_string);
435
if (checked_url != null)
437
results_model.append (checked_url, urls.icon, Categories.RECENT,
438
"text/html", search.search_string,
442
var bookmark_matches = bookmarks.prefix_search (search.search_string);
443
append_bookmarks (bookmark_matches, results_model, Categories.FOLDERS);
445
/* FIXME: Add downloads */
447
Unity.FilesLens.append_events_sorted (results, results_model);
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);
826
check_empty_search (search, results_model);
829
debug ("FULL SEARCH TIME FOR '%s': %fms",
830
search.get_search_string(), full_timer.elapsed()*1000);
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
836
458
Idle.add (() => {
837
files.thaw_notify ();
459
scope.thaw_notify ();
841
463
search.finished ();
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)
849
bool active_section_hint_new = false;
851
if (files.browser != null)
855
File folder = get_folder_for_pathbar_section (section);
857
yield activate (folder.get_uri ());
858
} catch (IOError ee) {
859
warning ("Failed to activate URI '%s': %s",
860
folder.get_uri (), ee.message);
862
debug ("Browsed %s from path bar", folder.get_uri ());
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;
878
/* If we have the UnityActiveSection hint and we didn't just set it,
880
if (files.get_hint (ACTIVE_SECTION_HINT) != null &&
881
!active_section_hint_new)
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");
890
var sections_model = files.sections_model;
892
if (Section.LAST_SECTION != sections_model.get_n_rows())
894
critical ("Section model malformed");
898
if (section > Section.LAST_SECTION)
900
critical ("Active section out of bounds: %u", section);
466
private string get_current_type ()
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;
474
private TimeRange get_current_timerange ()
476
var filter = scope.get_filter ("modified") as RadioOptionFilter;
477
Unity.FilterOption? option = filter.get_active_option ();
479
string date = option == null ? "all" : option.id;
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 ());
488
return new TimeRange.anytime ();
491
private async void update_without_search_async (LensSearch search)
493
var results_model = scope.results_model;
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 ();
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 ();
500
string type_id = get_current_type ();
502
var result_type = type_id == "folder" ? ResultType.MOST_RECENT_ORIGIN
503
: ResultType.MOST_RECENT_SUBJECTS;
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));
915
var timer = new Timer();
916
var events = yield log.find_events (
917
new Zeitgeist.TimeRange.anytime(),
919
Zeitgeist.StorageState.ANY,
510
/* Get results ranked by recency */
511
var timer = new Timer ();
512
var results = yield log.find_events (get_current_timerange (),
514
Zeitgeist.StorageState.ANY,
924
debug ("Got %u events for section %u in %fms",
925
events.size(), section, timer.elapsed()*1000);
927
/* Don't clear the model before we have the results ready */
928
results_model.clear();
930
if (section == Section.ALL_FILES)
932
append_bookmarks (bookmarks.list(), results_model);
933
yield update_downloads_async (results_model, groups_model);
936
Unity.FilesPlace.append_events_sorted (events,
937
results_model, groups_model,
520
debug ("Found %u/%u no search results in %fms",
521
results.size (), results.estimated_matches (),
522
timer.elapsed()*1000);
524
results_model.clear ();
526
if (type_id == "all" || type_id == "folder")
527
append_bookmarks (bookmarks.list(), results_model);
529
yield update_downloads_async (results_model);
531
Unity.FilesLens.append_events_sorted (results, results_model);
940
533
} catch (GLib.Error e) {
941
warning ("Error fetching recetnly used files: %s", e.message);
534
warning ("Error performing empty search: %s",
944
check_empty_section (section, results_model);
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
950
542
Idle.add (() => {
951
files.thaw_notify ();
543
scope.thaw_notify ();
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)
960
554
foreach (var bookmark in bookmarks)
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,
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)
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);
999
private void reset_browsing_state ()
1001
/* We check for actual changes before resetting the properties, in order
1002
* to avoid spurious PlaceEntryInfoChanged signals which Unity doesn't
1004
if (files.browser != null)
1006
files.browser = null;
1009
if (files.sections_model != sections_model)
1011
files.sections_model = sections_model;
1014
files.clear_hints ();
1019
* Override of the default activation handler. The files place daemon
1020
* can handle activation of folders which puts it into "folder browsing mode"
1022
public async uint32 activate (string uri)
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" */
1029
browse_current_folder_in_nautilus ();
1030
return ActivationStatus.ACTIVATED_HIDE_DASH;
1033
var f = File.new_for_uri (uri);
1035
if (f.query_file_type (0, null) != FileType.DIRECTORY)
1037
debug ("Declined activation of URI '%s': Not a directory", uri);
1038
return ActivationStatus.NOT_ACTIVATED;
1041
debug ("Browsing folder: %s", uri);
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;
1048
browser.record_state (state, uri); // FIXME: Nicer comment than just the URI
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 */
1055
/* If we are entering browsing mode record the active section,
1056
* and set the UnityExtraAction hint to "Browse current folder
1058
if (files.browser == null)
1060
browsing_root = (Section) files.active_section;
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");
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
1070
files.browser = browser;
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;
1078
browse_folder.begin (f);
1080
return ActivationStatus.ACTIVATED_SHOW_DASH;
1083
private async void browse_folder (File folder)
1085
var results_model = files.entry_renderer_info.results_model;
1086
SList<FileInfo> file_infos;
1088
// FIXME: Alphabetic sorting of folder contents
1089
/* Populate the results_model with folder contents */
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);
1098
results_model.clear ();
1099
foreach (var info in file_infos) {
1100
if (info.get_is_hidden() || info.get_is_backup ())
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);
1111
yield update_pathbar_model (folder);
1114
private async void update_pathbar_model (File folder)
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;
1125
if (parent.get_path () == home_path)
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);
1134
pathbar_model.prepend (finfo.get_display_name (), "");
1135
} while ((parent = parent.get_parent ()) != null);
1137
string section_name = sections_model.get_string (sections_model.get_iter_at_row (browsing_root), 0);
1138
pathbar_model.prepend (section_name, "");
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)
1145
uint n_path_sections = pathbar_model.get_n_rows ();
1146
if (path_element >= n_path_sections - 1)
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 ());
1153
uint to_remove = n_path_sections - path_element - 1;
1154
File uri = File.new_for_uri (browsing_uri);
1156
while (to_remove > 0)
1158
uri = uri.get_parent ();
1162
debug ("For section %u found: %s", path_element, uri.get_uri ());
1166
/* Launch default file manager (Nautilus) on the currently browsed folder */
1167
private void browse_current_folder_in_nautilus ()
1169
if (browsing_uri == null || browsing_uri == "")
1171
warning ("Unable to open current folder in file manager. " +
1172
"We don't have a current folder!");
1176
debug ("Opening folder current folder '%s' in file manager",
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);
1187
public void check_empty_search (PlaceSearch? search,
1188
Dee.Model results_model)
1190
if (results_model.get_n_rows () > 0)
1193
if (search_is_invalid(search))
1196
results_model.append ("", "", Group.EMPTY_SEARCH,
1197
"", _("Your search did not match any files"), "");
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"), "");
1206
public void check_empty_section (Section section,
1207
Dee.Model results_model)
1209
if (results_model.get_n_rows () > 0)
1216
case Section.ALL_FILES:
1217
msg = _("There are no files in your Home folder");
1219
case Section.DOCUMENTS:
1220
msg = _("There are no documents in your Home folder");
1222
case Section.FOLDERS:
1223
msg = _("There are no folders in your Home folder");
1225
case Section.IMAGES:
1226
msg = _("There are no images in your Home folder");
1229
msg = _("There are no audio files in your Home folder");
1231
case Section.VIDEOS:
1232
msg = _("There are no videos in your Home folder");
1234
case Section.PRESENTATIONS:
1235
msg = _("There are no presentations in your Home folder");
1238
msg = _("There are no other files in your Home folder");
1241
warning ("Unknown section number: %u", section);
1242
msg = _("There are no files in your Home folder");
1246
results_model.append ("", "", Group.EMPTY_SECTION, "", msg, "");
587
results_model.append (uri, icon_hint, Categories.DOWNLOADS,
588
mimetype, info.get_display_name (), uri);
592
public Unity.ActivationResponse activate (string uri)
594
debug (@"Activating: $uri");
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);
1249
604
private void on_zeitgeist_changed ()
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");
1257
} /* End: Daemon class */
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,
1265
int group_override = -1,
1266
Gee.Set<string>? seen_uris = null)
1268
Gee.Set<string> _seen_uris;
1270
if (seen_uris == null)
1271
_seen_uris = new Gee.HashSet<string>(str_hash);
1273
_seen_uris = seen_uris;
1275
foreach (var ev in events)
1277
if (ev.num_subjects() > 0)
1279
// FIXME: We only use the first subject...
1280
Zeitgeist.Subject su = ev.get_subject(0);
1281
//var timer = new Timer();
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);
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,
619
foreach (var ev in events)
621
if (ev.num_subjects() > 0)
623
// FIXME: We only use the first subject...
624
Zeitgeist.Subject su = ev.get_subject(0);
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);
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
637
FileInfo info = file.query_info(FILE_ATTRIBUTE_STANDARD_IS_HIDDEN, 0, null);
638
if (info.get_is_hidden())
640
} catch (GLib.Error e) {
641
// as error occurred file must be missing therefore ignoring it
1292
_seen_uris.add (uri);
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
1299
FileInfo info = file.query_info(FILE_ATTRIBUTE_STANDARD_IS_HIDDEN, 0, null);
1300
if (info.get_is_hidden())
1302
} catch (GLib.Error e) {
1303
// as error occurred file must be missing therefore ignoring it
1308
if (section == Section.FOLDERS)
1310
File dir = File.new_for_uri(uri).get_parent();
1311
uri = dir.get_uri ();
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
1319
warning ("Unable to get display name for %s", uri);
1320
display_name = dir.get_basename ();
1322
mimetype = "inode/directory";
1325
string icon = Utils.get_icon_for_uri (uri, mimetype);
1328
string comment = "";
1330
/* We call this as we want the comment string */
1331
group_id= Utils.get_time_group (ev, groups, out comment);
1333
if (group_override >= 0)
1335
group_id = (uint) group_override;
1338
results.append (uri, icon, group_id, mimetype,
1339
display_name, comment);
1342
//debug(" + %s: %fms", uri, timer.elapsed()*1000);
646
if (section == Section.FOLDERS)
648
File dir = File.new_for_uri(uri).get_parent();
649
uri = dir.get_uri ();
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
657
warning ("Unable to get display name for %s", uri);
658
display_name = dir.get_basename ();
660
mimetype = "inode/directory";
663
string icon = Utils.get_icon_for_uri (uri, mimetype);
668
category_id = file.query_file_type (0, null) == FileType.DIRECTORY ? Categories.FOLDERS
670
results.append (uri, icon, category_id, mimetype,
671
display_name, comment);
1348
676
} /* namespace */