[gnome-maps] Replace GtkEntryCompletion with SearchPopup



commit 267b1d5ea295d6d6044a5ad001262bc96a5699e0
Author: Jonas Danielsson <jonas threetimestwo org>
Date:   Mon Nov 24 03:49:21 2014 -0500

    Replace GtkEntryCompletion with SearchPopup
    
    https://bugzilla.gnome.org/show_bug.cgi?id=739036

 src/placeEntry.js   |   65 +++++++++++++------
 src/placeStore.js   |   17 -----
 src/search-popup.ui |   61 ++++++++++++-----
 src/searchPopup.js  |  179 +++++++++++++++++++++++++++++++++++++++++++++-----
 4 files changed, 250 insertions(+), 72 deletions(-)
---
diff --git a/src/placeEntry.js b/src/placeEntry.js
index 409bc17..9855c2a 100644
--- a/src/placeEntry.js
+++ b/src/placeEntry.js
@@ -21,6 +21,7 @@
  *         Mattias Bengtsson <mattias jc bengtsson gmail com>
  */
 
+const GLib = imports.gi.GLib;
 const GObject = imports.gi.GObject;
 const Geocode = imports.gi.GeocodeGlib;
 const Gtk = imports.gi.Gtk;
@@ -94,17 +95,31 @@ const PlaceEntry = new Lang.Class({
         let parseOnFocusOut = props.parseOnFocusOut;
         delete props.parseOnFocusOut;
 
-        props.completion = this._createCompletion();
         this.parent(props);
 
+        this._filter = new Gtk.TreeModelFilter({ child_model: Application.placeStore });
+        this._filter.set_visible_func(this._completionVisibleFunc.bind(this),
+                                      null,
+                                      null);
+
         this._popover = this._createPopover(numVisible, maxChars);
 
+        this._refreshFilter();
+
         this.connect('activate', this._onActivate.bind(this));
-        this.connect('search-changed', (function() {
-            this.popover.hide();
+        this.connect('changed', (function() {
+            this._refreshFilter();
 
-            if (this.text.length === 0)
+            if (this.text.length === 0) {
+                this._popover.hide();
                 this.place = null;
+                return;
+            }
+
+            if (this._filter.iter_n_children(null) > 0)
+                this._popover.showCompletion();
+            else
+                this._popover.hide();
         }).bind(this));
 
         if (parseOnFocusOut) {
@@ -115,21 +130,6 @@ const PlaceEntry = new Lang.Class({
         }
     },
 
-    _createCompletion: function() {
-        let { completion } = Utils.getUIObject('place-entry',
-                                               ['completion']);
-
-        completion.set_model(Application.placeStore);
-        completion.set_match_func(PlaceStore.completionMatchFunc);
-
-        completion.connect('match-selected', (function(c, model, iter) {
-            this.place = model.get_value(iter, PlaceStore.Columns.PLACE);
-            return true;
-        }).bind(this));
-
-        return completion;
-    },
-
     _createPopover: function(numVisible, maxChars) {
         let popover = new SearchPopup.SearchPopup({ num_visible:   numVisible,
                                                     relative_to:   this,
@@ -149,6 +149,33 @@ const PlaceEntry = new Lang.Class({
         return popover;
     },
 
+    _refreshFilter: function() {
+        /* Filter model based on input text */
+        this._filter.refilter();
+        this._popover.updateCompletion(this._filter, this.text);
+    },
+
+    _completionVisibleFunc: function(model, iter) {
+        let name = model.get_value(iter, PlaceStore.Columns.NAME);
+        let key = this.text;
+
+        if (key.length === 0)
+            return true;
+
+        if (name === null)
+            return false;
+
+        key = GLib.utf8_normalize(key, -1, GLib.NormalizeMode.ALL);
+        if (key === null)
+            return false;
+
+        name = GLib.utf8_normalize(name, -1, GLib.NormalizeMode.ALL);
+        if (name === null)
+            return false;
+
+        return name.toLowerCase().search(key.toLowerCase()) !== -1;
+    },
+
     _validateCoordinates: function(lat, lon) {
         return lat <= 90 && lat >= -90 && lon <= 180 && lon >= -180;
     },
diff --git a/src/placeStore.js b/src/placeStore.js
index a117476..b259237 100644
--- a/src/placeStore.js
+++ b/src/placeStore.js
@@ -46,23 +46,6 @@ const Columns = {
     ADDED: 4
 };
 
-function completionMatchFunc(completion, key, iter) {
-    let model = completion.get_model();
-    let name = model.get_value(iter, Columns.NAME);
-
-    if (name === null)
-        return false;
-
-    name = GLib.utf8_normalize (name, -1, GLib.NormalizeMode.ALL);
-    if (name === null)
-        return false;
-
-    if (!GLib.ascii_strncasecmp(name, key, key.length))
-        return true;
-    else
-        return false;
-}
-
 const PlaceStore = new Lang.Class({
     Name: 'PlaceStore',
     Extends: Gtk.ListStore,
diff --git a/src/search-popup.ui b/src/search-popup.ui
index a768c89..ae7d839 100644
--- a/src/search-popup.ui
+++ b/src/search-popup.ui
@@ -3,43 +3,68 @@
   <!-- interface-requires gtk+ 3.10 -->
   <template class="Gjs_SearchPopup" parent="GtkPopover">
     <property name="visible">False</property>
-    <property name="no_show_all">True</property>
     <property name="hexpand">False</property>
+    <property name="modal">False</property>
     <style>
       <class name="maps-popover"/>
     </style>
     <child>
-      <object class="GtkStack" id="stack">
+      <object class="GtkGrid" id="mainGrid">
         <property name="visible">True</property>
-        <property name="can_focus">False</property>
-        <property name="transition-type">crossfade</property>
-        <style>
-          <class name="maps-stack"/>
-        </style>
+        <property name="can_focus">True</property>
+        <property name="orientation">vertical</property>
         <child>
-          <object class="GtkScrolledWindow" id="scrolledWindow">
+          <object class="GtkRevealer" id="hintRevealer">
             <property name="visible">True</property>
             <property name="can_focus">False</property>
-            <property name="hscrollbar_policy">never</property>
-            <property name="shadow_type">in</property>
             <child>
-              <object class="GtkListBox" id="list">
+              <object class="GtkLabel" id="hintLabel">
                 <property name="visible">True</property>
                 <property name="can_focus">False</property>
-                <property name="expand">True</property>
-                <property name="activate_on_single_click">True</property>
+                <property name="label" translatable="yes">Press enter to search</property>
+                <property name="margin_bottom">10</property>
+                <property name="margin_top">5</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
               </object>
             </child>
           </object>
         </child>
         <child>
-          <object class="GtkSpinner" id="spinner">
+          <object class="GtkStack" id="stack">
             <property name="visible">True</property>
             <property name="can_focus">False</property>
-            <property name="halign">center</property>
-            <property name="valign">center</property>
-            <property name="width_request">16</property>
-            <property name="height_request">16</property>
+            <property name="transition-type">crossfade</property>
+            <style>
+              <class name="maps-stack"/>
+            </style>
+            <child>
+              <object class="GtkScrolledWindow" id="scrolledWindow">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="hscrollbar_policy">never</property>
+                <property name="shadow_type">in</property>
+                <child>
+                  <object class="GtkListBox" id="list">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="expand">True</property>
+                    <property name="activate_on_single_click">True</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkSpinner" id="spinner">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="halign">center</property>
+                <property name="valign">center</property>
+                <property name="width_request">16</property>
+                <property name="height_request">16</property>
+              </object>
+            </child>
           </object>
         </child>
       </object>
diff --git a/src/searchPopup.js b/src/searchPopup.js
index 2de9107..4564b82 100644
--- a/src/searchPopup.js
+++ b/src/searchPopup.js
@@ -18,13 +18,23 @@
  * Author: Jonas Danielsson <jonas threetimestwo org>
  */
 
+const Gdk = imports.gi.Gdk;
 const GObject = imports.gi.GObject;
 const Gtk = imports.gi.Gtk;
 const Lang = imports.lang;
 
 const PlaceListRow = imports.placeListRow;
+const PlaceStore = imports.placeStore;
 
 const _PLACE_ICON_SIZE = 20;
+const _ROW_HEIGHT = 50;
+
+const Mode = {
+    IDLE: 0, // Nothing going on
+    ACTIVATED: 1, // Just activated, ignore changes to text
+    COMPLETION: 2, // We are doing completion against placeStore
+    RESULT: 3 // We are displaying results
+};
 
 const SearchPopup = new Lang.Class({
     Name: 'SearchPopup',
@@ -33,7 +43,8 @@ const SearchPopup = new Lang.Class({
         'selected' : { param_types: [ GObject.TYPE_OBJECT ] }
     },
     Template: 'resource:///org/gnome/maps/search-popup.ui',
-    InternalChildren: [ 'scrolledWindow',
+    InternalChildren: [ 'hintRevealer',
+                        'scrolledWindow',
                         'stack',
                         'spinner',
                         'list' ],
@@ -47,10 +58,29 @@ const SearchPopup = new Lang.Class({
 
         this.parent(props);
 
-        this._list.connect('row-activated', (function(list, row) {
-            if (row)
-                this.emit('selected', row.place);
-        }).bind(this));
+        this._entry = this.relative_to;
+
+         this._list.connect('row-activated', (function(list, row) {
+             if (row)
+                 this.emit('selected', row.place);
+         }).bind(this));
+
+        // Make sure we clear all selected rows when the search string change
+        this._entry.connect('changed',
+                            this._list.unselect_all.bind(this._list));
+
+        // Do not show 'press enter to search' when we have
+        // selected rows in completion mode.
+        this._list.connect('selected-rows-changed',
+                           this._updateHint.bind(this));
+
+        // We need to propagate events to the listbox so that we can
+        // keep typing while selecting a place. But we do not want to
+        // propagate the 'enter' key press if there is a selection.
+        this._entry.connect('key-press-event',
+                            this._propagateKeys.bind(this));
+        this._entry.connect('button-press-event',
+                            this._list.unselect_all.bind(this._list));
 
         this._list.set_header_func(function(row, before) {
             let header = new Gtk.Separator();
@@ -62,32 +92,53 @@ const SearchPopup = new Lang.Class({
 
         let rowHeight = PlaceListRow.ROW_HEIGHT + 6; // For the header
         this._scrolledWindow.min_content_height = numVisible * rowHeight;
+
+        // This silents warning at Maps exit about this widget being
+        // visible but not mapped.
+        this.connect('unmap', function(popover) { popover.hide(); });
     },
 
     showSpinner: function() {
         this._spinner.start();
-        this._stack.set_visible_child(this._spinner);
+        this._stack.visible_child = this._spinner;
+        this._updateHint();
 
-        if (!this.get_visible())
+        if (!this.visible)
             this.show();
     },
 
     showResult: function() {
+        this._mode = Mode.RESULT;
+
         if (this._spinner.active)
             this._spinner.stop();
 
-        this._stack.set_visible_child(this._scrolledWindow);
+        this._stack.visible_child = this._scrolledWindow;
+
+        let row = this._list.get_row_at_index(0);
+        if (row)
+            this._list.select_row(row);
 
-        if (!this.get_visible())
+        if (!this.visible)
             this.show();
+    },
+
+    showCompletion: function() {
+        if (this._mode === Mode.ACTIVATED) {
+            this._mode = Mode.IDLE;
+            return;
+        }
 
-        this.grab_focus();
+        this._mode = Mode.COMPLETION;
+        this._stack.visible_child = this._scrolledWindow;
+        this._updateHint();
+
+        if (!this.visible)
+            this.show();
     },
 
     vfunc_hide: function() {
-        if (this._spinner.active)
-            this._spinner.stop();
-
+        this._hintRevealer.reveal_child = false;
         this.parent();
     },
 
@@ -100,11 +151,103 @@ const SearchPopup = new Lang.Class({
             if (!place.location)
                 return;
 
-            let row = new PlaceListRow.PlaceListRow({ place: place,
-                                                      searchString: searchString,
-                                                      maxChars: this._maxChars,
-                                                      can_focus: true });
-            this._list.add(row);
+            this._addRow(place, null, searchString);
+        }).bind(this));
+    },
+
+    updateCompletion: function(filter, searchString) {
+        this._list.forall(function(row) {
+            row.destroy();
+        });
+
+        filter.foreach((function(model, path, iter) {
+            let place = model.get_value(iter, PlaceStore.Columns.PLACE);
+            let type = model.get_value(iter, PlaceStore.Columns.TYPE);
+            this._addRow(place, type, searchString);
         }).bind(this));
+    },
+
+    _addRow: function(place, type, searchString) {
+        let row = new PlaceListRow.PlaceListRow({ place: place,
+                                                  searchString: searchString,
+                                                  type: type,
+                                                  maxChars: this._maxChars,
+                                                  can_focus: true });
+        this._list.add(row);
+    },
+
+    _updateHint: function() {
+        if (this._stack.visible_child === this._spinner) {
+            this._hintRevealer.reveal_child = false;
+            return;
+        }
+
+        if (this._list.get_selected_rows().length > 0)
+            this._hintRevealer.reveal_child = false;
+        else
+            this._hintRevealer.reveal_child = true;
+    },
+
+    _propagateKeys: function(entry, event) {
+        let row;
+
+        if (this.visible) {
+            row = this._list.get_selected_row();
+            if (!row)
+                row = this._list.get_row_at_index(0);
+        } else
+            row = this._list.get_row_at_index(0);
+
+        if (!row)
+            return false;
+
+        let length = this._list.get_children().length;
+        let keyval = event.get_keyval()[1];
+
+        if (keyval === Gdk.KEY_Escape) {
+            this._list.unselect_all();
+            this.hide();
+            return false;
+        }
+
+        // If we get an 'enter' keypress and we have a selected
+        // row, we do not want to propagate the event.
+        if ((this.visible && row.is_selected()) &&
+            keyval === Gdk.KEY_Return ||
+            keyval === Gdk.KEY_KP_ENTER ||
+            keyval === Gdk.KEY_ISO_Enter) {
+            row.activate();
+            this._mode = Mode.ACTIVATED;
+
+            return true;
+        } else if (keyval === Gdk.KEY_KP_Up || keyval === Gdk.KEY_Up) {
+            this.show();
+
+            if (!row.is_selected()) {
+                let pRow = this._list.get_row_at_index(length - 1);
+                this._list.select_row(pRow);
+                return false;
+            }
+
+            if (row.get_index() > 0) {
+                let pRow = this._list.get_row_at_index(row.get_index() - 1);
+                this._list.select_row(pRow);
+            } else
+                this._list.unselect_all();
+        } else if (keyval === Gdk.KEY_KP_Down || keyval === Gdk.KEY_Down) {
+            this.show();
+
+            if (!row.is_selected()) {
+                this._list.select_row(row);
+                return false;
+            }
+
+            if (row.get_index() !== (length - 1)) {
+                let nRow = this._list.get_row_at_index(row.get_index() + 1);
+                this._list.select_row(nRow);
+            } else
+                this._list.unselect_all();
+        }
+        return false;
     }
 });


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