[gnome-maps/wip/mlundblad/osm-add-location: 5/6] osmEdit: Add support for creating new locations in the edit dialog



commit b6acf447327b182fe8156ced9606a1c6b773e788
Author: Marcus Lundblad <ml update uu se>
Date:   Wed Dec 30 00:24:00 2015 +0100

    osmEdit: Add support for creating new locations in the edit dialog
    
    Adds the ability to edit newly created locations in the dialog.
    Also adds a module to handle OSM tag key/value mapping to translated
    POI types and a list of recently used POI types.

 data/org.gnome.Maps.data.gresource.xml |    3 +
 data/ui/osm-edit-dialog.ui             |  109 +++++++++++++++++
 data/ui/osm-type-list-row.ui           |   29 +++++
 data/ui/osm-type-search-entry.ui       |   10 ++
 data/ui/osm-type-search-popover.ui     |   16 +++
 src/org.gnome.Maps.src.gresource.xml   |    4 +
 src/osmEditDialog.js                   |  203 ++++++++++++++++++++++++++++++--
 src/osmTypeListRow.js                  |   52 ++++++++
 src/osmTypeLookup.js                   |  170 ++++++++++++++++++++++++++
 src/osmTypeSearchEntry.js              |   76 ++++++++++++
 src/osmTypeSearchPopover.js            |   68 +++++++++++
 11 files changed, 731 insertions(+), 9 deletions(-)
---
diff --git a/data/org.gnome.Maps.data.gresource.xml b/data/org.gnome.Maps.data.gresource.xml
index 61c4f8b..36e71bd 100644
--- a/data/org.gnome.Maps.data.gresource.xml
+++ b/data/org.gnome.Maps.data.gresource.xml
@@ -18,6 +18,9 @@
     <file preprocess="xml-stripblanks">ui/notification.ui</file>
     <file preprocess="xml-stripblanks">ui/osm-account-dialog.ui</file>
     <file preprocess="xml-stripblanks">ui/osm-edit-dialog.ui</file>
+    <file preprocess="xml-stripblanks">ui/osm-type-list-row.ui</file>
+    <file preprocess="xml-stripblanks">ui/osm-type-search-entry.ui</file>
+    <file preprocess="xml-stripblanks">ui/osm-type-search-popover.ui</file>
     <file preprocess="xml-stripblanks">ui/place-bubble.ui</file>
     <file preprocess="xml-stripblanks">ui/place-entry.ui</file>
     <file preprocess="xml-stripblanks">ui/place-list-row.ui</file>
diff --git a/data/ui/osm-edit-dialog.ui b/data/ui/osm-edit-dialog.ui
index 3f2623d..4bc9ba5 100644
--- a/data/ui/osm-edit-dialog.ui
+++ b/data/ui/osm-edit-dialog.ui
@@ -51,6 +51,56 @@
                     <property name="row-spacing">12</property>
                     <property name="column-spacing">6</property>
                     <property name="margin-bottom">12</property>
+                    <child>
+                      <object class="GtkLabel" id="typeLabel">
+                        <property name="visible">False</property>
+                        <property name="can_focus">False</property>
+                        <property name="label" translatable="true">Type</property>
+                        <property name="halign">GTK_ALIGN_END</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="left_attach">0</property>
+                        <property name="top_attach">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkButton" id="typeButton">
+                        <property name="visible">False</property>
+                        <property name="can_focus">True</property>
+                        <property name="hexpand">True</property>
+                        <child>
+                          <object class="GtkGrid">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="row-spacing">5</property>
+                            <property name="column-spacing">5</property>
+                            <child>
+                              <object class="GtkLabel" id="typeValueLabel">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <property name="label" translatable="yes">None</property>
+                              </object>
+                            </child>
+                            <child>
+                              <object class="GtkImage">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <property name="halign">GTK_ALIGN_END</property>
+                                <property name="hexpand">True</property>
+                                <property name="icon-name">go-next-symbolic</property>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="left_attach">1</property>
+                        <property name="top_attach">0</property>
+                      </packing>
+                    </child>
                   </object>
                 </child>
                 <child>
@@ -159,6 +209,65 @@
                 <property name="name">upload</property>
               </packing>
             </child>
+            <child>
+              <object class="GtkGrid" id="typeSearchGrid">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="margin_start">60</property>
+                <property name="margin_end">60</property>
+                <property name="margin_top">15</property>
+                <property name="margin_bottom">30</property>
+                <property name="row-spacing">5</property>
+                <!--
+                <child>
+                  <object class="Gjs_OSMTypeSearchEntry" id="typeSearchEntry">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="hexpand">True</property>
+                    <property name="margin_start">10</property>
+                    <property name="margin_end">10</property>
+                    <property name="margin_bottom">10</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">0</property>
+                  </packing>
+                </child>
+                -->
+                <child>
+                  <object class="GtkLabel" id="recentTypesLabel">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="label" translatable="yes">Recently Used</property>
+                    <property name="halign">GTK_ALIGN_START</property>
+                    <style>
+                      <class name="dim-label"/>
+                    </style>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkListBox" id="recentTypesListBox">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="selection-mode">none</property>
+                    <style>
+                      <class name="frame"/>
+                    </style>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">2</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="name">select-type</property>
+              </packing>
+            </child>
           </object>
         </child>
       </object>
diff --git a/data/ui/osm-type-list-row.ui b/data/ui/osm-type-list-row.ui
new file mode 100644
index 0000000..4dda238
--- /dev/null
+++ b/data/ui/osm-type-list-row.ui
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.12"/>
+  <template class="Gjs_OSMTypeListRow" parent="GtkListBoxRow">
+    <property name="visible">True</property>
+    <child>
+      <object class="GtkGrid" id="grid">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="row-homogeneous">True</property>
+        <property name="margin">5</property>
+        <child>
+          <object class="GtkLabel" id="name">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="halign">start</property>
+            <property name="valign">end</property>
+            <property name="hexpand">True</property>
+            <property name="use_markup">True</property>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">0</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/data/ui/osm-type-search-entry.ui b/data/ui/osm-type-search-entry.ui
new file mode 100644
index 0000000..db292a7
--- /dev/null
+++ b/data/ui/osm-type-search-entry.ui
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.12"/>
+  <template class="Gjs_OSMTypeSearchEntry" parent="GtkSearchEntry">
+    <property name="hexpand">True</property>
+    <property name="margin_start">10</property>
+    <property name="margin_end">10</property>
+    <property name="margin_bottom">10</property>
+  </template>
+</interface>
diff --git a/data/ui/osm-type-search-popover.ui b/data/ui/osm-type-search-popover.ui
new file mode 100644
index 0000000..81e6c35
--- /dev/null
+++ b/data/ui/osm-type-search-popover.ui
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.12"/>
+  <template class="Gjs_OSMTypeSearchPopover" parent="Gjs_PropagatingSearchPopover">
+    <property name="position">GTK_POS_BOTTOM</property>
+    <property name="modal">False</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>
+  </template>
+</interface>
diff --git a/src/org.gnome.Maps.src.gresource.xml b/src/org.gnome.Maps.src.gresource.xml
index 916c151..ebaea93 100644
--- a/src/org.gnome.Maps.src.gresource.xml
+++ b/src/org.gnome.Maps.src.gresource.xml
@@ -37,6 +37,10 @@
     <file>osmConnection.js</file>
     <file>osmEdit.js</file>
     <file>osmEditDialog.js</file>
+    <file>osmTypeSearchEntry.js</file>
+    <file>osmTypeListRow.js</file>
+    <file>osmTypeLookup.js</file>
+    <file>osmTypeSearchPopover.js</file>
     <file>osmUtils.js</file>
     <file>overpass.js</file>
     <file>place.js</file>
diff --git a/src/osmEditDialog.js b/src/osmEditDialog.js
index 599e98f..ba6f121 100644
--- a/src/osmEditDialog.js
+++ b/src/osmEditDialog.js
@@ -23,12 +23,17 @@
 const _ = imports.gettext.gettext;
 
 const Gio = imports.gi.Gio;
+const GObject = imports.gi.GObject;
 const Gtk = imports.gi.Gtk;
 const Lang = imports.lang;
 
 const Application = imports.application;
+const Maps = imports.gi.GnomeMaps;
 const OSMConnection = imports.osmConnection;
+const OSMTypeLookup = imports.osmTypeLookup;
+const OSMTypeSearchEntry = imports.osmTypeSearchEntry;
 const OSMUtils = imports.osmUtils;
+const Utils = imports.utils;
 
 const Response = {
     UPLOADED: 0,
@@ -88,17 +93,43 @@ const OSMEditDialog = new Lang.Class({
                         'editorGrid',
                         'commentTextView',
                         'addFieldPopoverGrid',
-                        'addFieldButton'],
+                        'addFieldButton',
+                        'typeSearchGrid',
+                        'typeLabel',
+                        'typeButton',
+                        'typeValueLabel',
+                        'recentTypesLabel',
+                        'recentTypesListBox',
+                        'headerBar'],
 
     _init: function(params) {
         this._place = params.place;
         delete params.place;
 
+        this._addLocation = params.addLocation;
+        delete params.addLocation;
+
+        this._latitude = params.latitude;
+        delete params.latitude;
+
+        this._longitude = params.longitude;
+        delete params.longitude;
+
         /* This is a construct-only property and cannot be set by GtkBuilder */
         params.use_header_bar = true;
 
         this.parent(params);
 
+        /* It seems it's not possible to use a custom widget implemented in GJS
+           from within a widget template */
+        this._typeSearch = new OSMTypeSearchEntry.OSMTypeSearchEntry();
+        this._typeSearchGrid.attach(this._typeSearch, 0, 0, 1, 1);
+        this._typeSearch.visible = true;
+        this._typeSearch.can_focus = true;
+
+        let typeSearchPopover = this._typeSearch.popover;
+        typeSearchPopover.connect('selected', this._onTypeSelected.bind(this));
+
         this._cancellable = new Gio.Cancellable();
         this._cancellable.connect((function() {
             this.response(Response.CANCELLED);
@@ -108,14 +139,48 @@ const OSMEditDialog = new Lang.Class({
             this._cancellable.cancel();
         }).bind(this));
 
+        if (this._addLocation) {
+            this._headerBar.title = _("Add Location");
+            this._typeLabel.visible = true;
+            this._typeButton.visible = true;
+        }
+
         this._isEditing = false;
         this._nextButton.connect('clicked', this._onNextClicked.bind(this));
         this._cancelButton.connect('clicked', this._onCancelClicked.bind(this));
         this._backButton.connect('clicked', this._onBackClicked.bind(this));
+        this._typeButton.connect('clicked', this._onTypeClicked.bind(this));
+
+        if (this._addLocation) {
+            this._headerBar.title = _("Add Location");
+
+            /* the OSMObject ID, version, and changeset ID is unknown for now */
+            let newNode =
+                Maps.OSMNode.new(0, 0, 0, this._longitude, this._latitude);
+            /* set a placeholder name tag to always get a name entry for new
+               locations */
+            newNode.set_tag('name', '');
+            this._loadOSMData(newNode);
+            this._isEditing = true;
+        } else {
+            this._osmType = this._place.osmType;
+            Application.osmEdit.fetchObject(this._place,
+                                            this._onObjectFetched.bind(this),
+                                            this._cancellable);
+        }
+
+        /* store original title of the dialog to be able to restore it when
+           coming back from type selection */
+        this._originalTitle = this._headerBar.title;
+        this._updateRecentTypesList();
+
+        this._recentTypesListBox.set_header_func(function (row, previous) {
+            row.set_header(new Gtk.Separator());
+        });
 
-        Application.osmEdit.fetchObject(this._place,
-                                        this._onObjectFetched.bind(this),
-                                        this._cancellable);
+        this._recentTypesListBox.connect('row-activated', (function(listbox, row) {
+            this._onTypeSelected(null, row._key, row._value, row._title);
+        }).bind(this));
     },
 
     _onNextClicked: function() {
@@ -130,11 +195,130 @@ const OSMEditDialog = new Lang.Class({
             // upload data to OSM
             let comment = this._commentTextView.buffer.text;
             Application.osmEdit.uploadObject(this._osmObject,
-                                             this._place.osmType, comment,
+                                             this._osmType, comment,
                                              this._onObjectUploaded.bind(this));
         }
     },
 
+    _onTypeClicked: function() {
+        this._cancelButton.visible = false;
+        this._backButton.visible = true;
+        this._headerBar.title = _("Select Type");
+        this._stack.visible_child_name = 'select-type';
+    },
+
+    _onTypeSelected: function(popover, key, value, title) {
+        this._typeValueLabel.label = title;
+        this._updateType(key, value);
+
+        if (popover)
+            popover.hide();
+
+        /* clear out type search entry */
+        this._typeSearch.text = '';
+
+        /* go back to the editing stack page */
+        this._backButton.visible = false;
+        this._cancelButton.visible = true;
+        this._stack.visible_child_name = 'editor';
+        this._headerBar.title = this._originalTitle;
+
+        /* update recent types store */
+        OSMTypeLookup.recentTypesStore.pushType(key, value);
+
+        /* enable the Next button, so that it's possible to just change the type
+           of an object without changing anything else */
+        this._nextButton.sensitive = true;
+
+        this._updateRecentTypesList();
+    },
+
+    _updateType: function(key, value) {
+        /* clear out any previous type-related OSM tags */
+        OSMTypeLookup.OSM_TYPE_TAGS.forEach((function (tag) {
+            this._osmObject.delete_tag(tag);
+        }).bind(this));
+
+        this._osmObject.set_tag(key, value);
+    },
+
+    /* update visibility and enable the type selection button if the object has
+       a well-known type (based on a known set of tags) */
+    _updateTypeButton: function() {
+        let numTypeTags = 0;
+        let lastTypeTag = null;
+
+        for (let i = 0; i < OSMTypeLookup.OSM_TYPE_TAGS.length; i++) {
+            let key = OSMTypeLookup.OSM_TYPE_TAGS[i];
+            let value = this._osmObject.get_tag(key);
+
+            if (value != null) {
+                numTypeTags++;
+                lastTypeTag = key;
+            }
+        }
+
+        /* if the object has none of tags set, enable the button and keep the
+           pre-set "None" label */
+        if (numTypeTags === 0) {
+            this._typeLabel.visible = true;
+            this._typeButton.visible = true;
+        } else if (numTypeTags === 1) {
+            let value = this._osmObject.get_tag(lastTypeTag);
+            let typeTitle = OSMTypeLookup.lookupType(lastTypeTag, value);
+
+            /* if the type tag has a value we know of, and possible has
+               translations for */
+            if (typeTitle != null) {
+                this._typeValueLabel.label = typeTitle;
+                this._typeLabel.visible = true;
+                this._typeButton.visible = true;
+            }
+        }
+    },
+
+    _updateRecentTypesList: function() {
+        let recentTypes = OSMTypeLookup.recentTypesStore.recentTypes;
+
+        if (recentTypes.length > 0) {
+            let children = this._recentTypesListBox.get_children();
+
+            for (let i = 0; i < children.length; i++) {
+                children[i].destroy();
+            }
+
+            this._recentTypesLabel.visible = true;
+            this._recentTypesListBox.visible = true;
+
+            for (let i = 0; i < recentTypes.length; i++) {
+                let key = recentTypes[i].key;
+                let value = recentTypes[i].value;
+                let title = OSMTypeLookup.lookupType(key, value);
+
+                let row = new Gtk.ListBoxRow({visible: true, hexpand: true});
+                let grid = new Gtk.Grid({visible: true,
+                                         margin_top: 6, margin_bottom: 6,
+                                         margin_start: 12, margin_end: 12});
+                let label = new Gtk.Label({visible: true, halign: Gtk.Align.START,
+                                           label: title});
+
+                label.get_style_context().add_class('dim-label');
+
+                row._title = title;
+                row._key = key;
+                row._value = value;
+
+                row.add(grid);
+                grid.add(label);
+
+                this._recentTypesListBox.add(row);
+            }
+        } else {
+            this._recentTypesLabel.visible = false;
+            this._recentTypesListBox.visible = false;
+        }
+    },
+
     _switchToUpload: function() {
         this._stack.set_visible_child_name('upload');
         this._nextButton.label = _("Done");
@@ -155,6 +339,7 @@ const OSMEditDialog = new Lang.Class({
         this._stack.set_visible_child_name('editor');
         this._isEditing = true;
         this._commentTextView.buffer.text = '';
+        this._headerBar.title = this._originalTitle;
     },
 
     _onObjectFetched: function(success, status, osmObject, osmType, error) {
@@ -196,7 +381,7 @@ const OSMEditDialog = new Lang.Class({
 
     /* GtkContainer.child_get_property doesn't seem to be usable from GJS */
     _getRowOfDeleteButton: function(button) {
-        for (let row = 0;; row++) {
+        for (let row = 1;; row++) {
             let label = this._editorGrid.get_child_at(0, row);
             let deleteButton = this._editorGrid.get_child_at(2, row);
 
@@ -369,13 +554,12 @@ const OSMEditDialog = new Lang.Class({
         }
     },
 
-    _loadOSMData: function(osmObject, osmType) {
+    _loadOSMData: function(osmObject) {
         this._osmObject = osmObject;
-        this._osmType = osmType;
 
         /* keeps track of the current insertion row in the grid for editing
            widgets */
-        this._currentRow = 0;
+        this._currentRow = 1;
 
         /* create edit widgets */
         for (let i = 0; i < OSM_FIELDS.length; i++) {
@@ -391,6 +575,7 @@ const OSMEditDialog = new Lang.Class({
         }
 
         this._updateAddFieldMenu();
+        this._updateTypeButton();
         this._stack.visible_child_name = 'editor';
     }
 });
diff --git a/src/osmTypeListRow.js b/src/osmTypeListRow.js
new file mode 100644
index 0000000..00c7577
--- /dev/null
+++ b/src/osmTypeListRow.js
@@ -0,0 +1,52 @@
+/* -*- Mode: JS2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- */
+/* vim: set et ts=4 sw=4: */
+/*
+ * Copyright (c) 2015 Marcus Lundblad.
+ *
+ * GNOME Maps 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 2 of the License, or (at your
+ * option) any later version.
+ *
+ * GNOME Maps 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 GNOME Maps; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Marcus Lundblad <ml update uu se>
+ */
+
+const Gtk = imports.gi.Gtk;
+const Lang = imports.lang;
+
+const OSMTypeListRow = new Lang.Class({
+    Name: 'OSMTypeListRow',
+    Extends: Gtk.ListBoxRow,
+    Template: 'resource:///org/gnome/Maps/ui/osm-type-list-row.ui',
+    InternalChildren: [ 'name' ],
+
+    _init: function(props) {
+        this._type = props.type;
+        delete props.type;
+
+        this.parent(props);
+
+        this._name.label = this._type.title;
+    },
+
+    get key() {
+        return this._type.key;
+    },
+
+    get value() {
+        return this._type.value;
+    },
+
+    get title() {
+        return this._type.title;
+    }
+});
+
diff --git a/src/osmTypeLookup.js b/src/osmTypeLookup.js
new file mode 100644
index 0000000..0c1eaea
--- /dev/null
+++ b/src/osmTypeLookup.js
@@ -0,0 +1,170 @@
+/* -*- Mode: JS2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- */
+/* vim: set et ts=4 sw=4: */
+/*
+ * Copyright (c) 2015 Marcus Lundblad.
+ *
+ * GNOME Maps 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 2 of the License, or (at your
+ * option) any later version.
+ *
+ * GNOME Maps 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 GNOME Maps; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Marcus Lundblad <ml update uu se>
+ */
+
+const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
+const Lang = imports.lang;
+
+const Utils = imports.utils;
+
+const _RECENT_TYPES_STORE_FILE = 'maps-recent-types.json';
+const _NUM_RECENT_TYPES = 10;
+
+/* Lists the OSM tags we base our notion of location types on */
+const OSM_TYPE_TAGS = ['amenity', 'leisure', 'office', 'place', 'shop', 'tourism' ];
+
+const _file = Gio.file_new_for_uri('resource://org/gnome/Maps/osm-types.json');
+const [_status, _buffer] = _file.load_contents(null);
+const OSM_TYPE_MAP = JSON.parse(_buffer);
+
+/* Sort function comparing two type values accoring to the locale-specific
+   comparison of the type title */
+function _sortType(t1, t2) {
+    return t1.title.toLocaleLowerCase().localeCompare(t2.title.toLocaleLowerCase());
+}
+
+/* find the localized display title and a normalized locale-specific lower case
+   value for search purposes, given a type mapping value,
+   also cache the title to avoid re-iterating the language map every time,
+   and store the lower-case normalized title in the current locale */
+function _lookupTitle(item) {
+    let langs = GLib.get_language_names();
+    let title = item.cachedTitle;
+
+    if (title)
+        return [title, item.normalizedTitle];
+
+    for (let i = 0; i < langs.length; i++) {
+        title = item.title[langs[i].replace('_', '-')];
+
+        if (title) {
+            let normalizedTitle = title.toLocaleLowerCase();
+
+            item.cachedTitle = title;
+            item.normalizedTitle = normalizedTitle;
+            return [title, normalizedTitle];
+        }
+    }
+
+    return null;
+}
+
+function findMatches(prefix, maxMatches) {
+    let numMatches = 0;
+    let prefixLength = prefix.length;
+    let normalized = prefix.toLocaleLowerCase();
+    let matches = [];
+
+    for (let type in OSM_TYPE_MAP) {
+        let item = OSM_TYPE_MAP[type];
+        let [title, normalizedTitle] = _lookupTitle(item);
+        let parts = type.split('/');
+
+        /* if the (locale-case-normalized) title matches parts of the search
+           string, or as a convenience for expert mappers, if the search string
+           is prefix of the raw OSM tag value */
+        if (normalizedTitle.indexOf(normalized) != -1
+            || (prefixLength >= 3 && parts[1].startsWith(prefix))) {
+            numMatches++;
+            matches.push({key: parts[0], value: parts[1], title: title});
+        }
+
+        if (numMatches === maxMatches)
+            break;
+    }
+
+    return matches.sort(_sortType);
+}
+
+/* return the title of a type with a given key/value if it is known by us */
+function lookupType(key, value) {
+    let item = OSM_TYPE_MAP[key + '/' + value];
+
+    if (item) {
+        let [title, _] = _lookupTitle(item);
+        return title;
+    } else
+        return null;
+}
+
+const RecentTypesStore = new Lang.Class({
+    Name: 'RecentTypesStore',
+    Extends: GLib.Object,
+
+    _init: function() {
+        this.parent();
+        this._filename = GLib.build_filenamev([GLib.get_user_data_dir(),
+                                              _RECENT_TYPES_STORE_FILE]);
+        this._load();
+    },
+
+    get recentTypes() {
+        return this._recentTypes;
+    },
+
+    _load: function() {
+        if (!GLib.file_test(this._filename, GLib.FileTest.EXISTS)) {
+            this._recentTypes = [];
+            return;
+        }
+
+        let buffer = Utils.readFile(this._filename);
+        if (buffer === null) {
+            this._recentTypes = [];
+            return;
+        }
+
+        this._recentTypes = JSON.parse(buffer);
+    },
+
+    _save: function() {
+        let buffer = JSON.stringify(this._recentTypes);
+        if (!Utils.writeFile(this._filename, buffer))
+            log('Failed to write recent types file!');
+    },
+
+    /* push a type key/value as the most recently used type */
+    pushType: function(key, value) {
+        /* find out if the type is already stored */
+        let pos = -1;
+        for (let i = 0; i < this._recentTypes.length; i++) {
+            if (this._recentTypes[i].key === key &&
+                this._recentTypes[i].value === value) {
+                pos = i;
+                break;
+            }
+        }
+
+        /* remove the type if it was already found in the list */
+        if (pos != -1)
+            this._recentTypes.splice(pos, 1);
+
+        this._recentTypes.unshift({key: key, value: value});
+
+        /* prune the list */
+        this._recentTypes.splice(_NUM_RECENT_TYPES);
+
+        this._save();
+    }
+});
+
+const recentTypesStore = new RecentTypesStore();
+
diff --git a/src/osmTypeSearchEntry.js b/src/osmTypeSearchEntry.js
new file mode 100644
index 0000000..ce1d8b1
--- /dev/null
+++ b/src/osmTypeSearchEntry.js
@@ -0,0 +1,76 @@
+/* -*- Mode: JS2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- */
+/* vim: set et ts=4 sw=4: */
+/*
+ * Copyright (c) 2015 Marcus Lundblad.
+ *
+ * GNOME Maps 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 2 of the License, or (at your
+ * option) any later version.
+ *
+ * GNOME Maps 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 GNOME Maps; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Marcus Lundblad <ml update uu se>
+ */
+
+const Gtk = imports.gi.Gtk;
+const Lang = imports.lang;
+
+const OSMTypeSearchPopover = imports.osmTypeSearchPopover;
+const OSMTypeLookup = imports.osmTypeLookup;
+const Utils = imports.utils;
+
+const MAX_MATCHES = 10;
+
+const OSMTypeSearchEntry = new Lang.Class({
+    Name: 'OSMTypeSearchEntry',
+    Extends: Gtk.SearchEntry,
+    Template: 'resource:///org/gnome/Maps/ui/osm-type-search-entry.ui',
+
+    _init: function(props) {
+        this.parent(props);
+
+        this._popover =
+            new OSMTypeSearchPopover.OSMTypeSearchPopover({relative_to: this});
+
+        this.connect('size-allocate', (function(widget, allocation) {
+            // Magic number to make the alignment pixel perfect.
+            let width_request = allocation.width + 20;
+            this._popover.width_request = width_request;
+        }).bind(this));
+
+        this.connect('search-changed', this._onSearchChanged.bind(this));
+        this.connect('activate', this._onSearchChanged.bind(this));
+    },
+
+    get popover() {
+        return this._popover;
+    },
+
+    _onSearchChanged: function() {
+        if (this.text.length === 0) {
+            this._popover.hide();
+            return;
+        }
+
+        /* Note: Not sure if searching already on one character might be a bit
+           too much, but unsure about languages such as Chinese and Japanese
+           using ideographs. */
+        if (this.text.length >= 1) {
+            let matches = OSMTypeLookup.findMatches(this.text, MAX_MATCHES);
+
+            if (matches.length > 0) {
+                /* show search results */
+                this._popover.showMatches(matches);
+            } else {
+                this._popover.hide();
+            }
+        }
+    }
+});
diff --git a/src/osmTypeSearchPopover.js b/src/osmTypeSearchPopover.js
new file mode 100644
index 0000000..9416022
--- /dev/null
+++ b/src/osmTypeSearchPopover.js
@@ -0,0 +1,68 @@
+/* -*- Mode: JS2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- */
+/* vim: set et ts=4 sw=4: */
+/*
+ * Copyright (c) 2015 Marcus Lundblad.
+ *
+ * GNOME Maps 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 2 of the License, or (at your
+ * option) any later version.
+ *
+ * GNOME Maps 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 GNOME Maps; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Marcus Lundblad <ml update uu se>
+ */
+
+const GObject = imports.gi.GObject;
+const Gtk = imports.gi.Gtk;
+const Lang = imports.lang;
+
+const OSMTypeListRow = imports.osmTypeListRow;
+const PropagatingSearchPopover = imports.propagatingSearchPopover;
+
+const OSMTypeSearchPopover = new Lang.Class({
+    Name: 'OSMTypeSearchPopover',
+    Extends: PropagatingSearchPopover.PropagatingSearchPopover,
+    Template: 'resource:///org/gnome/Maps/ui/osm-type-search-popover.ui',
+    Signals : {
+        /* signal emitted when selecting a type, indicates OSM key and value
+           and display title */
+        'selected' : { param_types: [ GObject.TYPE_STRING,
+                                      GObject.TYPE_STRING,
+                                      GObject.TYPE_STRING ] }
+    },
+    InternalChildren: ['list'],
+
+    _init: function(props) {
+        this.parent(props);
+
+        this._list.connect('row-activated', (function(list, row) {
+            if (row)
+                this.emit('selected', row.key, row.value, row.title);
+        }).bind(this));
+    },
+
+    showMatches: function(matches) {
+        this._list.forall(function(row) {
+            row.destroy();
+        });
+
+        matches.forEach((function(type) {
+            this._addRow(type);
+        }).bind(this));
+        this.show();
+    },
+
+    _addRow: function(type) {
+        let row = new OSMTypeListRow.OSMTypeListRow({ type: type,
+                                                      can_focus: true });
+        this._list.add(row);
+    }
+ });
+ 


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