[dconf-editor] Add search results view



commit 201534436a47e33f15c730063475ede61bf90b41
Author: Davi da Silva Böger <dsboger gmail com>
Date:   Thu Nov 23 19:25:32 2017 -0200

    Add search results view

 editor/browser-view.ui            |   10 +
 editor/browser-view.vala          |   82 ++++--
 editor/dconf-editor.gresource.xml |    1 +
 editor/dconf-editor.ui            |   58 ++++-
 editor/dconf-model.vala           |    4 +-
 editor/dconf-window.vala          |   92 ++++--
 editor/key-list-box-row.vala      |   43 ++-
 editor/meson.build                |    2 +
 editor/registry-search.ui         |   32 ++
 editor/registry-search.vala       |  599 +++++++++++++++++++++++++++++++++++++
 editor/registry-view.vala         |    5 +
 11 files changed, 865 insertions(+), 63 deletions(-)
---
diff --git a/editor/browser-view.ui b/editor/browser-view.ui
index 83bea2b..8199245 100644
--- a/editor/browser-view.ui
+++ b/editor/browser-view.ui
@@ -63,6 +63,16 @@
             <property name="name">properties-view</property>
           </packing>
         </child>
+        <child>
+          <object class="RegistrySearch"  id="search_results_view">
+            <property name="visible">True</property>
+            <property name="revealer">revealer</property>
+            <signal name="request_path" handler="request_path_test"/>
+          </object>
+          <packing>
+            <property name="name">search-results-view</property>
+          </packing>
+        </child>
       </object>
     </child>
     <child>
diff --git a/editor/browser-view.vala b/editor/browser-view.vala
index fbddf58..7b7c201 100644
--- a/editor/browser-view.vala
+++ b/editor/browser-view.vala
@@ -38,8 +38,10 @@ class BrowserView : Grid, PathElement
     [GtkChild] private Stack stack;
     [GtkChild] private RegistryView browse_view;
     [GtkChild] private RegistryInfo properties_view;
+    [GtkChild] private RegistrySearch search_results_view;
+    private Widget? pre_search_view = null;
 
-    private SortingOptions sorting_options;
+    public SortingOptions sorting_options { get; private set; }
 
     public bool small_keys_list_rows
     {
@@ -55,7 +57,7 @@ class BrowserView : Grid, PathElement
     private DConfWindow window {
         get {
             if (_window == null)
-                _window = (DConfWindow) DConfWindow._get_parent (DConfWindow._get_parent (this));
+                _window = (DConfWindow) DConfWindow._get_parent (DConfWindow._get_parent 
(DConfWindow._get_parent (this)));
             return (!) _window;
         }
     }
@@ -73,8 +75,9 @@ class BrowserView : Grid, PathElement
         settings.bind ("sort-folders", sorting_options, "sort-folders", GLib.SettingsBindFlags.GET);
 
         sorting_options.notify.connect (() => {
-                if (!is_not_browsing_view () && current_directory.need_sorting (sorting_options))
+                if (current_view_is_browse_view () && current_directory.need_sorting (sorting_options))
                     need_reload_warning_revealer.set_reveal_child (true);
+                // TODO reload search results too
             });
 
         destroy.connect (() => {
@@ -113,7 +116,8 @@ class BrowserView : Grid, PathElement
     }
     private void _show_browse_view (string path)
     {
-        stack.set_transition_type (current_path.has_prefix (path) ? StackTransitionType.CROSSFADE : 
StackTransitionType.NONE);
+        stack.set_transition_type (current_path.has_prefix (path) && pre_search_view == null ? 
StackTransitionType.CROSSFADE : StackTransitionType.NONE);
+        pre_search_view = null;
         need_reload_warning_revealer.set_reveal_child (false);
         browse_view.show_multiple_schemas_warning (current_directory.warning_multiple_schemas);
 
@@ -136,11 +140,34 @@ class BrowserView : Grid, PathElement
         need_reload_warning_revealer.set_reveal_child (false);
         browse_view.show_multiple_schemas_warning (false);
 
-        stack.set_transition_type (path.has_prefix (current_path) && current_path.length == 
path.last_index_of_char ('/') + 1 ? StackTransitionType.CROSSFADE : StackTransitionType.NONE);
+        stack.set_transition_type (path.has_prefix (current_path) && current_path.length == 
path.last_index_of_char ('/') + 1 && pre_search_view == null ? StackTransitionType.CROSSFADE : 
StackTransitionType.NONE);
+        pre_search_view = null;
         update_current_path (path);
         stack.set_visible_child (properties_view);
     }
 
+    public void show_search_view (string term)
+    {
+        search_results_view.start_search (term);
+        if (pre_search_view == null)
+        {
+            pre_search_view = stack.visible_child;
+            stack.set_transition_type (StackTransitionType.NONE);
+            stack.visible_child = search_results_view;
+        }
+    }
+
+    public void hide_search_view ()
+    {
+        if (pre_search_view != null)
+        {
+            stack.set_transition_type (StackTransitionType.NONE);
+            stack.visible_child = (!) pre_search_view;
+            pre_search_view = null;
+        }
+        search_results_view.stop_search ();
+    }
+
     private void update_current_path (string path)
     {
         revealer.path_changed ();
@@ -161,42 +188,57 @@ class BrowserView : Grid, PathElement
 
     public bool show_row_popover ()
     {
-        if (is_not_browsing_view ())
-            return false;
-        return browse_view.show_row_popover ();
+        if (current_view_is_browse_view ())
+            return browse_view.show_row_popover ();
+        if (current_view_is_search_results_view ())
+            return search_results_view.show_row_popover ();
+        return false;
     }
 
     public void toggle_boolean_key ()
     {
-        if (is_not_browsing_view ())
-            return;                         // TODO something, probably
-        browse_view.toggle_boolean_key ();
+        if (current_view_is_browse_view ())
+            browse_view.toggle_boolean_key ();
+        else if (current_view_is_search_results_view ())
+            search_results_view.toggle_boolean_key ();
     }
 
     public void set_to_default ()
     {
-        if (is_not_browsing_view ())
-            return;
-        browse_view.set_to_default ();
+        if (current_view_is_browse_view ())
+            browse_view.set_to_default ();
+        else if (current_view_is_search_results_view ())
+            search_results_view.set_to_default ();
     }
 
     public void discard_row_popover ()
     {
-        if (is_not_browsing_view ())
-            return;
-        browse_view.discard_row_popover ();
+        if (current_view_is_browse_view ())
+            browse_view.discard_row_popover ();
+        else if (current_view_is_search_results_view ())
+            search_results_view.discard_row_popover ();
     }
 
     private void invalidate_popovers ()
     {
         browse_view.invalidate_popovers ();
+        search_results_view.invalidate_popovers ();
         window.update_hamburger_menu ();
     }
 
-    private bool is_not_browsing_view ()
+    public bool current_view_is_browse_view ()
+    {
+        return stack.get_visible_child () == browse_view;
+    }
+
+    public bool current_view_is_properties_view ()
+    {
+        return stack.get_visible_child () == properties_view;
+    }
+
+    public bool current_view_is_search_results_view ()
     {
-        string? visible_child_name = stack.get_visible_child_name ();
-        return (visible_child_name == null || ((!) visible_child_name) != "browse-view");
+        return stack.get_visible_child () == search_results_view;
     }
 
     /*\
diff --git a/editor/dconf-editor.gresource.xml b/editor/dconf-editor.gresource.xml
index 186906b..5e240a8 100644
--- a/editor/dconf-editor.gresource.xml
+++ b/editor/dconf-editor.gresource.xml
@@ -14,6 +14,7 @@
     <file preprocess="xml-stripblanks">property-row.ui</file>
     <file preprocess="xml-stripblanks">registry-info.ui</file>
     <file preprocess="xml-stripblanks">registry-placeholder.ui</file>
+    <file preprocess="xml-stripblanks">registry-search.ui</file>
     <file preprocess="xml-stripblanks">registry-view.ui</file>
   </gresource>
   <gresource prefix="/ca/desrt/dconf-editor/gtk">
diff --git a/editor/dconf-editor.ui b/editor/dconf-editor.ui
index bd7889b..0f6f982 100644
--- a/editor/dconf-editor.ui
+++ b/editor/dconf-editor.ui
@@ -65,9 +65,10 @@
         </child>
         <child>
           <object class="GtkToggleButton">
-            <property name="visible">False</property>
+            <property name="visible">True</property>
             <property name="valign">center</property>
-            <!-- TODO <property name="focus-on-click">False</property> does not work here because of 
explicit focus grab -->
+            <property name="active" bind-source="search_bar" bind-property="search-mode-enabled" 
bind-flags="bidirectional">False</property>
+            <property name="focus-on-click">False</property>
             <!-- <accelerator key="F" signal="toggled" modifiers="GDK_CONTROL_MASK"/> TODO -->
             <style>
               <class name="image-button"/>
@@ -113,9 +114,58 @@
       <object class="GtkOverlay">
         <property name="visible">True</property>
         <child>
-          <object class="BrowserView" id="browser_view">
+          <object class="GtkGrid">
             <property name="visible">True</property>
-            <signal name="request_path" handler="request_path"/>
+            <property name="orientation">vertical</property>
+            <child>
+              <object class="GtkRevealer">
+                <property name="visible">True</property>
+                <property name="reveal-child" bind-source="search_bar" bind-property="search-mode-enabled" 
bind-flags="bidirectional">False</property>
+                <child>
+                  <object class="GtkSearchBar" id="search_bar">
+                    <property name="visible">True</property>
+                    <property name="search-mode-enabled">False</property>
+                    <property name="show-close-button">False</property>
+                    <child>
+                      <object class="GtkBox"> <!-- https://bugzilla.gnome.org/show_bug.cgi?id=769876 -->
+                        <property name="visible">True</property>
+                        <property name="orientation">horizontal</property>
+                        <style>
+                          <class name="linked"/>
+                        </style>
+                        <child>
+                          <object class="GtkSearchEntry" id="search_entry">
+                            <property name="visible">True</property>
+                            <property name="width-request">350</property>
+                            <signal name="search-changed" handler="search_changed"/>
+                            <signal name="stop-search" handler="search_cancelled"/>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkButton" id="search_options_button">
+                            <property name="visible">False</property>
+                            <property name="sensitive" bind-source="search_bar" 
bind-property="search-mode-enabled"/>
+                            <child>
+                              <object class="GtkImage">
+                                <property name="visible">True</property>
+                                <property name="icon-name">pan-down-symbolic</property>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+             </object>
+            </child>
+            <child>
+              <object class="BrowserView" id="browser_view">
+                <property name="visible">True</property>
+                <property name="vexpand">True</property>
+                <signal name="request_path" handler="request_path"/>
+              </object>
+            </child>
           </object>
         </child>
         <child type="overlay">
diff --git a/editor/dconf-model.vala b/editor/dconf-model.vala
index 2508c73..5992ec0 100644
--- a/editor/dconf-model.vala
+++ b/editor/dconf-model.vala
@@ -728,7 +728,7 @@ public class SortingOptions : Object
     public bool case_sensitive { get; set; default = false; }
     public MergeType sort_folders { get; set; default = MergeType.MIXED; }
 
-    private SettingComparator get_comparator ()
+    public SettingComparator get_comparator ()
     {
         if (sort_folders == MergeType.FIRST)
         {
@@ -778,7 +778,7 @@ public class SortingOptions : Object
 
 /* Comparison functions */
 
-interface SettingComparator : Object
+public interface SettingComparator : Object
 {
     public abstract int compare (SettingObject a, SettingObject b);
 }
diff --git a/editor/dconf-window.vala b/editor/dconf-window.vala
index 12295fe..9ce224c 100644
--- a/editor/dconf-window.vala
+++ b/editor/dconf-window.vala
@@ -29,9 +29,9 @@ class DConfWindow : ApplicationWindow
         { "enter-delay-mode", enter_delay_mode }
     };
 
-    public string current_path { private get; set; default = "/"; } // not synced bidi, needed for saving on 
destroy, even after child destruction
+    public string current_path { get; set; default = "/"; } // not synced bidi, needed for saving on 
destroy, even after child destruction
 
-    private SettingsModel model = new SettingsModel ();
+    public SettingsModel model { get; private set; default=new SettingsModel (); }
 
     private int window_width = 0;
     private int window_height = 0;
@@ -47,6 +47,9 @@ class DConfWindow : ApplicationWindow
     [GtkChild] private Bookmarks bookmarks_button;
     [GtkChild] private MenuButton info_button;
     [GtkChild] private PathBar pathbar;
+    [GtkChild] private SearchBar search_bar;
+    [GtkChild] private SearchEntry search_entry;
+
     [GtkChild] private BrowserView browser_view;
 
     [GtkChild] private Revealer notification_revealer;
@@ -111,6 +114,9 @@ class DConfWindow : ApplicationWindow
         if (settings.get_boolean ("small-bookmarks-rows"))
             context.add_class ("small-bookmarks-rows");
 
+        search_bar.connect_entry (search_entry);
+        search_bar.notify ["search-mode-enabled"].connect (search_changed);
+
         browser_view.bind_property ("current-path", this, "current-path");    // TODO in UI file?
 
         settings.bind ("mouse-use-extra-buttons", this, "mouse-extra-buttons", 
SettingsBindFlags.GET|SettingsBindFlags.NO_SENSITIVITY);
@@ -168,6 +174,11 @@ class DConfWindow : ApplicationWindow
         return (!) parent;
     }
 
+    public string[] get_bookmarks ()
+    {
+        return settings.get_strv ("bookmarks");
+    }
+
     /*\
     * * Window management callbacks
     \*/
@@ -265,27 +276,25 @@ class DConfWindow : ApplicationWindow
 
         Directory? dir = model.get_directory (folder_name);
         if (dir == null)
-        {
             cannot_find_folder (folder_name);
-            return;
-        }
-        if (full_name == folder_name)
-        {
+        else if (full_name == folder_name)
             browser_view.set_directory ((!) dir, pathbar.get_selected_child (full_name));
-            return;
-        }
+        else
+        {
+            string [] names = full_name.split ("/");
+            string object_name = names [names.length - 1];
 
-        string [] names = full_name.split ("/");
-        string object_name = names [names.length - 1];
+            Key? existing_key = SettingsModel.get_key_from_path_and_name (((!) dir).key_model, object_name);
 
-        Key? existing_key = SettingsModel.get_key_from_path_and_name (((!) dir).key_model, object_name);
+            if (existing_key == null)
+                cannot_find_key (object_name, (!) dir);
+            else if (((!) existing_key) is DConfKey && ((DConfKey) (!) existing_key).is_ghost)
+                key_has_been_removed (object_name, (!) dir);
+            else
+                browser_view.show_properties_view ((Key) (!) existing_key, full_name, ((!) 
dir).warning_multiple_schemas);
+        }
 
-        if (existing_key == null)
-            cannot_find_key (object_name, (!) dir);
-        else if (((!) existing_key) is DConfKey && ((DConfKey) (!) existing_key).is_ghost)
-            key_has_been_removed (object_name, (!) dir);
-        else
-            browser_view.show_properties_view ((Key) (!) existing_key, full_name, ((!) 
dir).warning_multiple_schemas);
+        search_bar.search_mode_enabled = false; // do last to avoid flickering RegistryView before 
PropertiesView when selecting a search result
     }
 
     /*\
@@ -352,6 +361,25 @@ class DConfWindow : ApplicationWindow
     }
 
     /*\
+    * * Search
+    \*/
+
+    [GtkCallback]
+    private void search_changed ()
+    {
+        if (search_bar.search_mode_enabled)
+            browser_view.show_search_view (search_entry.text);
+        else
+            browser_view.hide_search_view ();
+    }
+
+    [GtkCallback]
+    private void search_cancelled ()
+    {
+        browser_view.hide_search_view ();
+    }
+
+    /*\
     * * Other callbacks
     \*/
 
@@ -377,6 +405,11 @@ class DConfWindow : ApplicationWindow
     [GtkCallback]
     private bool on_key_press_event (Widget widget, Gdk.EventKey event)     // TODO better?
     {
+        Widget? focus = get_focus ();
+        if (!(focus is Entry) && !(focus is TextView)) // why is this needed?
+            if (search_bar.handle_event (event))
+                return true;
+
         string name = (!) (Gdk.keyval_name (event.keyval) ?? "");
 
         if ((event.state & Gdk.ModifierType.CONTROL_MASK) != 0)
@@ -401,14 +434,19 @@ class DConfWindow : ApplicationWindow
                     browser_view.discard_row_popover ();
                     bookmarks_button.set_bookmarked (false);
                     return true;
-//                case "f":
-//                    if (bookmarks_button.active)
-//                        bookmarks_button.active = false;
-//                    if (info_button.active)
-//                        info_button.active = false;
-//                    browser_view.discard_row_popover ();
-//                    browser_view.set_search_mode (null);
-//                    return true;
+                case "f":
+                    if (bookmarks_button.active)
+                        bookmarks_button.active = false;
+                    if (info_button.active)
+                        info_button.active = false;
+                    browser_view.discard_row_popover ();
+                    if (!search_bar.search_mode_enabled)
+                        search_bar.search_mode_enabled = true;
+                    else if (!search_entry.has_focus)
+                        search_entry.grab_focus ();
+                    else
+                        search_bar.search_mode_enabled = false;
+                    return true;
                 case "c":
                     browser_view.discard_row_popover (); // TODO avoid duplicate get_selected_row () call
                     string? selected_row_text = browser_view.get_copy_text ();
@@ -429,7 +467,6 @@ class DConfWindow : ApplicationWindow
                 case "KP_Enter":
                     if (info_button.active || bookmarks_button.active)
                         return false;
-//                    browser_view.set_search_mode (false);
                     browser_view.discard_row_popover ();
                     browser_view.toggle_boolean_key ();
                     return true;
@@ -441,7 +478,6 @@ class DConfWindow : ApplicationWindow
                 case "KP_Decimal":
                     if (info_button.active || bookmarks_button.active)
                         return false;
-//                    browser_view.set_search_mode (false);
                     browser_view.discard_row_popover ();
                     browser_view.set_to_default ();
                     return true;
diff --git a/editor/key-list-box-row.vala b/editor/key-list-box-row.vala
index 1130681..003adcd 100644
--- a/editor/key-list-box-row.vala
+++ b/editor/key-list-box-row.vala
@@ -28,7 +28,7 @@ private class ListBoxRowWrapper : ListBoxRow
     }
 }
 
-private class ListBoxRowHeader : Separator
+private class ListBoxRowHeader : Grid
 {
     public override void get_preferred_width (out int minimum_width, out int natural_width)
     {
@@ -40,10 +40,13 @@ private class ListBoxRowHeader : Separator
 private abstract class ClickableListBoxRow : EventBox
 {
     public signal void on_row_clicked ();
+    public signal void on_open_parent ();
     public signal void on_delete_call ();
 
     public abstract string get_text ();
 
+    public bool search_result_mode { protected get; construct; default=false; }
+
     /*\
     * * Dismiss popover on window resize
     \*/
@@ -112,11 +115,12 @@ private abstract class ClickableListBoxRow : EventBox
 private class FolderListBoxRow : ClickableListBoxRow
 {
     [GtkChild] private Label folder_name_label;
-    private string full_name;
+    public string full_name;
 
-    public FolderListBoxRow (string label, string path)
+    public FolderListBoxRow (string label, string path, bool search_result_mode=false)
     {
-        folder_name_label.set_text (label);
+        Object (search_result_mode: search_result_mode);
+        folder_name_label.set_text (search_result_mode ? path : label);
         full_name = path;
     }
 
@@ -127,6 +131,12 @@ private class FolderListBoxRow : ClickableListBoxRow
 
     protected override bool generate_popover (ContextPopover popover, bool unused)  // TODO better
     {
+        if (search_result_mode)
+        {
+            popover.new_action ("open_parent", () => on_open_parent ());
+            popover.new_section ();
+        }
+
         popover.new_action ("open", () => on_row_clicked ());
         popover.new_copy_action (get_text ());
 
@@ -190,7 +200,7 @@ private abstract class KeyListBoxRow : ClickableListBoxRow
         }
 
         update ();
-        key_name_label.set_label (abstract_key.name);
+        key_name_label.set_label (search_result_mode ? abstract_key.full_name : abstract_key.name);
 
         ulong key_value_changed_handler = abstract_key.value_changed.connect (() => {
                 update ();
@@ -214,9 +224,9 @@ private class KeyListBoxRowEditableNoSchema : KeyListBoxRow
     public DConfKey key { get; construct; }
     private override Key abstract_key { get { return (Key) key; }}
 
-    public KeyListBoxRowEditableNoSchema (DConfKey _key)
+    public KeyListBoxRowEditableNoSchema (DConfKey _key, bool search_result_mode=false)
     {
-        Object (key: _key);
+        Object (key: _key, search_result_mode : search_result_mode);
 
         if (boolean_switch != null)
             ((!) boolean_switch).notify ["active"].connect (() => key.value = new Variant.boolean (((!) 
boolean_switch).get_active ()));
@@ -269,6 +279,12 @@ private class KeyListBoxRowEditableNoSchema : KeyListBoxRow
             return true;
         }
 
+        if (search_result_mode)
+        {
+            popover.new_action ("open_parent", () => on_open_parent ());
+            popover.new_section ();
+        }
+
         popover.new_action ("customize", () => on_row_clicked ());
         popover.new_copy_action (get_text ());
 
@@ -326,9 +342,9 @@ private class KeyListBoxRowEditable : KeyListBoxRow
     private override Key abstract_key { get { return (Key) key; }}
     private ulong boolean_switch_toggled_handler = 0;
 
-    public KeyListBoxRowEditable (GSettingsKey _key)
+    public KeyListBoxRowEditable (GSettingsKey _key, bool search_result_mode=false)
     {
-        Object (key: _key);
+        Object (key: _key, search_result_mode : search_result_mode);
 
         if (boolean_switch != null)
             boolean_switch_toggled_handler = ((!) boolean_switch).notify ["active"].connect (() => {
@@ -385,6 +401,12 @@ private class KeyListBoxRowEditable : KeyListBoxRow
 
     protected override bool generate_popover (ContextPopover popover, bool delayed_apply_menu)
     {
+        if (search_result_mode)
+        {
+            popover.new_action ("open_parent", () => on_open_parent ());
+            popover.new_section ();
+        }
+
         popover.new_action ("customize", () => on_row_clicked ());
         popover.new_copy_action (get_text ());
 
@@ -511,6 +533,9 @@ private class ContextPopover : Popover
             case "open":
                 /* Translators: "open folder" action in the right-click menu on a folder */
                 current_section.append (_("Open"), group_dot_action);               return;
+            case "open_parent":
+                /* Translators: "open parent folder" action in the right-click menu on a folder in a search 
result */
+                current_section.append (_("Open parent folder"), group_dot_action);               return;
             case "erase":
                 /* Translators: "erase key" action in the right-click menu on a key without schema */
                 current_section.append (_("Erase key"), group_dot_action);          return;
diff --git a/editor/meson.build b/editor/meson.build
index bcc14ae..1f5c55e 100644
--- a/editor/meson.build
+++ b/editor/meson.build
@@ -61,6 +61,7 @@ sources = files(
   'pathbar.vala',
   'registry-info.vala',
   'registry-placeholder.vala',
+  'registry-search.vala',
   'registry-view.vala'
 )
 
@@ -80,6 +81,7 @@ resource_data = files(
   'property-row.ui',
   'registry-info.ui',
   'registry-placeholder.ui',
+  'registry-search.ui',
   'registry-view.ui'
 )
 
diff --git a/editor/registry-search.ui b/editor/registry-search.ui
new file mode 100644
index 0000000..247e7e0
--- /dev/null
+++ b/editor/registry-search.ui
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.0 -->
+  <template class="RegistrySearch" parent="GtkGrid">
+    <property name="orientation">vertical</property>
+    <child>
+      <object class="GtkScrolledWindow" id="scrolled">
+        <property name="visible">True</property>
+        <property name="expand">True</property>
+        <child>
+          <object class="GtkListBox" id="key_list_box">
+            <property name="visible">True</property>
+            <property name="activate-on-single-click">True</property>
+            <property name="selection-mode">browse</property>
+            <style>
+              <class name="keys-list"/>
+              <class name="search-results-list"/>
+            </style>
+            <signal name="row-activated" handler="row_activated_cb"/>
+            <child type="placeholder">
+              <object class="RegistryPlaceholder">
+                <property name="label" translatable="yes">No matches</property>
+                <property name="icon-name">ca.desrt.dconf-editor-symbolic</property>
+                <property name="big">True</property>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/editor/registry-search.vala b/editor/registry-search.vala
new file mode 100644
index 0000000..47278cb
--- /dev/null
+++ b/editor/registry-search.vala
@@ -0,0 +1,599 @@
+/*
+  This file is part of Dconf Editor
+
+  Dconf Editor is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  Dconf Editor is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with Dconf Editor.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+using Gtk;
+
+[GtkTemplate (ui = "/ca/desrt/dconf-editor/ui/registry-search.ui")]
+class RegistrySearch : Grid, PathElement, BrowsableView
+{
+    public Behaviour behaviour { private get; set; }
+
+    //[GtkChild] private ScrolledWindow scrolled;
+
+    [GtkChild] private ListBox key_list_box;
+
+    private GLib.ListStore rows_possibly_with_popover = new GLib.ListStore (typeof (ClickableListBoxRow));
+
+    private bool _small_keys_list_rows;
+    public bool small_keys_list_rows
+    {
+        set
+        {
+            _small_keys_list_rows = value;
+            key_list_box.foreach((row) => {
+                    Widget row_child = ((ListBoxRow) row).get_child ();
+                    if (row_child is KeyListBoxRow)
+                        ((KeyListBoxRow) row_child).small_keys_list_rows = value;
+                });
+        }
+    }
+
+    public ModificationsRevealer revealer { private get; set; }
+
+    private BrowserView? _browser_view = null;
+    private BrowserView browser_view {
+        get {
+            if (_browser_view == null)
+                _browser_view = (BrowserView) DConfWindow._get_parent (DConfWindow._get_parent (this));
+            return (!) _browser_view;
+        }
+    }
+
+    private DConfWindow? _window = null;
+    private DConfWindow window {
+        get {
+            if (_window == null)
+                _window = (DConfWindow) DConfWindow._get_parent (DConfWindow._get_parent 
(DConfWindow._get_parent (browser_view)));
+            return (!) _window;
+        }
+    }
+
+    private GLib.ListStore search_results_model = new GLib.ListStore (typeof (SettingObject));
+
+    construct
+    {
+        key_list_box.set_header_func (update_search_results_header);
+    }
+
+    /*\
+    * * Updating
+    \*/
+/*
+    public void select_row_named (string selected, bool grab_focus)
+    {
+        check_resize ();
+        ListBoxRow? row = key_list_box.get_row_at_index (get_row_position (selected));
+        if (row == null)
+            assert_not_reached ();
+        scroll_to_row ((!) row, grab_focus);
+    }
+    public void select_first_row (bool grab_focus)
+    {
+        ListBoxRow? row = key_list_box.get_row_at_index (0);
+        if (row != null)
+            scroll_to_row ((!) row, grab_focus);
+    }
+    private int get_row_position (string selected)
+        requires (key_model != null)
+    {
+        uint position = 0;
+        while (position < ((!) key_model).get_n_items ())
+        {
+            SettingObject object = (SettingObject) ((!) key_model).get_object (position);
+            if (object.full_name == selected)
+                return (int) position;
+            position++;
+        }
+        assert_not_reached ();
+    }
+    private void scroll_to_row (ListBoxRow row, bool grab_focus)
+    {
+        key_list_box.select_row (row);
+        if (grab_focus)
+            row.grab_focus ();
+
+        Allocation list_allocation, row_allocation;
+        scrolled.get_allocation (out list_allocation);
+        row.get_allocation (out row_allocation);
+        key_list_box.get_adjustment ().set_value (row_allocation.y + (int) ((row_allocation.height - 
list_allocation.height) / 2.0));
+    }
+/*
+    /*\
+    * * Key ListBox
+    \*/
+
+    private Widget new_list_box_row (Object item)
+    {
+        ClickableListBoxRow row;
+        SettingObject setting_object = (SettingObject) item;
+        string full_name = setting_object.full_name;
+        string parent_path;
+        if (full_name.has_suffix ("/"))
+            parent_path = SettingsModel.get_base_path (full_name [0:full_name.length - 1]);
+        else
+            parent_path = SettingsModel.get_base_path (full_name);
+        bool is_local_result = parent_path == window.current_path;
+        ulong on_delete_call_handler;
+
+        if (setting_object is Directory)
+        {
+            row = new FolderListBoxRow (setting_object.name, setting_object.full_name, !is_local_result);
+            on_delete_call_handler = row.on_delete_call.connect (() => browser_view.reset_objects 
(((Directory) setting_object).key_model, true));
+        }
+        else
+        {
+            if (setting_object is GSettingsKey)
+                row = new KeyListBoxRowEditable ((GSettingsKey) setting_object, !is_local_result);
+            else
+                row = new KeyListBoxRowEditableNoSchema ((DConfKey) setting_object, !is_local_result);
+
+            Key key = (Key) setting_object;
+            KeyListBoxRow key_row = (KeyListBoxRow) row;
+            key_row.small_keys_list_rows = _small_keys_list_rows;
+
+            on_delete_call_handler = row.on_delete_call.connect (() => set_key_value (key, null));
+            ulong set_key_value_handler = key_row.set_key_value.connect ((variant) => { set_key_value (key, 
variant); set_delayed_icon (row, key); });
+            ulong change_dismissed_handler = key_row.change_dismissed.connect (() => revealer.dismiss_change 
(key));
+
+            ulong key_planned_change_handler = key.notify ["planned-change"].connect (() => set_delayed_icon 
(row, key));
+            ulong key_planned_value_handler = key.notify ["planned-value"].connect (() => set_delayed_icon 
(row, key));
+            set_delayed_icon (row, key);
+
+            row.destroy.connect (() => {
+                    key_row.disconnect (set_key_value_handler);
+                    key_row.disconnect (change_dismissed_handler);
+                    key.disconnect (key_planned_change_handler);
+                    key.disconnect (key_planned_value_handler);
+                });
+        }
+
+        ulong on_row_clicked_handler = row.on_row_clicked.connect (() => request_path 
(setting_object.full_name));
+        ulong on_row_open_parent_handler = row.on_open_parent.connect (() => {
+                request_path (parent_path); // TODO selected
+            });
+        ulong button_press_event_handler = row.button_press_event.connect (on_button_pressed);
+
+        row.destroy.connect (() => {
+                row.disconnect (on_delete_call_handler);
+                row.disconnect (on_row_clicked_handler);
+                row.disconnect (on_row_open_parent_handler);
+                row.disconnect (button_press_event_handler);
+            });
+
+        /* Wrapper ensures max width for rows */
+        ListBoxRowWrapper wrapper = new ListBoxRowWrapper ();
+        wrapper.set_halign (Align.CENTER);
+        wrapper.add (row);
+        if (row is FolderListBoxRow)
+            wrapper.get_style_context ().add_class ("folder-row");
+        else
+            wrapper.get_style_context ().add_class ("key-row");
+        return wrapper;
+    }
+
+    private void set_delayed_icon (ClickableListBoxRow row, Key key)
+    {
+        if (key.planned_change)
+        {
+            StyleContext context = row.get_style_context ();
+            context.add_class ("delayed");
+            if (key is DConfKey)
+            {
+                if (key.planned_value == null)
+                    context.add_class ("erase");
+                else
+                    context.remove_class ("erase");
+            }
+        }
+        else
+            row.get_style_context ().remove_class ("delayed");
+    }
+
+    private bool on_button_pressed (Widget widget, Gdk.EventButton event)
+    {
+        ListBoxRow list_box_row = (ListBoxRow) widget.get_parent ();
+        key_list_box.select_row (list_box_row);
+        list_box_row.grab_focus ();
+
+        if (event.button == Gdk.BUTTON_SECONDARY)
+        {
+            ClickableListBoxRow row = (ClickableListBoxRow) widget;
+
+            int event_x = (int) event.x;
+            if (event.window != widget.get_window ())   // boolean value switch
+            {
+                int widget_x, unused;
+                event.window.get_position (out widget_x, out unused);
+                event_x += widget_x;
+            }
+
+            row.show_right_click_popover (get_current_delay_mode (), event_x);
+            rows_possibly_with_popover.append (row);
+        }
+
+        return false;
+    }
+
+    [GtkCallback]
+    private void row_activated_cb (ListBoxRow list_box_row)
+    {
+        ((ClickableListBoxRow) list_box_row.get_child ()).on_row_clicked ();
+    }
+
+    public void invalidate_popovers ()
+    {
+        uint position = 0;
+        ClickableListBoxRow? row = (ClickableListBoxRow?) rows_possibly_with_popover.get_item (0);
+        while (row != null)
+        {
+            ((!) row).destroy_popover ();
+            position++;
+            row = (ClickableListBoxRow?) rows_possibly_with_popover.get_item (position);
+        }
+        rows_possibly_with_popover.remove_all ();
+    }
+
+    /*public string? get_selected_row_name ()
+    {
+        ListBoxRow? selected_row = key_list_box.get_selected_row ();
+        if (selected_row != null)
+        {
+            int position = ((!) selected_row).get_index ();
+            return ((SettingObject) ((!) key_model).get_object (position)).full_name;
+        }
+        else
+            return null;
+    }*/
+
+    /*\
+    * * Revealer stuff
+    \*/
+
+    public bool get_current_delay_mode ()
+    {
+        return browser_view.get_current_delay_mode ();
+    }
+
+    private void set_key_value (Key key, Variant? new_value)
+    {
+        if (get_current_delay_mode ())
+            revealer.add_delayed_setting (key, new_value);
+        else if (new_value != null)
+            key.value = (!) new_value;
+        else if (key is GSettingsKey)
+            ((GSettingsKey) key).set_to_default ();
+        else if (behaviour != Behaviour.UNSAFE)
+        {
+            browser_view.enter_delay_mode ();
+            revealer.add_delayed_setting (key, null);
+        }
+        else
+            ((DConfKey) key).erase ();
+    }
+
+    /*\
+    * * Keyboard calls
+    \*/
+
+    public bool show_row_popover ()
+    {
+        ListBoxRow? selected_row = (ListBoxRow?) key_list_box.get_selected_row ();
+        if (selected_row == null)
+            return false;
+
+        ClickableListBoxRow row = (ClickableListBoxRow) ((!) selected_row).get_child ();
+        row.show_right_click_popover (get_current_delay_mode ());
+        rows_possibly_with_popover.append (row);
+        return true;
+    }
+
+    public string? get_copy_text ()
+    {
+        ListBoxRow? selected_row = key_list_box.get_selected_row ();
+        if (selected_row == null)
+            return null;
+        else
+            return ((ClickableListBoxRow) ((!) selected_row).get_child ()).get_text ();
+    }
+
+    public void toggle_boolean_key ()
+    {
+        ListBoxRow? selected_row = (ListBoxRow?) key_list_box.get_selected_row ();
+        if (selected_row == null)
+            return;
+
+        if (!(((!) selected_row).get_child () is KeyListBoxRow))
+            return;
+
+        ((KeyListBoxRow) ((!) selected_row).get_child ()).toggle_boolean_key ();
+    }
+
+    public void set_to_default ()
+    {
+        ListBoxRow? selected_row = (ListBoxRow?) key_list_box.get_selected_row ();
+        if (selected_row == null)
+            return;
+
+        ((ClickableListBoxRow) ((!) selected_row).get_child ()).on_delete_call ();
+    }
+
+    public void discard_row_popover ()
+    {
+        ListBoxRow? selected_row = (ListBoxRow?) key_list_box.get_selected_row ();
+        if (selected_row == null)
+            return;
+
+        ((ClickableListBoxRow) ((!) selected_row).get_child ()).hide_right_click_popover ();
+    }
+
+    /*\
+    * * Search
+    \*/
+
+    private string? old_term;
+    // indices for the start of each section. used to know where to insert search hits and to update the 
headers
+    // must be updated before changing the list model, so that the header function works correctly
+    private int post_local;
+    private int post_bookmarks;
+    private int post_folders;
+    private uint? search_source = null;
+    private GLib.Queue<Directory> search_nodes = new GLib.Queue<Directory> ();
+
+    public void stop_search ()
+    {
+        key_list_box.bind_model (null, null);
+        stop_global_search ();
+        search_results_model.remove_all ();
+        post_local = -1;
+        post_bookmarks = -1;
+        post_folders = -1;
+        old_term = null;
+    }
+
+    public void start_search (string term)
+    {
+        if (old_term == null || term != (!) old_term)
+        {
+            SettingsModel model = window.model;
+            string current_path = window.current_path;
+            if (old_term != null && term.has_prefix((!) old_term))
+            {
+                pause_global_search ();
+                refine_local_results (term);
+                refine_bookmarks_results (term);
+                if ((!) old_term == "")
+                    start_global_search (model, current_path, term);
+                else
+                {
+                    refine_global_results (term);
+                    resume_global_search (current_path, term); // update search term
+                }
+            }
+            else
+            {
+                stop_global_search ();
+                search_results_model.remove_all ();
+                post_local = -1;
+                post_folders = -1;
+
+                local_search (model, current_path, term);
+                bookmark_search (model, current_path, term);
+                key_list_box.bind_model (search_results_model, new_list_box_row);
+                if (term != "")
+                    start_global_search (model, current_path, term);
+            }
+            old_term = term;
+        }
+    }
+
+    private void refine_local_results (string term)
+    {
+        for (int i = post_local - 1; i >= 0; i--)
+        {
+            SettingObject item = (SettingObject) search_results_model.get_item (i);
+            if (!(term in item.name))
+            {
+                post_local--;
+                post_bookmarks--;
+                post_folders--;
+                search_results_model.remove (i);
+            }
+        }
+    }
+
+    private void refine_bookmarks_results (string term)
+    {
+        for (int i = post_bookmarks - 1; i >= post_local; i--)
+        {
+            SettingObject item = (SettingObject) search_results_model.get_item (i);
+            if (!(term in item.name))
+            {
+                post_bookmarks--;
+                post_folders--;
+                search_results_model.remove (i);
+            }
+        }
+    }
+
+    private void refine_global_results (string term)
+    {
+        for (int i = (int) search_results_model.get_n_items () - 1; i >= post_folders; i--)
+        {
+            SettingObject item = (SettingObject) search_results_model.get_item (i);
+            if (!(term in item.name))
+                search_results_model.remove (i);
+        }
+        for (int i = post_folders - 1; i >= post_local; i--)
+        {
+            SettingObject item = (SettingObject) search_results_model.get_item (i);
+            if (!(term in item.name))
+            {
+                post_folders--;
+                search_results_model.remove (i);
+            }
+        }
+    }
+
+    private bool local_search (SettingsModel model, string current_path, string term)
+    {
+        SettingComparator comparator = browser_view.sorting_options.get_comparator ();
+        GLib.CompareDataFunc compare = (a, b) => comparator.compare((SettingObject) a, (SettingObject) b);
+
+        if (current_path.has_suffix ("/"))
+        {
+            Directory? local = model.get_directory (current_path);
+            if (local != null)
+            {
+                GLib.ListStore key_model = ((!) local).key_model;
+                for (int i = 0; i < key_model.get_n_items (); i++)
+                {
+                    SettingObject item = (SettingObject) key_model.get_item (i);
+                    if (term in item.name)
+                        search_results_model.insert_sorted (item, compare);
+                }
+            }
+        }
+        post_local = (int) search_results_model.get_n_items ();
+        post_bookmarks = post_local;
+        post_folders = post_local;
+
+        if (term == "")
+            return false;
+        return true;
+    }
+
+    private bool bookmark_search (SettingsModel model, string current_path, string term)
+    {
+        foreach (string bookmark in window.get_bookmarks ())
+        {
+            bool local_again = model.get_parent_path (bookmark) == current_path;
+            if (local_again)
+                continue;
+            SettingObject? setting_object = model.get_object (bookmark);
+            if (setting_object == null)
+                continue;
+            if (term in ((!) setting_object).name)
+            {
+                post_bookmarks++;
+                post_folders++;
+                search_results_model.insert (post_bookmarks - 1, (!) setting_object);
+            }
+        }
+
+        return true;
+    }
+
+    private void stop_global_search ()
+    {
+        pause_global_search ();
+        search_nodes.clear ();
+    }
+
+    private void start_global_search (SettingsModel model, string current_path, string term)
+    {
+        search_nodes.push_head ((!) model.get_directory ("/"));
+        resume_global_search (current_path, term);
+    }
+
+    private void pause_global_search ()
+    {
+        if (search_source != null)
+        {
+            Source.remove ((!) search_source);
+            search_source = null;
+        }
+    }
+
+    private void resume_global_search (string current_path, string term)
+    {
+        search_source = Idle.add (() => {
+                if (global_search_step (current_path, term))
+                    return true;
+                search_source = null;
+                return false;
+            });
+    }
+
+    private bool global_search_step (string current_path, string term)
+    {
+        if (!search_nodes.is_empty ())
+        {
+            Directory next = (!) search_nodes.pop_head ();
+            bool local_again = next.full_name == current_path;
+
+            GLib.ListStore next_key_model = next.key_model;
+            for (int i = 0; i < next_key_model.get_n_items (); i++)
+            {
+                SettingObject item = (SettingObject) next_key_model.get_item (i);
+                if (item is Directory)
+                {
+                    if (!local_again && term in item.name)
+                        search_results_model.insert (post_folders++, item);
+                    search_nodes.push_tail ((Directory) item); // we still search local children
+                }
+                else
+                {
+                    if (!local_again && term in item.name)
+                        search_results_model.append (item);
+                }
+            }
+            return true;
+        }
+
+        return false;
+    }
+
+    private void update_search_results_header (ListBoxRow row, ListBoxRow? before)
+    {
+        ListBoxRowHeader header = new ListBoxRowHeader ();
+        header.visible = true;
+        header.orientation = Orientation.VERTICAL;
+        header.halign = Align.CENTER;
+
+        string? label_text = null;
+        if (before == null && post_local > 0)
+            label_text = _("Current folder");
+        else if (row.get_index () == post_local && post_local != post_bookmarks)
+            label_text = _("Bookmarks");
+        else if (row.get_index () == post_bookmarks && post_bookmarks != post_folders)
+            label_text = _("Folders");
+        else if (row.get_index () == post_folders)
+            label_text = _("Keys");
+
+        if (label_text != null)
+        {
+            Label label = new Label ((!) label_text);
+            label.visible = true;
+            label.halign = Align.START;
+            label.margin_top = 10; // TODO CSS
+            label.margin_left = 10; // TODO CSS
+            label.get_style_context ().add_class ("dim-label");
+            label.get_style_context ().add_class ("bold-label");
+            header.add (label);
+        }
+
+        if (before != null || label_text != null)
+        {
+            Separator separator = new Separator (Orientation.HORIZONTAL);
+            separator.visible = true;
+            separator.hexpand = true;
+            header.add (separator);
+        }
+
+        row.set_header (header);
+    }
+}
diff --git a/editor/registry-view.vala b/editor/registry-view.vala
index 640cdd7..2fdca7c 100644
--- a/editor/registry-view.vala
+++ b/editor/registry-view.vala
@@ -126,6 +126,11 @@ class RegistryView : Grid, PathElement, BrowsableView
             ListBoxRowHeader header = new ListBoxRowHeader ();
             header.set_halign (Align.CENTER);
             header.show ();
+
+            Separator separator = new Separator (Orientation.HORIZONTAL);
+            separator.show ();
+            separator.set_hexpand (true);
+            header.add (separator);
             row.set_header (header);
         }
     }


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]