Merge lp:~jeremywootten/pantheon-files/fix-1604300-merge-find-functionalities-part2 into lp:~elementary-apps/pantheon-files/trunk

Proposed by Jeremy Wootten
Status: Rejected
Rejected by: Danielle Foré
Proposed branch: lp:~jeremywootten/pantheon-files/fix-1604300-merge-find-functionalities-part2
Merge into: lp:~elementary-apps/pantheon-files/trunk
Prerequisite: lp:~jeremywootten/pantheon-files/fix-1604300-merge-find-functionalities-part1
Diff against target: 573 lines (+242/-117)
1 file modified
libwidgets/View/SearchResults.vala (+242/-117)
To merge this branch: bzr merge lp:~jeremywootten/pantheon-files/fix-1604300-merge-find-functionalities-part2
Reviewer Review Type Date Requested Status
elementary Apps team Pending
Review via email: mp+313862@code.launchpad.net

Description of the change

This branch adds multi-level sorting to the search results as well as introducing categories to distinguish files in the current folder, below the current folder, in the bookmarks and in zeitgeist. Within categories the results are sorted with those starting with the search term coming first and then sorting by name.

Zeitgeist searching is fixed so more matches are found.

Each category is counted separately so a large number of hits in one category does not hide all the results in another category.

Hover-select is reverted as the search window is now likely to overlap the mouse cursor and result in a random initial selection otherwise.

To post a comment you must log in.
2415. By Jeremy Wootten

Merge changes in parent branch

2416. By Jeremy Wootten

No hover-select; no select headers

Revision history for this message
Danielle Foré (danrabbit) wrote :

I agree with removing the hover select. It causes some unintended behavior when searching.

2417. By Jeremy Wootten

Merge trunk to r2470

Revision history for this message
Danielle Foré (danrabbit) wrote :
Revision history for this message
Danielle Foré (danrabbit) wrote :

Gonna reject this proposal since the other branch supersedes it

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'libwidgets/View/SearchResults.vala'
2--- libwidgets/View/SearchResults.vala 2017-01-13 13:31:06 +0000
3+++ libwidgets/View/SearchResults.vala 2017-02-04 13:36:24 +0000
4@@ -20,6 +20,40 @@
5 {
6 public class SearchResults : Gtk.Window, Searchable
7 {
8+ /* The order of these categories governs the order in which matches appear in the search view.
9+ * The category represents a first level sort. Within a category the matches sort alphabetically on name */
10+ private enum Category {
11+ CURRENT_HEADER,
12+ CURRENT_BEGINS,
13+ CURRENT_CONTAINS,
14+ CURRENT_ELLIPSIS,
15+ DEEP_HEADER,
16+ DEEP_BEGINS,
17+ DEEP_CONTAINS,
18+ DEEP_ELLIPSIS,
19+ ZEITGEIST_HEADER,
20+ ZEITGEIST_BEGINS,
21+ ZEITGEIST_CONTAINS,
22+ ZEITGEIST_ELLIPSIS,
23+ BOOKMARK_HEADER,
24+ BOOKMARK_BEGINS,
25+ BOOKMARK_CONTAINS,
26+ BOOKMARK_ELLIPSIS
27+ }
28+
29+ /* This function converts a Category enum to a letter which can be prefixed to the match
30+ * name to form a sort key. This ensures that the categories appear in the list in the
31+ * desired order - that is within each class of results (current folder, deep search,
32+ * zeitgeist search and bookmark search), after the header, the matches appear with the
33+ * "begins with" ones first, then the "contains" and finally an "ellipsis" pseudo-match
34+ * appears if MAX_RESULTS is exceeded for that category.
35+ */
36+ /* The constant "sort_string" serves as an array of characters indexed by the enum. */
37+ private const string sort_string = "ABCDEFGHIJKLMN";
38+ private static string category_to_string (Category category) {
39+ return (sort_string.get_char ((uint)category)).to_string ();
40+ }
41+
42 class Match : Object
43 {
44 public string name { get; construct; }
45@@ -27,32 +61,34 @@
46 public string path_string { get; construct; }
47 public Icon icon { get; construct; }
48 public File? file { get; construct; }
49+ public string sortkey { get; construct; }
50
51- public Match (FileInfo info, string path_string, File parent)
52- {
53- Object (name: info.get_display_name (),
54+ public Match (FileInfo info, string path_string, File parent, SearchResults.Category category) {
55+ var _name = info.get_display_name ();
56+ Object (name: Markup.escape_text (_name),
57 mime: info.get_content_type (),
58 icon: info.get_icon (),
59 path_string: path_string,
60- file: parent.resolve_relative_path (info.get_name ()));
61+ file: parent.resolve_relative_path (info.get_name ()),
62+ sortkey: category_to_string (category) + _name);
63 }
64
65- public Match.from_bookmark (Bookmark bookmark)
66- {
67- Object (name: bookmark.label,
68+ public Match.from_bookmark (Bookmark bookmark, SearchResults.Category category) {
69+ Object (name: Markup.escape_text (bookmark.label),
70 mime: "inode/directory",
71 icon: bookmark.get_icon (),
72 path_string: "",
73- file: bookmark.get_location ());
74+ file: bookmark.get_location (),
75+ sortkey: SearchResults.category_to_string (category) + bookmark.label);
76 }
77
78- public Match.ellipsis ()
79- {
80+ public Match.ellipsis (SearchResults.Category category) {
81 Object (name: "...",
82 mime: "",
83 icon: null,
84 path_string: "",
85- file: null);
86+ file: null,
87+ sortkey: SearchResults.category_to_string (category));
88 }
89 }
90
91@@ -79,7 +115,8 @@
92 Zeitgeist.Index zg_index;
93 GenericArray<Zeitgeist.Event> templates;
94
95- int display_count;
96+ int current_count;
97+ int deep_count;
98
99 bool local_search_finished = false;
100 bool global_search_finished = false;
101@@ -87,10 +124,11 @@
102 bool is_grabbing = false;
103 Gdk.Device? device = null;
104
105- Gtk.TreeIter local_results;
106- Gtk.TreeIter global_results;
107- Gtk.TreeIter bookmark_results;
108- Gtk.TreeIter no_results_label;
109+ Gtk.TreeIter? current_results = null;
110+ Gtk.TreeIter? deep_results = null;
111+ Gtk.TreeIter? zeitgeist_results = null;
112+ Gtk.TreeIter? bookmark_results = null;
113+
114 Gtk.TreeView view;
115 Gtk.TreeStore list;
116 Gtk.TreeModelFilter filter;
117@@ -126,10 +164,15 @@
118
119 view = new Gtk.TreeView ();
120 view.get_selection ().set_mode (Gtk.SelectionMode.BROWSE);
121+ /* Do not select category headers */
122+ view.get_selection ().set_select_function ((selection, list, path, path_selected) => {
123+ return path.get_depth () != 0;
124+ });
125 view.headers_visible = false;
126 view.show_expanders = false;
127 view.level_indentation = 12;
128- view.set_hover_selection (true);
129+ /* Revert to no hover select as the search window is now much larger */
130+ view.set_hover_selection (false);
131
132 get_style_context ().add_class ("completion-popup");
133
134@@ -153,16 +196,21 @@
135
136 view.append_column (column);
137
138- list = new Gtk.TreeStore (5, typeof (string), typeof (GLib.Icon),
139- typeof (string), typeof (File), typeof (bool));
140+ list = new Gtk.TreeStore (6,
141+ typeof (string), /*0 file basename or category name */
142+ typeof (GLib.Icon), /*1 file icon */
143+ typeof (string?), /*2 file location */
144+ typeof (File?), /*3 file object */
145+ typeof (bool), /*4 icon is visible */
146+ typeof (string)); /*5 Sort key */
147+
148 filter = new Gtk.TreeModelFilter (list, null);
149+
150 filter.set_visible_func ((model, iter) => {
151- if (iter == no_results_label)
152- return n_results < 1;
153-
154 /* hide empty category headers */
155 return list.iter_depth (iter) != 0 || list.iter_has_child (iter);
156 });
157+
158 view.model = filter;
159
160 list.row_changed.connect ((path, iter) => {
161@@ -174,12 +222,27 @@
162 }
163 });
164
165- list.append (out local_results, null);
166- list.@set (local_results, 0, get_category_header (_("In This Folder")));
167+ list.set_sort_column_id (5, Gtk.SortType.ASCENDING);
168+
169+ list.append (out current_results, null);
170+ list.@set (current_results,
171+ 0, get_category_header (_("In This Folder")),
172+ 5, SearchResults.category_to_string (Category.CURRENT_HEADER));
173+
174+ list.append (out deep_results, null);
175+ list.@set (deep_results,
176+ 0, get_category_header (_("Below This Folder")),
177+ 5, SearchResults.category_to_string (Category.CURRENT_HEADER));
178+
179 list.append (out bookmark_results, null);
180- list.@set (bookmark_results, 0, get_category_header (_("Bookmarks")));
181- list.append (out global_results, null);
182- list.@set (global_results, 0, get_category_header (_("Everywhere Else")));
183+ list.@set (bookmark_results,
184+ 0, get_category_header (_("Bookmarks")),
185+ 5, SearchResults.category_to_string (Category.CURRENT_HEADER));
186+
187+ list.append (out zeitgeist_results, null);
188+ list.@set (zeitgeist_results,
189+ 0, get_category_header (_("Recently used")),
190+ 5, SearchResults.category_to_string (Category.CURRENT_HEADER));
191
192 scroll.add (view);
193 frame.add (scroll);
194@@ -242,8 +305,8 @@
195 }
196
197 var include_hidden = GOF.Preferences.get_default ().pref_show_hidden_files;
198-
199- display_count = 0;
200+ current_count = 0;
201+ deep_count = 0;
202 directory_queue = new Gee.LinkedList<File> ();
203 waiting_results = new Gee.HashMap<Gtk.TreeIter?,Gee.List> ();
204 current_root = folder;
205@@ -277,7 +340,7 @@
206 new Thread<void*> (null, () => {
207 local_search_finished = false;
208 while (!file_search_operation.is_cancelled () && directory_queue.size > 0) {
209- visit (search_term, include_hidden, file_search_operation);
210+ visit (search_term, include_hidden, file_search_operation, folder);
211 }
212
213 local_search_finished = true;
214@@ -286,17 +349,17 @@
215 return null;
216 });
217
218- get_zg_results.begin (search_term);
219-
220- var bookmarks_matched = new Gee.LinkedList<Match> ();
221-
222- foreach (var bookmark in BookmarkList.get_instance ().list) {
223- if (term_matches (search_term, bookmark.label)) {
224- bookmarks_matched.add (new Match.from_bookmark (bookmark));
225- }
226+ get_zg_results.begin (search_term);
227+
228+ var bookmarks_matched = new Gee.LinkedList<Match> ();
229+ var begins_with = false;
230+ foreach (var bookmark in BookmarkList.get_instance ().list) {
231+ if (term_matches (search_term, bookmark.label, out begins_with)) {
232+ bookmarks_matched.add (new Match.from_bookmark (bookmark, begins_with ? Category.BOOKMARK_BEGINS : Category.BOOKMARK_CONTAINS));
233 }
234+ }
235
236- add_results (bookmarks_matched, bookmark_results);
237+ add_results (bookmarks_matched, bookmark_results);
238 }
239
240 /** Signal handlers **/
241@@ -648,15 +711,14 @@
242 return;
243 }
244
245- Gtk.TreeIter iter;
246- File file;
247-
248 foreach (var match in new_results) {
249- /* prevent results from showing in both global and local results */
250- if (parent == global_results) {
251+ Gtk.TreeIter? iter = null;
252+ File file;
253+ /* do not add global result if already in local results */
254+ if (parent == zeitgeist_results) {
255 var already_added = false;
256
257- for (var valid = list.iter_nth_child (out iter, local_results, 0); valid;
258+ for (var valid = list.iter_nth_child (out iter, current_results, 0); valid;
259 valid = list.iter_next (ref iter)) {
260
261 list.@get (iter, 3, out file);
262@@ -667,10 +729,25 @@
263 }
264 }
265
266- if (already_added)
267+ if (!already_added) {
268+ for (var valid = list.iter_nth_child (out iter, deep_results, 0); valid;
269+ valid = list.iter_next (ref iter)) {
270+
271+ list.@get (iter, 3, out file);
272+
273+ if (file != null && match.file != null && file.equal (match.file)) {
274+ already_added = true;
275+ break;
276+ }
277+ }
278+ }
279+
280+ if (already_added) {
281 continue;
282- } else if (parent == local_results) {
283- for (var valid = list.iter_nth_child (out iter, global_results, 0); valid;
284+ }
285+ } else if (parent == current_results) {
286+ /* remove current search result from global if in global results */
287+ for (var valid = list.iter_nth_child (out iter, zeitgeist_results, 0); valid;
288 valid = list.iter_next (ref iter)) {
289
290 list.@get (iter, 3, out file);
291@@ -680,13 +757,25 @@
292 break;
293 }
294 }
295- }
296+ } else if (parent == deep_results) {
297+ /* remove deep search result from from global if in global results */
298+ for (var valid = list.iter_nth_child (out iter, zeitgeist_results, 0); valid;
299+ valid = list.iter_next (ref iter)) {
300+
301+ list.@get (iter, 3, out file);
302+
303+ if (file != null && match.file != null && file.equal (match.file)) {
304+ list.remove (ref iter);
305+ break;
306+ }
307+ }
308+ }
309
310 var location = "<span %s>%s</span>".printf (get_pango_grey_color_string (),
311- Markup.escape_text (match.path_string));
312+ Markup.escape_text (match.path_string));
313
314 list.append (out iter, parent);
315- list.@set (iter, 0, Markup.escape_text (match.name), 1, match.icon, 2, location, 3, match.file, 4, true);
316+ list.@set (iter, 0, match.name, 1, match.icon, 2, location, 3, match.file, 4, true, 5, match.sortkey);
317 n_results++;
318
319 view.expand_all ();
320@@ -804,21 +893,23 @@
321 FileAttribute.STANDARD_TYPE + "," +
322 FileAttribute.STANDARD_ICON;
323
324- void visit (string term, bool include_hidden, Cancellable cancel)
325- {
326-
327- FileEnumerator enumerator;
328+ void visit (string term, bool include_hidden, Cancellable cancel, File root_folder) {
329 var folder = directory_queue.poll ();
330
331 if (folder == null) {
332 return;
333 }
334
335+ bool in_root = folder.equal (root_folder);
336+ var category_count = in_root ? current_count : deep_count;
337+
338 var depth = 0;
339
340 File f = folder;
341 var path_string = "";
342- while (!f.equal (current_root)) {
343+
344+
345+ while (f != null && !f.equal (current_root)) {
346 path_string = f.get_basename () + (path_string == "" ? "" : Path.DIR_SEPARATOR_S + path_string);
347 f = f.get_parent ();
348 depth++;
349@@ -828,6 +919,7 @@
350 return;
351 }
352
353+ FileEnumerator enumerator;
354 try {
355 enumerator = folder.enumerate_children (ATTRIBUTES, 0, cancel);
356 } catch (Error e) {
357@@ -837,8 +929,13 @@
358 var new_results = new Gee.LinkedList<Match> ();
359
360 FileInfo info = null;
361+ Category cat;
362+
363 try {
364- while (!cancel.is_cancelled () && (info = enumerator.next_file (null)) != null) {
365+ while (!cancel.is_cancelled () &&
366+ (info = enumerator.next_file (null)) != null &&
367+ category_count < MAX_RESULTS) {
368+
369 if (info.get_is_hidden () && !include_hidden) {
370 continue;
371 }
372@@ -847,43 +944,47 @@
373 directory_queue.add (folder.resolve_relative_path (info.get_name ()));
374 }
375
376- if (term_matches (term, info.get_display_name ())) {
377- new_results.add (new Match (info, path_string, folder));
378- }
379+ var begins_with = false;
380+ if (term_matches (term, info.get_display_name (), out begins_with)) {
381+ if (in_root) {
382+ cat = begins_with ? Category.CURRENT_BEGINS : Category.CURRENT_CONTAINS;
383+ } else {
384+ cat = begins_with ? Category.DEEP_BEGINS : Category.DEEP_CONTAINS;
385+ }
386+ new_results.add (new Match (info, path_string, folder, cat));
387+ category_count++;
388+ }
389 }
390 } catch (Error e) {warning ("Error enumerating in visit");}
391
392 if (new_results.size < 1) {
393+ cat = in_root ? Category.CURRENT_ELLIPSIS : Category.DEEP_ELLIPSIS;
394+ new_results.add (new Match.ellipsis (cat));
395 return;
396- }
397-
398- if (!cancel.is_cancelled ()) {
399- var new_count = display_count + new_results.size;
400- if (new_count > MAX_RESULTS) {
401- cancel.cancel ();
402-
403- var num_ok = MAX_RESULTS - display_count;
404- if (num_ok < new_results.size) {
405- var count = 0;
406- var it = new_results.iterator ();
407- while (it.next ()) {
408- count++;
409- if (count > num_ok)
410- it.remove ();
411- }
412- }
413-
414- new_results.add (new Match.ellipsis ());
415-
416- display_count = MAX_RESULTS;
417- } else
418- display_count = new_count;
419+ } else if (!cancel.is_cancelled ()) {
420+ if (in_root) {
421+ current_count = category_count;
422+ } else {
423+ deep_count = category_count;
424+ }
425
426 /* use a closure here to get vala to pass the userdata that we actually want */
427 Idle.add (() => {
428- add_results (new_results, local_results);
429+ add_results (new_results, in_root ? current_results : deep_results);
430 return false;
431 });
432+
433+
434+ if (category_count >= MAX_RESULTS) {
435+ cat = in_root ? Category.CURRENT_ELLIPSIS : Category.DEEP_ELLIPSIS;
436+ new_results.add (new Match.ellipsis (cat));
437+ return;
438+ }
439+
440+
441+ if (current_count >= MAX_RESULTS && deep_count >= MAX_RESULTS) {
442+ cancel.cancel ();
443+ }
444 }
445 }
446
447@@ -893,11 +994,11 @@
448
449 Zeitgeist.ResultSet results;
450 try {
451- results = yield zg_index.search (term,
452+ results = yield zg_index.search ("name:" + term + "*",
453 new Zeitgeist.TimeRange.anytime (),
454 templates,
455 0, /* offset */
456- MAX_RESULTS + 1,
457+ MAX_RESULTS * 3,
458 Zeitgeist.ResultType.MOST_POPULAR_SUBJECTS,
459 current_operation);
460 } catch (IOError.CANCELLED e) {
461@@ -911,57 +1012,81 @@
462 return;
463 }
464
465- var i = 0;
466-
467 var matches = new Gee.LinkedList<Match> ();
468 var home = File.new_for_path (Environment.get_home_dir ());
469- while (results.has_next () && !current_operation.is_cancelled ()) {
470+ Category cat;
471+ var i = 0;
472+ while (results.has_next () && !current_operation.is_cancelled () && !global_search_finished) {
473+
474 var result = results.next_value ();
475 foreach (var subject in result.subjects.data) {
476 if (i == MAX_RESULTS) {
477- matches.add (new Match.ellipsis ());
478+ matches.add (new Match.ellipsis (Category.ZEITGEIST_ELLIPSIS));
479+ global_search_finished = true;
480 break;
481 }
482
483 try {
484 var file = File.new_for_uri (subject.uri);
485- var path_string = "";
486- var parent = file;
487- while ((parent = parent.get_parent ()) != null) {
488- if (parent.equal (current_root))
489- break;
490-
491- if (parent.equal (home)) {
492- path_string = "~/" + path_string;
493- break;
494- }
495-
496- if (path_string == "")
497- path_string = parent.get_basename ();
498- else
499- path_string = parent.get_basename () + Path.DIR_SEPARATOR_S + path_string;
500- }
501-
502- var info = yield file.query_info_async (ATTRIBUTES, 0, Priority.DEFAULT, current_operation);
503- matches.add (new Match (info, path_string, file.get_parent ()));
504-
505- i++;
506+ /* Zeitgeist search finds search term anywhere in path. We are only interested
507+ * when the search term is in the basename */
508+ while (file != null && !file.get_basename ().contains (term)) {
509+ file = file.get_parent ();
510+ }
511+ if (file != null) {
512+ var path_string = "";
513+ var parent = file;
514+ while ((parent = parent.get_parent ()) != null) {
515+ if (parent.equal (current_root)) {
516+ break;
517+ }
518+
519+ if (parent.equal (home)) {
520+ path_string = "~/" + path_string;
521+ break;
522+ }
523+
524+ if (path_string == "") {
525+ path_string = parent.get_basename ();
526+ } else {
527+ path_string = parent.get_basename () + Path.DIR_SEPARATOR_S + path_string;
528+ }
529+ }
530+
531+ /* Eliminate duplicate matches */
532+ bool found = false;
533+ foreach (Match m in matches) {
534+ if (m.path_string == path_string) {
535+ found = true;
536+ break;
537+ }
538+ }
539+
540+ if (!found) {
541+ var info = yield file.query_info_async (ATTRIBUTES, 0, Priority.DEFAULT, current_operation);
542+ var name = info.get_display_name ();
543+ cat = name.has_prefix (term) ? Category.ZEITGEIST_BEGINS : Category.ZEITGEIST_CONTAINS;
544+ matches.add (new Match (info, path_string, file.get_parent (), cat));
545+ i++;
546+ }
547+ }
548 } catch (Error e) {}
549 }
550 }
551
552- if (!current_operation.is_cancelled ())
553- add_results (matches, global_results);
554+ if (!current_operation.is_cancelled ()) {
555+ add_results (matches, zeitgeist_results);
556+ }
557
558 global_search_finished = true;
559 Idle.add (send_search_finished);
560 }
561
562- bool term_matches (string term, string name)
563- {
564- /**TODO** improve */
565+ bool term_matches (string term, string name, out bool begins_with ) {
566 /* term is assumed to be down */
567- return name.normalize ().casefold ().contains (term);
568+ var n = name.normalize ().casefold ();
569+ begins_with = n.has_prefix (term);
570+ return n.contains (term);
571 }
572
573 string get_category_header (string title)

Subscribers

People subscribed via source and target branches

to all changes: