[gnome-maps/wip/mlundblad/search-as-you-type: 1/2] placeEntry, placePopover: Auto-complete searches



commit 94b334cb18595e53270b2282b7adfbe593709419
Author: Marcus Lundblad <ml update uu se>
Date:   Sun May 5 21:38:14 2019 +0200

    placeEntry, placePopover: Auto-complete searches
    
    Implement auto-complete search-as-you-type.

 src/placeEntry.js   | 151 ++++++++++++++++++++++++++++++++++++----------------
 src/placePopover.js |  61 ++++++---------------
 2 files changed, 121 insertions(+), 91 deletions(-)
---
diff --git a/src/placeEntry.js b/src/placeEntry.js
index 4872f46..c9950e3 100644
--- a/src/placeEntry.js
+++ b/src/placeEntry.js
@@ -34,6 +34,12 @@ const PlaceStore = imports.placeStore;
 const PlacePopover = imports.placePopover;
 const Utils = imports.utils;
 
+// minimum number of characters to start completion
+const MIN_CHARS_COMPLETION = 3;
+
+// pattern matching CJK ideographic characters
+const IDEOGRAPH_PATTERN = /[\u3300-\u9fff]/
+
 var PlaceEntry = GObject.registerClass({
     Properties: {
         'place': GObject.ParamSpec.object('place',
@@ -54,11 +60,13 @@ var PlaceEntry = GObject.registerClass({
 
         if (p) {
             if (p.name) {
-                this.text = p.name;
+                this._placeText = p.name;
             } else
-                this.text = p.location.latitude + ', ' + p.location.longitude;
+                this._placeText = p.location.latitude + ', ' + p.location.longitude;
         } else
-            this.text = '';
+            this._placeText = '';
+
+        this.text = this._placeText;
 
         this._place = p;
         this.notify('place');
@@ -85,9 +93,6 @@ var PlaceEntry = GObject.registerClass({
         let maxChars = props.maxChars;
         delete props.maxChars;
 
-        let parseOnFocusOut = props.parseOnFocusOut;
-        delete props.parseOnFocusOut;
-
         this._matchRoute = props.matchRoute || false;
         delete props.matchRoute;
 
@@ -98,30 +103,48 @@ var PlaceEntry = GObject.registerClass({
 
         this._popover = this._createPopover(numVisible, maxChars);
 
-        this.connect('activate', this._onActivate.bind(this));
-        this.connect('search-changed', () => {
-            if (this._cancellable)
-                this._cancellable.cancel();
+        this.connect('search-changed', this._onSearchChanged.bind(this));
 
-            this._refreshFilter();
+        this._cache = {};
 
-            if (this.text.length === 0) {
-                this._popover.hide();
-                this.place = null;
-                return;
-            }
+        // clear cache when view moves, as result are location-dependent
+        this._mapView.view.connect('notify::latitude', () => this._cache = {});
+    }
 
-            if (this._filter.iter_n_children(null) > 0)
-                this._popover.showCompletion();
-            else
-                this._popover.hide();
-        });
+    _onSearchChanged() {
+        if (this._parse())
+            return;
+
+        // wait for an ongoing search
+        if (this._cancellable)
+            return;
 
-        if (parseOnFocusOut) {
-            this.connect('focus-out-event', () => {
-                this._parse();
-                return false;
-            });
+        /* start search if more than the threashold number of characters have
+         * been entered, or if the first character is in the ideographic CJK
+         * block, as for these, shorter strings could be meaningful
+         */
+        if ((this.text.length >= MIN_CHARS_COMPLETION ||
+             (this.text.length > 0 && this.text[0].match(IDEOGRAPH_PATTERN))) &&
+            this.text !== this._placeText) {
+            let cachedResults = this._cache[this.text];
+
+            if (cachedResults) {
+                this._updateResults(cachedResults);
+            } else {
+                // if no previous search has been performed, show spinner
+                if (!this._previousSearch ||
+                    this._previousSearch.length < MIN_CHARS_COMPLETION ||
+                    this._placeText) {
+                    this._popover.showSpinner();
+                }
+                this._placeText = '';
+                this._doSearch();
+            }
+        } else {
+            this._popover.hide();
+            if (this.text.length === 0)
+                this.place = null;
+            this._previousSearch = null;
         }
     }
 
@@ -162,7 +185,9 @@ var PlaceEntry = GObject.registerClass({
         let place = model.get_value(iter, PlaceStore.Columns.PLACE);
         let type = model.get_value(iter, PlaceStore.Columns.TYPE);
 
-        if (!this._matchRoute && type === PlaceStore.PlaceType.RECENT_ROUTE)
+        if (type !== PlaceStore.PlaceType.CONTACT &&
+            type !== PlaceStore.PlaceType.RECENT_ROUTE ||
+            (!this._matchRoute && type === PlaceStore.PlaceType.RECENT_ROUTE))
             return false;
 
         if (place !== null)
@@ -172,10 +197,7 @@ var PlaceEntry = GObject.registerClass({
     }
 
     _parse() {
-        if (this.text.length === 0) {
-            this.place = null;
-            return true;
-        }
+        let parsed = false;
 
         if (this.text.startsWith('geo:')) {
             let location = new Geocode.Location();
@@ -188,40 +210,77 @@ var PlaceEntry = GObject.registerClass({
                 Utils.showDialog(msg, Gtk.MessageType.ERROR, this.get_toplevel());
             }
 
-            return true;
+            parsed = true;
         }
 
         let parsedLocation = Place.Place.parseCoordinates(this.text);
         if (parsedLocation) {
             this.place = new Place.Place({ location: parsedLocation });
-            return true;
+            parsed = true;
         }
 
-        return false;
-    }
-
-    _onActivate() {
-        if (this._parse())
-            return;
+        if (parsed && this._cancellable)
+            this._cancellable.cancel();
 
-        let bbox = this._mapView.view.get_bounding_box();
-
-        this._popover.showSpinner();
+        return parsed;
+    }
 
+    _doSearch() {
+        if (this._cancellable)
+            this._cancellable.cancel();
         this._cancellable = new Gio.Cancellable();
+        this._previousSearch = this.text;
         GeocodeFactory.getGeocoder().search(this.text,
                                             this._mapView.view.latitude,
                                             this._mapView.view.longitude,
                                             this._cancellable,
                                             (places, error) => {
+            this._cancellable = null;
+            this._updateResults(places);
+
+            // cache results for later
+            this._cache[this.text] = places;
+
+            // if search input has been updated, trigger a refresh
+            if (this.text !== this._previousSearch)
+                this._onSearchChanged();
+        });
+    }
 
-            if (!places) {
+    _updateResults(places) {
+        if (!places) {
                 this.place = null;
                 this._popover.showNoResult();
                 return;
-            }
-            this._popover.updateResult(places, this.text);
-            this._popover.showResult();
+        }
+
+        let completedPlaces = [];
+
+
+        this._filter.refilter();
+        this._filter.foreach((model, path, iter) => {
+            let place = model.get_value(iter, PlaceStore.Columns.PLACE);
+            let type = model.get_value(iter, PlaceStore.Columns.TYPE);
+
+            completedPlaces.push({ place: place, type: type });
+        });
+
+        let placeStore = Application.placeStore;
+
+        places.forEach((place) => {
+            let type;
+
+            if (placeStore.exists(place, PlaceStore.PlaceType.RECENT))
+                type = PlaceStore.PlaceType.RECENT;
+            else if (placeStore.exists(place, PlaceStore.PlaceType.FAVORITE))
+                type = PlaceStore.PlaceType.FAVORITE;
+            else
+                type = PlaceStore.PlaceType.ANY;
+
+            completedPlaces.push({ place: place, type: type });
         });
+
+        this._popover.updateResult(completedPlaces, this.text);
+        this._popover.showResult();
     }
 });
diff --git a/src/placePopover.js b/src/placePopover.js
index 108137a..75d1382 100644
--- a/src/placePopover.js
+++ b/src/placePopover.js
@@ -27,13 +27,6 @@ const SearchPopover = imports.searchPopover;
 
 const _PLACE_ICON_SIZE = 20;
 
-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
-};
-
 var PlacePopover = GObject.registerClass({
     Signals : {
         'selected' : { param_types: [ GObject.TYPE_OBJECT ] }
@@ -57,20 +50,12 @@ var PlacePopover = GObject.registerClass({
         super._init(props);
 
         this._entry = this.relative_to;
-        this._entry.connect('notify::place', () => this._mode = Mode.ACTIVATED);
-
-        Application.routingDelegator.graphHopper.route.connect('update', () => {
-            this._mode = Mode.ACTIVATED;
-        });
 
         this._list.connect('row-activated', (list, row) => {
             if (row)
                 this.emit('selected', row.place);
         });
 
-        // Make sure we clear all selected rows when the search string change
-        this._entry.connect('changed', () => this._list.unselect_all());
-
         this._list.set_header_func((row, before) => {
             let header = new Gtk.Separator();
             if (before)
@@ -96,8 +81,6 @@ var PlacePopover = GObject.registerClass({
     }
 
     showResult() {
-        this._mode = Mode.RESULT;
-
         if (this._spinner.active)
             this._spinner.stop();
 
@@ -112,46 +95,34 @@ var PlacePopover = GObject.registerClass({
     }
 
     showNoResult() {
-        this._mode = Mode.IDLE;
-
         if (this._spinner.active)
             this._spinner.stop();
 
         this._stack.visible_child = this._noResultsLabel;
     }
 
-    showCompletion() {
-        if (this._mode === undefined || this._mode === Mode.ACTIVATED) {
-            this._mode = Mode.IDLE;
-            return;
-        }
-
-        this._mode = Mode.COMPLETION;
-        this._stack.visible_child = this._scrolledWindow;
-
-        if (!this.visible)
-            this.show();
-    }
-
     updateResult(places, searchString) {
-        this._list.forall((row) => row.destroy());
+        let i = 0;
+
+        places.forEach((p) => {
+            let row = this._list.get_row_at_index(i);
 
-        places.forEach((place) => {
-            if (!place.location)
-                return;
+            // update existing row, if there is one, otherwise create new
+            if (row)
+                row.update(p.place, p.type, searchString);
+            else
+                this._addRow(p.place, p.type, searchString);
 
-            this._addRow(place, null, searchString);
+            i++;
         });
-    }
 
-    updateCompletion(filter, searchString) {
-        this._list.forall((row) => row.destroy());
+        // remove remaining rows
+        let row = this._list.get_row_at_index(i);
 
-        filter.foreach((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);
-        });
+        while (row) {
+            row.destroy();
+            row = this._list.get_row_at_index(i);
+        }
     }
 
     _addRow(place, type, searchString) {


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