[gnome-maps/wip/completion: 2/2] Replace GtkEntryCompletion with SearchPopup



commit b34c3199f12b1a996de5f2fba3591af8f06b3492
Author: Jonas Danielsson <jonas threetimestwo org>
Date:   Fri Nov 21 22:19:53 2014 +0100

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

 src/placeEntry.js   |   59 +++++++++++++-------
 src/placeStore.js   |   22 +-------
 src/search-popup.ui |   60 +++++++++++++++------
 src/searchPopup.js  |  151 ++++++++++++++++++++++++++++++++++++++++++++++++---
 4 files changed, 228 insertions(+), 64 deletions(-)
---
diff --git a/src/placeEntry.js b/src/placeEntry.js
index 409bc17..806188d 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._popover = this._createPopover(numVisible, maxChars);
+        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.connect('activate', this._onActivate.bind(this));
-        this.connect('search-changed', (function() {
-            this.popover.hide();
-
-            if (this.text.length === 0)
+        this.connect('changed', (function() {
+            if (this.text.length === 0) {
+                this._popover.hide();
                 this.place = null;
+                return;
+            }
+
+            /* Filter model based on input text */
+            this._filter.refilter();
+
+            if (this._filter.iter_n_children(null) > 0)
+                this._popover.showCompletion(this._filter, this.text);
+            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,23 @@ const PlaceEntry = new Lang.Class({
         return popover;
     },
 
+    _completionVisibleFunc: function(model, iter) {
+        let name = model.get_value(iter, PlaceStore.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, this.text, this.text.length))
+            return true;
+        else
+            return false;
+
+    },
+
     _validateCoordinates: function(lat, lon) {
         return lat <= 90 && lat >= -90 && lon <= 180 && lon >= -180;
     },
diff --git a/src/placeStore.js b/src/placeStore.js
index 11cefe3..810303b 100644
--- a/src/placeStore.js
+++ b/src/placeStore.js
@@ -44,27 +44,11 @@ const Columns = {
     PLACE_ICON: 0,
     PLACE: 1,
     NAME: 2,
-    TYPE: 3,
-    ADDED: 4
+    TYPE_ICON: 3,
+    TYPE: 4,
+    ADDED: 5
 };
 
-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..aa923ad 100644
--- a/src/search-popup.ui
+++ b/src/search-popup.ui
@@ -5,41 +5,67 @@
     <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 baac607..ce12389 100644
--- a/src/searchPopup.js
+++ b/src/searchPopup.js
@@ -18,6 +18,7 @@
  * Author: Jonas Danielsson <jonas threetimestwo org>
  */
 
+const Gdk = imports.gi.Gdk;
 const GLib = imports.gi.GLib;
 const GObject = imports.gi.GObject;
 const GdkPixbuf = imports.gi.GdkPixbuf;
@@ -25,17 +26,26 @@ const Gtk = imports.gi.Gtk;
 const Lang = imports.lang;
 
 const PlaceFormatter = imports.placeFormatter;
+const PlaceStore = imports.placeStore;
 const Utils = imports.utils;
 
 const _PLACE_ICON_SIZE = 20;
 const _ROW_HEIGHT = 50;
 
+const Mode = {
+    IDLE: 0,
+    ACTIVATED: 1,
+    COMPLETION: 2,
+    RESULT: 3
+};
+
 const SearchPopupRow = new Lang.Class({
     Name: 'SearchPopupRow',
     Extends: Gtk.ListBoxRow,
     Template: 'resource:///org/gnome/maps/search-popup-row.ui',
     InternalChildren: [ 'icon',
                         'name',
+                        'typeIcon',
                         'details' ],
 
     _init: function(params) {
@@ -48,16 +58,22 @@ const SearchPopupRow = new Lang.Class({
         let maxChars = params.maxChars || 40;
         delete params.maxChars;
 
+        let typeIconPixbuf = params.typeIconPixbuf || null;
+        delete params.typeIconPixbuf;
+
         params.height_request = _ROW_HEIGHT;
         this.parent(params);
 
         let formatter = new PlaceFormatter.PlaceFormatter(this.place);
-        let title = GLib.markup_escape_text(formatter.title, -1);
+        this.title = GLib.markup_escape_text(formatter.title, -1);
 
-        this._name.label = this._boldMatch(title, searchString);
+        this._name.label = this._boldMatch(this.title, searchString);
         this._details.max_width_chars = maxChars;
         this._details.label = formatter.getDetailsString();
         this._icon.gicon = this.place.icon;
+        this._typeIcon.pixbuf = typeIconPixbuf;
+
+        this._mode = Mode.IDLE;
     },
 
     _boldMatch: function(title, string) {
@@ -80,7 +96,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' ],
@@ -94,11 +111,38 @@ const SearchPopup = new Lang.Class({
 
         this.parent(props);
 
-        this._list.connect('row-activated', (function(list, row) {
-            if (row)
-                this.emit('selected', row.place);
+        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', (function() {
+            this._list.unselect_all();
         }).bind(this));
 
+        // Do not show 'press enter to search' when we have
+        // selected rows in completion mode.
+        this._list.connect('selected-rows-changed', (function(list, row) {
+            if (this._mode !== Mode.COMPLETION)
+                return;
+
+            if (this._list.get_selected_rows().length > 0)
+                this._hintRevealer.reveal_child = false;
+            else
+                this._hintRevealer.reveal_child = true;
+         }).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();
             if (before)
@@ -113,6 +157,8 @@ const SearchPopup = new Lang.Class({
     },
 
     showSpinner: function() {
+        this._hintRevealer.reveal_child = false;
+
         this._spinner.start();
         this._stack.set_visible_child(this._spinner);
 
@@ -121,6 +167,12 @@ const SearchPopup = new Lang.Class({
     },
 
     showResult: function() {
+        if (this._mode !== Mode.RESULT) {
+            this._list.set_filter_func(null, null, null);
+            this._mode = Mode.RESULT;
+        }
+        this._hintRevealer.reveal_child = false;
+
         if (this._spinner.active)
             this._spinner.stop();
 
@@ -128,8 +180,21 @@ const SearchPopup = new Lang.Class({
 
         if (!this.get_visible())
             this.show();
+    },
+
+    showCompletion: function(filter, searchString) {
+        if (this._mode === Mode.ACTIVATED) {
+            this._mode = Mode.IDLE;
+            return;
+        }
+        this._populateFromFilter(filter, searchString);
+        this._mode = Mode.COMPLETION;
+        this._hintRevealer.reveal_child = true;
+
+        this._stack.set_visible_child(this._scrolledWindow);
 
-        this.grab_focus();
+        if (!this.get_visible())
+            this.show();
     },
 
     vfunc_hide: function() {
@@ -149,6 +214,78 @@ const SearchPopup = new Lang.Class({
                 return;
             let row = new SearchPopupRow({ place: place,
                                            searchString: searchString,
+                                           maxChars: this._maxChars });
+            this._list.add(row);
+        }).bind(this));
+    },
+
+    _propagateKeys: function(entry, event) {
+        let row = this._list.get_selected_row();
+        if (!row)
+            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.hide();
+        }
+
+        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;
+    },
+
+    _populateFromFilter: function(filter, searchString) {
+        this._list.forall(function(row) {
+            row.destroy();
+        });
+
+        let placeColumn = PlaceStore.Columns.PLACE;
+        let typeIconColumn = PlaceStore.Columns.TYPE_ICON;
+
+        filter.foreach((function(model, path, iter) {
+            let place = model.get_value(iter, placeColumn);
+            let typeIcon = model.get_value(iter, typeIconColumn);
+            let row = new SearchPopupRow({ place: place,
+                                           searchString: searchString,
+                                           typeIconPixbuf: typeIcon,
                                            maxChars: this._maxChars,
                                            can_focus: true });
             this._list.add(row);


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