diff options
author | Marcus Lundblad <ml@update.uu.se> | 2016-01-31 15:01:27 +0100 |
---|---|---|
committer | Marcus Lundblad <ml@update.uu.se> | 2016-02-12 08:34:54 +0100 |
commit | c4c25a8ed6553e081c3b723d2bbc8cd729a6dce8 (patch) | |
tree | d65b18b5ce711a861accbc399958d58614341bd4 | |
parent | 00aad3df22d846ca862caf36a6e39446cfa5ce6f (diff) | |
download | gnome-maps-c4c25a8ed6553e081c3b723d2bbc8cd729a6dce8.tar.gz |
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.
Also enables editing the POI type of existing objects in more simple
cases (known tag values with no combined tags).
https://bugzilla.gnome.org/show_bug.cgi?id=761327
-rw-r--r-- | data/org.gnome.Maps.data.gresource.xml | 3 | ||||
-rw-r--r-- | data/ui/osm-edit-dialog.ui | 111 | ||||
-rw-r--r-- | data/ui/osm-type-list-row.ui | 29 | ||||
-rw-r--r-- | data/ui/osm-type-popover.ui | 16 | ||||
-rw-r--r-- | data/ui/osm-type-search-entry.ui | 10 | ||||
-rw-r--r-- | src/application.js | 8 | ||||
-rw-r--r-- | src/org.gnome.Maps.src.gresource.xml | 4 | ||||
-rw-r--r-- | src/osmEditDialog.js | 217 | ||||
-rw-r--r-- | src/osmTypeListRow.js | 51 | ||||
-rw-r--r-- | src/osmTypePopover.js | 67 | ||||
-rw-r--r-- | src/osmTypeSearchEntry.js | 76 | ||||
-rw-r--r-- | src/osmTypes.js | 168 |
12 files changed, 738 insertions, 22 deletions
diff --git a/data/org.gnome.Maps.data.gresource.xml b/data/org.gnome.Maps.data.gresource.xml index 81dc771a..7483d3d4 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-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 3f2623d9..b3336066 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> @@ -168,7 +277,7 @@ <property name="visible">True</property> <property name="can-focus">False</property> <property name="show-close-button">False</property> - <property name="title" translatable="yes">Edit Location</property> + <property name="title" translatable="yes">Edit on OpenStreetMap</property> <child> <object class="GtkButton" id="cancelButton"> <property name="label" translatable="yes">Cancel</property> diff --git a/data/ui/osm-type-list-row.ui b/data/ui/osm-type-list-row.ui new file mode 100644 index 00000000..4dda2381 --- /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-popover.ui b/data/ui/osm-type-popover.ui new file mode 100644 index 00000000..4ae9e7fb --- /dev/null +++ b/data/ui/osm-type-popover.ui @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="3.12"/> + <template class="Gjs_OSMTypePopover" parent="Gjs_SearchPopover"> + <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/data/ui/osm-type-search-entry.ui b/data/ui/osm-type-search-entry.ui new file mode 100644 index 00000000..db292a76 --- /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/src/application.js b/src/application.js index aad5fd09..74b6fc89 100644 --- a/src/application.js +++ b/src/application.js @@ -38,6 +38,7 @@ const MainWindow = imports.mainWindow; const Maps = imports.gi.GnomeMaps; const NotificationManager = imports.notificationManager; const OSMEdit = imports.osmEdit; +const OSMTypeSearchEntry = imports.osmTypeSearchEntry; const PlaceStore = imports.placeStore; const RouteService = imports.routeService; const Settings = imports.settings; @@ -57,6 +58,9 @@ let contactStore = null; let osmEdit = null; let normalStartup = true; +const _ensuredTypes = [WebKit2.WebView, + OSMTypeSearchEntry.OSMTypeSearchEntry]; + const Application = new Lang.Class({ Name: 'Application', Extends: Gtk.Application, @@ -83,7 +87,9 @@ const Application = new Lang.Class({ GLib.set_prgname('gnome-maps'); /* Needed to be able to use in UI files */ - GObject.type_ensure(WebKit2.WebView); + _ensuredTypes.forEach(function(type) { + GObject.type_ensure(type); + }); this.parent({ application_id: 'org.gnome.Maps', flags: Gio.ApplicationFlags.HANDLES_OPEN }); diff --git a/src/org.gnome.Maps.src.gresource.xml b/src/org.gnome.Maps.src.gresource.xml index ba3150cf..ec88b7fa 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>osmTypePopover.js</file> + <file>osmTypes.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 f488ca42..2bace3ad 100644 --- a/src/osmEditDialog.js +++ b/src/osmEditDialog.js @@ -28,8 +28,12 @@ const Gtk = imports.gi.Gtk; const Lang = imports.lang; const Application = imports.application; +const Maps = imports.gi.GnomeMaps; const OSMConnection = imports.osmConnection; +const OSMTypes = imports.osmTypes; +const OSMTypeSearchEntry = imports.osmTypeSearchEntry; const OSMUtils = imports.osmUtils; +const Utils = imports.utils; const Response = { UPLOADED: 0, @@ -54,7 +58,7 @@ let _osmWikipediaRewriteFunc = function(text) { let wikipediaArticleFormatted = OSMUtils.getWikipediaOSMArticleFormatFromUrl(text); /* if the entered text is a Wikipedia link, - substitute it with the OSM-formatted Wikipedia article tag */ + * substitute it with the OSM-formatted Wikipedia article tag */ if (wikipediaArticleFormatted) return wikipediaArticleFormatted; else @@ -106,17 +110,44 @@ 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); + /* I could not get this widget working from within the widget template + * this results in a segfault. The widget definition is left in-place, + * but commented-out in the template file */ + 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); @@ -130,29 +161,176 @@ const OSMEditDialog = new Lang.Class({ 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 to OpenStreetMap"); + this._typeLabel.visible = true; + this._typeButton.visible = true; + + /* 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(); - Application.osmEdit.fetchObject(this._place, - this._onObjectFetched.bind(this), - this._cancellable); + this._recentTypesListBox.set_header_func(function (row, previous) { + row.set_header(new Gtk.Separator()); + }); + + this._recentTypesListBox.connect('row-activated', (function(listbox, row) { + this._onTypeSelected(null, row._key, row._value, row._title); + }).bind(this)); }, _onNextClicked: function() { if (this._isEditing) { - // switch to the upload view this._switchToUpload(); } else { - // turn on spinner this._stack.visible_child_name = 'loading'; this._nextButton.sensitive = false; this._backButton.sensitive = false; - // 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 */ + OSMTypes.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 */ + OSMTypes.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 < OSMTypes.OSM_TYPE_TAGS.length; i++) { + let key = OSMTypes.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 = OSMTypes.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 = OSMTypes.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 = OSMTypes.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"); @@ -173,6 +351,8 @@ const OSMEditDialog = new Lang.Class({ this._stack.set_visible_child_name('editor'); this._isEditing = true; this._commentTextView.buffer.text = ''; + this._typeSearch.text = ''; + this._headerBar.title = this._originalTitle; }, _onObjectFetched: function(success, status, osmObject, osmType, error) { @@ -195,7 +375,7 @@ const OSMEditDialog = new Lang.Class({ _showError: function(status, error) { /* set error message from specific error if available, otherwise use - a generic error message for the HTTP status code */ + * a generic error message for the HTTP status code */ let statusMessage = error ? error.message : OSMConnection.getStatusMessage(status); let messageDialog = @@ -214,7 +394,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); @@ -357,8 +537,8 @@ const OSMEditDialog = new Lang.Class({ this._addFieldButton.active = false; this._addOSMField(label, tag, '', type, rewriteFunc); /* add a "placeholder" empty OSM tag to keep the add field - menu updated, these tags will be filtered out if nothing - is entered */ + * menu updated, these tags will be filtered out if nothing + * is entered */ this._osmObject.set_tag(tag, ''); this._updateAddFieldMenu(); }).bind(this, label, tag, type, rewriteFunc)); @@ -368,8 +548,6 @@ const OSMEditDialog = new Lang.Class({ } } - /* update sensitiveness of the add details button, set it as - insensitive if all tags we support editing is already present */ this._addFieldButton.sensitive = !hasAllFields; }, @@ -387,15 +565,13 @@ 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; + * widgets */ + this._currentRow = 1; - /* create edit widgets */ for (let i = 0; i < OSM_FIELDS.length; i++) { let fieldSpec = OSM_FIELDS[i]; let name = fieldSpec.name; @@ -409,6 +585,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 00000000..ae5e17c4 --- /dev/null +++ b/src/osmTypeListRow.js @@ -0,0 +1,51 @@ +/* -*- 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/osmTypePopover.js b/src/osmTypePopover.js new file mode 100644 index 00000000..8881a028 --- /dev/null +++ b/src/osmTypePopover.js @@ -0,0 +1,67 @@ +/* -*- 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 SearchPopover = imports.searchPopover; + +const OSMTypePopover = new Lang.Class({ + Name: 'OSMTypePopover', + Extends: SearchPopover.SearchPopover, + InternalChildren: ['list'], + Template: 'resource:///org/gnome/Maps/ui/osm-type-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 ] } + }, + + _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); + } +}); diff --git a/src/osmTypeSearchEntry.js b/src/osmTypeSearchEntry.js new file mode 100644 index 00000000..2bbebbec --- /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 OSMTypePopover = imports.osmTypePopover; +const OSMTypes = imports.osmTypes; +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 OSMTypePopover.OSMTypePopover({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 = OSMTypes.findMatches(this.text, MAX_MATCHES); + + if (matches.length > 0) { + /* show search results */ + this._popover.showMatches(matches); + } else { + this._popover.hide(); + } + } + } +}); diff --git a/src/osmTypes.js b/src/osmTypes.js new file mode 100644 index 00000000..c464a743 --- /dev/null +++ b/src/osmTypes.js @@ -0,0 +1,168 @@ +/* -*- 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', + + _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(); |