From f43ef06db996e76c190768b544413a58d888afd8 Mon Sep 17 00:00:00 2001 From: teamcons Date: Thu, 22 Oct 2026 21:26:58 +0200 Subject: [PATCH 01/19] Initial draft: Introduce MarkerManager, Storage, Marker and MarkerData --- src/MapWidget.vala | 12 ++++ src/Objects/MarkerData.vala | 53 ++++++++++++++++ src/Services/MarkerManager.vala | 72 +++++++++++++++++++++ src/Services/Storage.vala | 84 ++++++++++++++++++++++++ src/Widgets/Marker.vala | 109 ++++++++++++++++++++++++++++++++ src/meson.build | 8 +++ 6 files changed, 338 insertions(+) create mode 100644 src/Objects/MarkerData.vala create mode 100644 src/Services/MarkerManager.vala create mode 100644 src/Services/Storage.vala create mode 100644 src/Widgets/Marker.vala diff --git a/src/MapWidget.vala b/src/MapWidget.vala index 6f69f2b1..ea1dd783 100644 --- a/src/MapWidget.vala +++ b/src/MapWidget.vala @@ -50,6 +50,18 @@ public class Maps.MapWidget : Gtk.Box { location_layer = new Shumate.MarkerLayer (map_widget.viewport); map_widget.add_overlay_layer (location_layer); + + + var markermanager = new MarkerManager (pin_layer); + + var markerdata = new MarkerData () { + latitude = 48.858400, + longitude = 2.294500, + name = "Eiffel Tower", + notes = "Oooh" + }; + + markermanager.add_marker (markerdata); } // Set the initial location of the map widget. diff --git a/src/Objects/MarkerData.vala b/src/Objects/MarkerData.vala new file mode 100644 index 00000000..6717dd4d --- /dev/null +++ b/src/Objects/MarkerData.vala @@ -0,0 +1,53 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + * 2018-2025 Ryo Nakano + * 2014-2015 Atlas Developers + */ + +public class Maps.MarkerData : Object { + + private static int last_id = 0; + + public double latitude {get; set;} + public double longitude {get; set;} + public string name {get; set;} + public string notes {get; set;} + public int id {get; private set;} + + public MarkerData () { + id = last_id; + last_id++; + } + + /** + * Used to recreate individual points of interest from storage data + */ + public MarkerData.from_json (Json.Object node) { + latitude = node.get_double_member ("lat"); + longitude = node.get_double_member ("long"); + name = node.get_string_member ("name"); + notes = node.get_string_member ("notes"); + } + + /*************************************************/ + /** + * Used to convert points of interest into objects we can store + */ + public Json.Object to_json () { + var builder = new Json.Builder (); + + builder.begin_object (); + builder.set_member_name ("lat"); + builder.add_double_value (latitude); + builder.set_member_name ("long"); + builder.add_double_value (longitude); + builder.set_member_name ("name"); + builder.add_string_value (name); + builder.set_member_name ("notes"); + builder.add_string_value (notes); + builder.end_object (); + + return builder.get_root ().get_object (); + } +} diff --git a/src/Services/MarkerManager.vala b/src/Services/MarkerManager.vala new file mode 100644 index 00000000..6e8bd5a8 --- /dev/null +++ b/src/Services/MarkerManager.vala @@ -0,0 +1,72 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + * 2018-2025 Ryo Nakano + * 2014-2015 Atlas Developers + */ + +public class Maps.MarkerManager : Object { + private Storage storage; + private Gee.ArrayList marker_list; + private unowned Shumate.MarkerLayer markerlayer; + + + public MarkerManager (Shumate.MarkerLayer markerlayer) { + this.markerlayer = markerlayer; + } + + construct { + storage = new Storage (); + } + + public async void populate_layer () { + var markerdata_array = storage.load (); + + foreach (var markerdata_note in markerdata_array.get_elements ()) { + var markerdata = new MarkerData.from_json (markerdata_note.get_object ()); + + add_marker (markerdata); + } + + save_all (); + } + + public void add_marker (MarkerData markerdata) { + var marker = new Marker (markerdata); + marker_list.add (marker); + markerlayer.add_marker (marker); + + marker.deletion_requested.connect (remove_marker); + } + + public void remove_marker (int marker_id) { + + // 0 is our current location + if (marker_id == 0) { + return; + } + + // Each MarkerData has an unique ID that mirrors their position in the list + var marker = marker_list[marker_id]; + marker.destroy (); + marker_list[marker_id] = null; + + save_all (); + } + + private void save_all () { + var json_array = new Json.Array (); + + foreach (var marker in marker_list) { + + // Deleted markers are set to null + if (marker == null) { + continue; + } + + json_array.add_object_element (marker.markerdata.to_json ()); + } + + storage.save (json_array); + } +} \ No newline at end of file diff --git a/src/Services/Storage.vala b/src/Services/Storage.vala new file mode 100644 index 00000000..8768d296 --- /dev/null +++ b/src/Services/Storage.vala @@ -0,0 +1,84 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + * 2018-2025 Ryo Nakano + * 2014-2015 Atlas Developers + */ + +public class Maps.Storage : Object { + + private const string STORAGE_FILENAME = "locations.json"; + private File datadir; + private string storage_file_path; + + construct { + var path_data = Environment.get_user_data_dir (); + datadir = File.new_for_path (path_data); + ensure_datadir (); + + storage_file_path = GLib.Path.build_path (Path.DIR_SEPARATOR_S, path_data, STORAGE_FILENAME); + } + + /*************************************************/ + /** + * Persistently check for the data directory and create if there is none + */ + private void ensure_datadir () { + debug ("do we have a data directory?"); + if (datadir.query_exists ()) { + debug ("Yes, nevermind"); + return; + } + + try { + datadir.make_directory_with_parents (); + debug ("yes we do now"); + + } catch (Error e) { + warning ("Failed to prepare target data directory %s", e.message); + } + } + + /*************************************************/ + /** + * Converts a Json.Node into a string and take care of saving it + */ + public void save (Json.Array json_data) { + ensure_datadir (); + debug ("Writing %u elements (Should be same number as points of interest)", json_data.get_length ()); + + try { + var generator = new Json.Generator (); + var node = new Json.Node (Json.NodeType.ARRAY); + node.set_array (json_data); + generator.set_root (node); + generator.to_file (storage_file_path); + + } catch (Error e) { + warning ("Failed to save points of interest %s", e.message); + } + } + + /*************************************************/ + /** + * Grab from storage, into a Json.Node we can parse. Insist if necessary + */ + public Json.Array load () { + + var parser = new Json.Parser (); + var array = new Json.Array (); + + try { + parser.load_from_mapped_file (storage_file_path); + var node = parser.get_root (); + array = node.get_array (); + + } catch (Error e) { + warning ("Failed to load from storage %s", e.message); + + } + + debug ("Retrieved %ui elements", array.get_length ()); + return array; + } +} \ No newline at end of file diff --git a/src/Widgets/Marker.vala b/src/Widgets/Marker.vala new file mode 100644 index 00000000..982b3aec --- /dev/null +++ b/src/Widgets/Marker.vala @@ -0,0 +1,109 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + * 2018-2025 Ryo Nakano + * 2014-2015 Atlas Developers + */ + +public class Maps.Marker : Shumate.Marker { + + public MarkerData markerdata {get; construct;} + + private Gtk.Popover popover; + public signal void deletion_requested (int markerdata_id); + + public Marker (MarkerData markerdata) { + Object ( + markerdata: markerdata, + latitude: markerdata.latitude, + longitude: markerdata.longitude + ); + } + + construct { + var box = new Granite.Box (Gtk.Orientation.VERTICAL, Granite.Box.Spacing.SINGLE) { + margin_top = 10, + margin_bottom = 10, + margin_start = 10, + margin_end = 10 + }; + + var name_entry = new Gtk.Entry () { + text = markerdata.name + }; + + var name_label = new Granite.HeaderLabel (_("Name:")) { + mnemonic_widget = name_entry + }; + + var comment_textview = new Granite.HyperTextView () { + accepts_tab = false, + wrap_mode = WORD_CHAR, + top_margin = 3, + right_margin = 3, + bottom_margin = 3, + left_margin = 3, + }; + + comment_textview.buffer.text = markerdata.notes; + + var comment_label = new Granite.HeaderLabel (_("Comments:")) { + mnemonic_widget = comment_textview + }; + + var scrolled = new Gtk.ScrolledWindow () { + child = comment_textview, + height_request = 100, + hexpand = true, + vexpand = true, + hscrollbar_policy = NEVER + }; + + var frame = new Gtk.Frame (null) { + child = scrolled + }; + + var delete_button = new Gtk.Button.with_label (_("Delete…")) { + halign = Gtk.Align.START, + }; + delete_button.add_css_class (Granite.CssClass.DESTRUCTIVE); + + //box.append (name_label); + box.append (name_entry); + //box.append (comment_label); + box.append (frame); + box.append (delete_button); + + popover = new Gtk.Popover () { + child = box, + position = Gtk.PositionType.TOP + }; + + var menubutton = new Gtk.MenuButton () { + popover = popover, + icon_name = "mail-unread", + tooltip_text = markerdata.name + }; + + child = menubutton; + + name_entry.bind_property ("text", + markerdata, "name", + GLib.BindingFlags.DEFAULT); + + name_entry.bind_property ("text", + menubutton, "tooltip-text", + GLib.BindingFlags.DEFAULT); + + comment_textview.buffer.bind_property ("text", + markerdata, "notes", + GLib.BindingFlags.DEFAULT); + + delete_button.clicked.connect (delete_marker); + } + + public void delete_marker () { + popover.popdown (); + deletion_requested (markerdata.id); + } +} diff --git a/src/meson.build b/src/meson.build index 61815a51..b82c252b 100644 --- a/src/meson.build +++ b/src/meson.build @@ -20,6 +20,10 @@ dependencies = [ dependency('libadwaita-1'), dependency('libgeoclue-2.0'), dependency('shumate-1.0'), + # Required by MarkerController + dependency('gee-0.8'), + # Required by Storage + dependency('json-glib-1.0'), ] sources = files( @@ -29,6 +33,10 @@ sources = files( 'MapWidget.vala', 'SearchResultItem.vala', 'Util.vala', + 'Objects' / 'MarkerData.vala', + 'Services' / 'MarkerManager.vala', + 'Services' / 'Storage.vala', + 'Widgets' / 'Marker.vala' ) executable( From f21975390315c626de0a3a97b409292d28cc472f Mon Sep 17 00:00:00 2001 From: teamcons Date: Thu, 22 Oct 2026 22:04:50 +0200 Subject: [PATCH 02/19] Make the linter happy, ensure popover on top --- src/Services/MarkerManager.vala | 6 +++--- src/Services/Storage.vala | 2 +- src/Widgets/Marker.vala | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Services/MarkerManager.vala b/src/Services/MarkerManager.vala index 6e8bd5a8..dce12912 100644 --- a/src/Services/MarkerManager.vala +++ b/src/Services/MarkerManager.vala @@ -58,15 +58,15 @@ public class Maps.MarkerManager : Object { var json_array = new Json.Array (); foreach (var marker in marker_list) { - + // Deleted markers are set to null if (marker == null) { continue; - } + } json_array.add_object_element (marker.markerdata.to_json ()); } storage.save (json_array); } -} \ No newline at end of file +} diff --git a/src/Services/Storage.vala b/src/Services/Storage.vala index 8768d296..375626f3 100644 --- a/src/Services/Storage.vala +++ b/src/Services/Storage.vala @@ -81,4 +81,4 @@ public class Maps.Storage : Object { debug ("Retrieved %ui elements", array.get_length ()); return array; } -} \ No newline at end of file +} diff --git a/src/Widgets/Marker.vala b/src/Widgets/Marker.vala index 982b3aec..0bd006ad 100644 --- a/src/Widgets/Marker.vala +++ b/src/Widgets/Marker.vala @@ -75,8 +75,7 @@ public class Maps.Marker : Shumate.Marker { box.append (delete_button); popover = new Gtk.Popover () { - child = box, - position = Gtk.PositionType.TOP + child = box }; var menubutton = new Gtk.MenuButton () { @@ -85,6 +84,7 @@ public class Maps.Marker : Shumate.Marker { tooltip_text = markerdata.name }; + popover.position = Gtk.PositionType.TOP; child = menubutton; name_entry.bind_property ("text", From dcfc57b8fdf32d8932e452aaefcf188c1e0f717b Mon Sep 17 00:00:00 2001 From: teamcons Date: Thu, 22 Oct 2026 22:29:10 +0200 Subject: [PATCH 03/19] Test out storage and clean up behind us --- src/MapWidget.vala | 8 +++++--- src/Services/MarkerManager.vala | 16 ++++++++++------ src/Services/Storage.vala | 6 ++---- src/Widgets/Marker.vala | 18 +++++++----------- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/MapWidget.vala b/src/MapWidget.vala index ea1dd783..0e35e1c1 100644 --- a/src/MapWidget.vala +++ b/src/MapWidget.vala @@ -57,11 +57,13 @@ public class Maps.MapWidget : Gtk.Box { var markerdata = new MarkerData () { latitude = 48.858400, longitude = 2.294500, - name = "Eiffel Tower", - notes = "Oooh" + name = "Eiffel Tower!", + notes = "Elly was here! The place was very crowded!" }; - markermanager.add_marker (markerdata); + // markermanager.add_marker (markerdata); + // markermanager.save_all (); + markermanager.populate_layer.begin (); } // Set the initial location of the map widget. diff --git a/src/Services/MarkerManager.vala b/src/Services/MarkerManager.vala index dce12912..741b583e 100644 --- a/src/Services/MarkerManager.vala +++ b/src/Services/MarkerManager.vala @@ -17,6 +17,7 @@ public class Maps.MarkerManager : Object { construct { storage = new Storage (); + marker_list = new Gee.ArrayList (); } public async void populate_layer () { @@ -36,25 +37,28 @@ public class Maps.MarkerManager : Object { marker_list.add (marker); markerlayer.add_marker (marker); + marker.changed.connect (save_all); marker.deletion_requested.connect (remove_marker); } public void remove_marker (int marker_id) { - - // 0 is our current location - if (marker_id == 0) { - return; - } + print ("Deletion requested!"); // Each MarkerData has an unique ID that mirrors their position in the list var marker = marker_list[marker_id]; + + marker.changed.disconnect (save_all); + + marker.deletion_requested.disconnect (remove_marker); marker.destroy (); marker_list[marker_id] = null; save_all (); } - private void save_all () { + public void save_all () { + print ("Saving!"); + var json_array = new Json.Array (); foreach (var marker in marker_list) { diff --git a/src/Services/Storage.vala b/src/Services/Storage.vala index 375626f3..7fe4eb18 100644 --- a/src/Services/Storage.vala +++ b/src/Services/Storage.vala @@ -17,9 +17,9 @@ public class Maps.Storage : Object { ensure_datadir (); storage_file_path = GLib.Path.build_path (Path.DIR_SEPARATOR_S, path_data, STORAGE_FILENAME); + print ("\nLocations are stored at %s", storage_file_path); } - /*************************************************/ /** * Persistently check for the data directory and create if there is none */ @@ -39,7 +39,6 @@ public class Maps.Storage : Object { } } - /*************************************************/ /** * Converts a Json.Node into a string and take care of saving it */ @@ -55,11 +54,10 @@ public class Maps.Storage : Object { generator.to_file (storage_file_path); } catch (Error e) { - warning ("Failed to save points of interest %s", e.message); + warning ("Failed to save locations: %s", e.message); } } - /*************************************************/ /** * Grab from storage, into a Json.Node we can parse. Insist if necessary */ diff --git a/src/Widgets/Marker.vala b/src/Widgets/Marker.vala index 0bd006ad..355492f0 100644 --- a/src/Widgets/Marker.vala +++ b/src/Widgets/Marker.vala @@ -10,6 +10,7 @@ public class Maps.Marker : Shumate.Marker { public MarkerData markerdata {get; construct;} private Gtk.Popover popover; + public signal void changed (); public signal void deletion_requested (int markerdata_id); public Marker (MarkerData markerdata) { @@ -32,10 +33,6 @@ public class Maps.Marker : Shumate.Marker { text = markerdata.name }; - var name_label = new Granite.HeaderLabel (_("Name:")) { - mnemonic_widget = name_entry - }; - var comment_textview = new Granite.HyperTextView () { accepts_tab = false, wrap_mode = WORD_CHAR, @@ -47,13 +44,9 @@ public class Maps.Marker : Shumate.Marker { comment_textview.buffer.text = markerdata.notes; - var comment_label = new Granite.HeaderLabel (_("Comments:")) { - mnemonic_widget = comment_textview - }; - var scrolled = new Gtk.ScrolledWindow () { child = comment_textview, - height_request = 100, + height_request = 80, hexpand = true, vexpand = true, hscrollbar_policy = NEVER @@ -68,9 +61,7 @@ public class Maps.Marker : Shumate.Marker { }; delete_button.add_css_class (Granite.CssClass.DESTRUCTIVE); - //box.append (name_label); box.append (name_entry); - //box.append (comment_label); box.append (frame); box.append (delete_button); @@ -100,6 +91,11 @@ public class Maps.Marker : Shumate.Marker { GLib.BindingFlags.DEFAULT); delete_button.clicked.connect (delete_marker); + popover.closed.connect (on_popover_closed); + } + + public void on_popover_closed () { + changed (); } public void delete_marker () { From de9e5a0cf417a0722ae7c71e8d0cd79b11d44113 Mon Sep 17 00:00:00 2001 From: teamcons Date: Thu, 22 Oct 2026 22:35:26 +0200 Subject: [PATCH 04/19] Add bespoke pin icon, and use trash-symbolic instead of a label --- src/MainWindow.vala | 7 +++++++ src/Widgets/Marker.vala | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/MainWindow.vala b/src/MainWindow.vala index c450261b..55e26fe1 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -26,6 +26,7 @@ public class Maps.MainWindow : Adw.ApplicationWindow { private Cancellable? search_cancellable = null; private Gtk.Button current_location; + private Gtk.Button pin_location; private Gtk.Spinner spinner; private Gtk.SearchEntry search_entry; private Gtk.Popover search_res_popover; @@ -42,6 +43,11 @@ public class Maps.MainWindow : Adw.ApplicationWindow { valign = CENTER }; + pin_location = new Gtk.Button.from_icon_name ("view-pin-symbolic") { + tooltip_text = _("Pin a location"), + valign = CENTER + }; + spinner = new Gtk.Spinner (); search_entry = new Gtk.SearchEntry () { @@ -160,6 +166,7 @@ public class Maps.MainWindow : Adw.ApplicationWindow { title_widget = search_clamp }; headerbar.pack_start (current_location); + headerbar.pack_start (pin_location); headerbar.pack_end (menu_button); headerbar.pack_end (spinner); diff --git a/src/Widgets/Marker.vala b/src/Widgets/Marker.vala index 355492f0..6a51d028 100644 --- a/src/Widgets/Marker.vala +++ b/src/Widgets/Marker.vala @@ -56,7 +56,8 @@ public class Maps.Marker : Shumate.Marker { child = scrolled }; - var delete_button = new Gtk.Button.with_label (_("Delete…")) { + var delete_button = new Gtk.Button.from_icon_name ("user-trash-symbolic") { + tooltip_text = _("Delete this location"), halign = Gtk.Align.START, }; delete_button.add_css_class (Granite.CssClass.DESTRUCTIVE); From 272171a96c69483a0e79bbf299fd2d2436058929 Mon Sep 17 00:00:00 2001 From: teamcons Date: Fri, 30 Oct 2026 02:11:53 +0100 Subject: [PATCH 05/19] map_widget, mainwindow: Functional pin mode --- src/MainWindow.vala | 49 +++++++++++++++++++++++++++++++ src/MapWidget.vala | 51 ++++++++++++++++++++++++--------- src/Objects/MarkerData.vala | 12 ++------ src/Services/MarkerManager.vala | 38 +++++++++--------------- src/Widgets/Marker.vala | 19 +++++++++--- 5 files changed, 116 insertions(+), 53 deletions(-) diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 55e26fe1..cf36c10d 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -20,6 +20,7 @@ public class Maps.MainWindow : Adw.ApplicationWindow { private const ActionEntry[] ACTION_ENTRIES = { { "search", on_search_activate }, + { "pin", on_pin_activate }, }; private int current_busy_reason = 0; private ListStore location_store; @@ -32,10 +33,18 @@ public class Maps.MainWindow : Adw.ApplicationWindow { private Gtk.Popover search_res_popover; private MapWidget map_widget; + private Gtk.GestureClick gesture_click; + private Gtk.EventControllerKey key_controller; + construct { title = _("Maps"); add_action_entries (ACTION_ENTRIES, this); + gesture_click = new Gtk.GestureClick (); + key_controller = new Gtk.EventControllerKey (); + ((Gtk.Widget)this).add_controller (gesture_click); + ((Gtk.Widget)this).add_controller (key_controller); + location_store = new ListStore (typeof (Geocode.Place)); current_location = new Gtk.Button.from_icon_name ("find-location-symbolic") { @@ -45,6 +54,7 @@ public class Maps.MainWindow : Adw.ApplicationWindow { pin_location = new Gtk.Button.from_icon_name ("view-pin-symbolic") { tooltip_text = _("Pin a location"), + action_name = "win.pin", valign = CENTER }; @@ -372,4 +382,43 @@ public class Maps.MainWindow : Adw.ApplicationWindow { var search_result_item = (SearchResultItem) list_item.child; search_result_item.place = (Geocode.Place) list_item.item; } + + private void on_pin_activate () { + pin_location.sensitive = false; + + set_cursor_from_name ("crosshair"); + map_widget.set_cursor_from_name ("crosshair"); + + gesture_click.released.connect (on_click); + key_controller.key_released.connect (on_keypress); + map_widget.leaving_pin_mode.connect (on_exit_pin_mode); + + map_widget.enter_pin_mode (); + } + + private void on_exit_pin_mode () { + set_cursor_from_name ("default"); + pin_location.sensitive = true; + + gesture_click.released.disconnect (on_click); + key_controller.key_released.disconnect (on_keypress); + map_widget.leaving_pin_mode.disconnect (on_exit_pin_mode); + } + + private void on_click (int n_press, double x, double y) { + debug ("%.2f, %.2f", x, y); + map_widget.on_exit_pin_mode (); + on_exit_pin_mode (); + return; + } + + private void on_keypress (uint keyval, uint keycode, Gdk.ModifierType state) { + + // We only exit on Escape, as people may need to use other keys to navigate on the map from the keyboard + if (keyval == Gdk.Key.Escape) { + map_widget.on_exit_pin_mode (); + on_exit_pin_mode (); + } + + } } diff --git a/src/MapWidget.vala b/src/MapWidget.vala index 0e35e1c1..a9deedff 100644 --- a/src/MapWidget.vala +++ b/src/MapWidget.vala @@ -22,11 +22,15 @@ public class Maps.MapWidget : Gtk.Box { private Shumate.MarkerLayer pin_layer; // Displays the position of current location private Shumate.MarkerLayer location_layer; - private GClue.Location? location = null; + private Gtk.GestureClick gesture_click; + + private MarkerManager marker_manager; private bool is_watching_location = false; + public signal void leaving_pin_mode (); + // The Royal Observatory private const double DEFAULT_LATITUDE = 51.2840; private const double DEFAULT_LONGITUDE = 0.0005; @@ -40,6 +44,9 @@ public class Maps.MapWidget : Gtk.Box { hexpand = true }; + gesture_click = new Gtk.GestureClick (); + map_widget.add_controller (gesture_click); + append (map_widget); base_map = map_widget.map; } @@ -51,19 +58,8 @@ public class Maps.MapWidget : Gtk.Box { location_layer = new Shumate.MarkerLayer (map_widget.viewport); map_widget.add_overlay_layer (location_layer); - - var markermanager = new MarkerManager (pin_layer); - - var markerdata = new MarkerData () { - latitude = 48.858400, - longitude = 2.294500, - name = "Eiffel Tower!", - notes = "Elly was here! The place was very crowded!" - }; - - // markermanager.add_marker (markerdata); - // markermanager.save_all (); - markermanager.populate_layer.begin (); + marker_manager = new MarkerManager (pin_layer); + marker_manager.populate_layer.begin (); } // Set the initial location of the map widget. @@ -205,4 +201,31 @@ public class Maps.MapWidget : Gtk.Box { pin_layer.add_marker (marker); } + + public void enter_pin_mode () { + map_widget.map.set_cursor_from_name ("crosshair"); + gesture_click.released.connect (on_click); + } + + public void on_exit_pin_mode () { + map_widget.map.set_cursor_from_name ("default"); + gesture_click.released.disconnect (on_click); + leaving_pin_mode (); + } + + private void on_click (int n_press, double x, double y) { + double latitude, longitude; + map_widget.viewport.widget_coords_to_location (map_widget, x, y, out latitude, out longitude); + + var markerdata = new MarkerData () { + latitude = latitude, + longitude = longitude + }; + + // We exit BEFORE adding, else new marker popups grabs focus and we can get into a weird limbo + marker_manager.add_marker (markerdata); + on_exit_pin_mode (); + + return; + } } diff --git a/src/Objects/MarkerData.vala b/src/Objects/MarkerData.vala index 6717dd4d..69fc6abf 100644 --- a/src/Objects/MarkerData.vala +++ b/src/Objects/MarkerData.vala @@ -7,18 +7,10 @@ public class Maps.MarkerData : Object { - private static int last_id = 0; - public double latitude {get; set;} public double longitude {get; set;} - public string name {get; set;} - public string notes {get; set;} - public int id {get; private set;} - - public MarkerData () { - id = last_id; - last_id++; - } + public string name {get; set; default = _("New location");} + public string notes {get; set; default = "";} /** * Used to recreate individual points of interest from storage data diff --git a/src/Services/MarkerManager.vala b/src/Services/MarkerManager.vala index 741b583e..ac604cdd 100644 --- a/src/Services/MarkerManager.vala +++ b/src/Services/MarkerManager.vala @@ -6,10 +6,11 @@ */ public class Maps.MarkerManager : Object { + private Storage storage; - private Gee.ArrayList marker_list; private unowned Shumate.MarkerLayer markerlayer; + private static bool popup_new_markers = false; public MarkerManager (Shumate.MarkerLayer markerlayer) { this.markerlayer = markerlayer; @@ -17,7 +18,6 @@ public class Maps.MarkerManager : Object { construct { storage = new Storage (); - marker_list = new Gee.ArrayList (); } public async void populate_layer () { @@ -25,50 +25,38 @@ public class Maps.MarkerManager : Object { foreach (var markerdata_note in markerdata_array.get_elements ()) { var markerdata = new MarkerData.from_json (markerdata_note.get_object ()); - add_marker (markerdata); } + popup_new_markers = true; save_all (); } public void add_marker (MarkerData markerdata) { var marker = new Marker (markerdata); - marker_list.add (marker); markerlayer.add_marker (marker); + if (popup_new_markers) { + marker.popup (); + } + marker.changed.connect (save_all); marker.deletion_requested.connect (remove_marker); } - public void remove_marker (int marker_id) { - print ("Deletion requested!"); - - // Each MarkerData has an unique ID that mirrors their position in the list - var marker = marker_list[marker_id]; - - marker.changed.disconnect (save_all); - - marker.deletion_requested.disconnect (remove_marker); - marker.destroy (); - marker_list[marker_id] = null; - + public void remove_marker (Shumate.Marker marker) { + markerlayer.remove_marker (marker); save_all (); } public void save_all () { - print ("Saving!"); + debug ("\nSaving"); var json_array = new Json.Array (); - foreach (var marker in marker_list) { - - // Deleted markers are set to null - if (marker == null) { - continue; - } - - json_array.add_object_element (marker.markerdata.to_json ()); + foreach (var marker in markerlayer.get_markers ()) { + var markerdata = ((Maps.Marker)marker).markerdata; + json_array.add_object_element (markerdata.to_json ()); } storage.save (json_array); diff --git a/src/Widgets/Marker.vala b/src/Widgets/Marker.vala index 6a51d028..407f1c9e 100644 --- a/src/Widgets/Marker.vala +++ b/src/Widgets/Marker.vala @@ -11,7 +11,7 @@ public class Maps.Marker : Shumate.Marker { private Gtk.Popover popover; public signal void changed (); - public signal void deletion_requested (int markerdata_id); + public signal void deletion_requested (Shumate.Marker marker); public Marker (MarkerData markerdata) { Object ( @@ -30,7 +30,8 @@ public class Maps.Marker : Shumate.Marker { }; var name_entry = new Gtk.Entry () { - text = markerdata.name + text = markerdata.name, + placeholder_text = _("Name of the location") }; var comment_textview = new Granite.HyperTextView () { @@ -70,9 +71,15 @@ public class Maps.Marker : Shumate.Marker { child = box }; + var image = new Gtk.Image.from_icon_name ("pointer") { + icon_size = Gtk.IconSize.LARGE + }; + var menubutton = new Gtk.MenuButton () { popover = popover, - icon_name = "mail-unread", + icon_name = "pointer", + child = image, + has_frame = false, tooltip_text = markerdata.name }; @@ -101,6 +108,10 @@ public class Maps.Marker : Shumate.Marker { public void delete_marker () { popover.popdown (); - deletion_requested (markerdata.id); + deletion_requested (this); + } + + public void popup () { + popover.popup (); } } From 74088a8e980b77f1291ed11cac9d15ffa4ad2196 Mon Sep 17 00:00:00 2001 From: teamcons Date: Fri, 30 Oct 2026 02:12:27 +0100 Subject: [PATCH 06/19] CSS: Hide the menubutton --- data/Application.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/data/Application.css b/data/Application.css index 48e10c9a..dce1ad4b 100644 --- a/data/Application.css +++ b/data/Application.css @@ -108,3 +108,9 @@ search-result-item.land-feature image { background-color: @LIME_500; -gtk-icon-shadow: 0 1px 3px @LIME_700; } + +/* Avoid styling the button frame we use for custom locations */ +map-marker menubutton button.toggle { + background: none; + box-shadow: none; +} \ No newline at end of file From e24f32add839ccee7ad8db0e8f87e2e98d7770ad Mon Sep 17 00:00:00 2001 From: teamcons Date: Fri, 30 Oct 2026 02:27:12 +0100 Subject: [PATCH 07/19] MarkerManager: Add a debouncer --- src/Services/MarkerManager.vala | 20 +++++++++++++++++--- src/Widgets/Marker.vala | 7 ++++--- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/Services/MarkerManager.vala b/src/Services/MarkerManager.vala index ac604cdd..1c0f8360 100644 --- a/src/Services/MarkerManager.vala +++ b/src/Services/MarkerManager.vala @@ -6,11 +6,13 @@ */ public class Maps.MarkerManager : Object { + + private static bool popup_new_markers = false; private Storage storage; private unowned Shumate.MarkerLayer markerlayer; - private static bool popup_new_markers = false; + private static uint debounce_timer_id; public MarkerManager (Shumate.MarkerLayer markerlayer) { this.markerlayer = markerlayer; @@ -29,7 +31,6 @@ public class Maps.MarkerManager : Object { } popup_new_markers = true; - save_all (); } public void add_marker (MarkerData markerdata) { @@ -50,8 +51,21 @@ public class Maps.MarkerManager : Object { } public void save_all () { - debug ("\nSaving"); + if (debounce_timer_id != 0) { + GLib.Source.remove (debounce_timer_id); + } + + debounce_timer_id = Timeout.add (750, debounce_handler); + } + + public bool debounce_handler () { + debounce_timer_id = 0; + immediately_save (); + return GLib.Source.REMOVE; + } + public void immediately_save () { + print ("\nSaving"); var json_array = new Json.Array (); foreach (var marker in markerlayer.get_markers ()) { diff --git a/src/Widgets/Marker.vala b/src/Widgets/Marker.vala index 407f1c9e..9ba94a91 100644 --- a/src/Widgets/Marker.vala +++ b/src/Widgets/Marker.vala @@ -47,7 +47,7 @@ public class Maps.Marker : Shumate.Marker { var scrolled = new Gtk.ScrolledWindow () { child = comment_textview, - height_request = 80, + height_request = 90, hexpand = true, vexpand = true, hscrollbar_policy = NEVER @@ -99,10 +99,11 @@ public class Maps.Marker : Shumate.Marker { GLib.BindingFlags.DEFAULT); delete_button.clicked.connect (delete_marker); - popover.closed.connect (on_popover_closed); + name_entry.changed.connect (on_changed); + comment_textview.buffer.changed.connect (on_changed); } - public void on_popover_closed () { + public void on_changed () { changed (); } From bbee6bf66c5a8e9071036963f1372903e5a042a7 Mon Sep 17 00:00:00 2001 From: teamcons Date: Fri, 30 Oct 2026 02:53:27 +0100 Subject: [PATCH 08/19] MainWindow: Use a ToggleButton --- src/MainWindow.vala | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/MainWindow.vala b/src/MainWindow.vala index cf36c10d..c2ebce0f 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -27,7 +27,7 @@ public class Maps.MainWindow : Adw.ApplicationWindow { private Cancellable? search_cancellable = null; private Gtk.Button current_location; - private Gtk.Button pin_location; + private Gtk.ToggleButton pin_location; private Gtk.Spinner spinner; private Gtk.SearchEntry search_entry; private Gtk.Popover search_res_popover; @@ -52,8 +52,9 @@ public class Maps.MainWindow : Adw.ApplicationWindow { valign = CENTER }; - pin_location = new Gtk.Button.from_icon_name ("view-pin-symbolic") { + pin_location = new Gtk.ToggleButton () { tooltip_text = _("Pin a location"), + icon_name = "view-pin-symbolic", action_name = "win.pin", valign = CENTER }; @@ -383,8 +384,10 @@ public class Maps.MainWindow : Adw.ApplicationWindow { search_result_item.place = (Geocode.Place) list_item.item; } + // We handle a pin mode at the level of the window, so we can disable the pin button, + // or cancel if the user press escape, or clicks anywhere but the map private void on_pin_activate () { - pin_location.sensitive = false; + pin_location.active = true; set_cursor_from_name ("crosshair"); map_widget.set_cursor_from_name ("crosshair"); @@ -398,13 +401,14 @@ public class Maps.MainWindow : Adw.ApplicationWindow { private void on_exit_pin_mode () { set_cursor_from_name ("default"); - pin_location.sensitive = true; + pin_location.active = false; gesture_click.released.disconnect (on_click); key_controller.key_released.disconnect (on_keypress); map_widget.leaving_pin_mode.disconnect (on_exit_pin_mode); } + // If this handler fires, then the user did not click on the map private void on_click (int n_press, double x, double y) { debug ("%.2f, %.2f", x, y); map_widget.on_exit_pin_mode (); @@ -412,13 +416,11 @@ public class Maps.MainWindow : Adw.ApplicationWindow { return; } + // We only exit on Escape, as people may need to use other keys to navigate on the map from the keyboard private void on_keypress (uint keyval, uint keycode, Gdk.ModifierType state) { - - // We only exit on Escape, as people may need to use other keys to navigate on the map from the keyboard if (keyval == Gdk.Key.Escape) { map_widget.on_exit_pin_mode (); on_exit_pin_mode (); } - } } From b877adb755016000d115fd8186219bf7cc2fb93c Mon Sep 17 00:00:00 2001 From: teamcons Date: Fri, 30 Oct 2026 02:54:06 +0100 Subject: [PATCH 09/19] Document for ease of maintenance --- src/Objects/MarkerData.vala | 14 +++++++------- src/Services/MarkerManager.vala | 14 ++++++++++---- src/Services/Storage.vala | 11 +++++++---- src/Widgets/Marker.vala | 7 +++++++ src/meson.build | 2 -- 5 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/Objects/MarkerData.vala b/src/Objects/MarkerData.vala index 69fc6abf..74ef876c 100644 --- a/src/Objects/MarkerData.vala +++ b/src/Objects/MarkerData.vala @@ -5,6 +5,13 @@ * 2014-2015 Atlas Developers */ +/** + * MarkerData represents the informations for a specific location + * + * It is used by {@link Maps.MarkerManager} to save, load, and represent locations on a map. + * + * It can be converted from or into a {@link Json.Object} for convenience + */ public class Maps.MarkerData : Object { public double latitude {get; set;} @@ -12,9 +19,6 @@ public class Maps.MarkerData : Object { public string name {get; set; default = _("New location");} public string notes {get; set; default = "";} - /** - * Used to recreate individual points of interest from storage data - */ public MarkerData.from_json (Json.Object node) { latitude = node.get_double_member ("lat"); longitude = node.get_double_member ("long"); @@ -22,10 +26,6 @@ public class Maps.MarkerData : Object { notes = node.get_string_member ("notes"); } - /*************************************************/ - /** - * Used to convert points of interest into objects we can store - */ public Json.Object to_json () { var builder = new Json.Builder (); diff --git a/src/Services/MarkerManager.vala b/src/Services/MarkerManager.vala index 1c0f8360..c9b871b5 100644 --- a/src/Services/MarkerManager.vala +++ b/src/Services/MarkerManager.vala @@ -5,6 +5,12 @@ * 2014-2015 Atlas Developers */ +/** + * MarkerManager manages locations on a {@link Shumate.MarkerLayer} + * + * It loads {@link Maps.MarkerData} from {@link Maps.Storage}, from which it creates {@link Maps.Marker} widgets and adds them to the layer + * + */ public class Maps.MarkerManager : Object { private static bool popup_new_markers = false; @@ -41,16 +47,16 @@ public class Maps.MarkerManager : Object { marker.popup (); } - marker.changed.connect (save_all); + marker.changed.connect (save_debounce); marker.deletion_requested.connect (remove_marker); } public void remove_marker (Shumate.Marker marker) { markerlayer.remove_marker (marker); - save_all (); + save_debounce (); } - public void save_all () { + public void save_debounce () { if (debounce_timer_id != 0) { GLib.Source.remove (debounce_timer_id); } @@ -65,7 +71,7 @@ public class Maps.MarkerManager : Object { } public void immediately_save () { - print ("\nSaving"); + debug ("Saving"); var json_array = new Json.Array (); foreach (var marker in markerlayer.get_markers ()) { diff --git a/src/Services/Storage.vala b/src/Services/Storage.vala index 7fe4eb18..be700ffa 100644 --- a/src/Services/Storage.vala +++ b/src/Services/Storage.vala @@ -5,6 +5,9 @@ * 2014-2015 Atlas Developers */ +/** + * A wrapper for disk operations for the custom locations from {@link Maps.MarkerManager} + */ public class Maps.Storage : Object { private const string STORAGE_FILENAME = "locations.json"; @@ -40,8 +43,8 @@ public class Maps.Storage : Object { } /** - * Converts a Json.Node into a string and take care of saving it - */ + * Converts a Json.Node into a string and take care of saving it + */ public void save (Json.Array json_data) { ensure_datadir (); debug ("Writing %u elements (Should be same number as points of interest)", json_data.get_length ()); @@ -59,8 +62,8 @@ public class Maps.Storage : Object { } /** - * Grab from storage, into a Json.Node we can parse. Insist if necessary - */ + * Grab from storage, into a Json.Node we can parse. Insist if necessary + */ public Json.Array load () { var parser = new Json.Parser (); diff --git a/src/Widgets/Marker.vala b/src/Widgets/Marker.vala index 9ba94a91..c71ba88e 100644 --- a/src/Widgets/Marker.vala +++ b/src/Widgets/Marker.vala @@ -5,6 +5,13 @@ * 2014-2015 Atlas Developers */ +/** + * Marker is a subclass of {@link Shumate.Marker} built from a {@link Maps.MarkerData} + * + * It uses as child a MenuButton, whose popover displays the MarkerData informations. + * + * The pointer is an image set as child of the MenuButton + */ public class Maps.Marker : Shumate.Marker { public MarkerData markerdata {get; construct;} diff --git a/src/meson.build b/src/meson.build index b82c252b..3b4264a5 100644 --- a/src/meson.build +++ b/src/meson.build @@ -20,8 +20,6 @@ dependencies = [ dependency('libadwaita-1'), dependency('libgeoclue-2.0'), dependency('shumate-1.0'), - # Required by MarkerController - dependency('gee-0.8'), # Required by Storage dependency('json-glib-1.0'), ] From ef557ac8a057922408c5410fe14233d65a0aab5a Mon Sep 17 00:00:00 2001 From: teamcons Date: Fri, 30 Oct 2026 02:55:41 +0100 Subject: [PATCH 10/19] Make the linter happy --- src/Services/MarkerManager.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Services/MarkerManager.vala b/src/Services/MarkerManager.vala index c9b871b5..7bb21a60 100644 --- a/src/Services/MarkerManager.vala +++ b/src/Services/MarkerManager.vala @@ -12,7 +12,7 @@ * */ public class Maps.MarkerManager : Object { - + private static bool popup_new_markers = false; private Storage storage; From c02fb9ae4f5a90076552a0d8a7ccff1cfbe5ee57 Mon Sep 17 00:00:00 2001 From: teamcons Date: Fri, 30 Oct 2026 03:52:14 +0100 Subject: [PATCH 11/19] Add missing to POTFILES and widen the popup --- po/POTFILES | 2 ++ src/Services/Storage.vala | 2 +- src/Widgets/Marker.vala | 12 ++++++------ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/po/POTFILES b/po/POTFILES index 2379a5f2..c872f0c7 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -1,4 +1,6 @@ src/Application.vala src/MainWindow.vala src/MapWidget.vala +src/Objects/MarkerData.vala src/SearchResultItem.vala +src/Widgets/Marker.vala \ No newline at end of file diff --git a/src/Services/Storage.vala b/src/Services/Storage.vala index be700ffa..e1431b46 100644 --- a/src/Services/Storage.vala +++ b/src/Services/Storage.vala @@ -47,7 +47,7 @@ public class Maps.Storage : Object { */ public void save (Json.Array json_data) { ensure_datadir (); - debug ("Writing %u elements (Should be same number as points of interest)", json_data.get_length ()); + debug ("Writing %u elements (Should be same number as locations)", json_data.get_length ()); try { var generator = new Json.Generator (); diff --git a/src/Widgets/Marker.vala b/src/Widgets/Marker.vala index c71ba88e..48ffd9ff 100644 --- a/src/Widgets/Marker.vala +++ b/src/Widgets/Marker.vala @@ -33,7 +33,8 @@ public class Maps.Marker : Shumate.Marker { margin_top = 10, margin_bottom = 10, margin_start = 10, - margin_end = 10 + margin_end = 10, + width_request = 210 }; var name_entry = new Gtk.Entry () { @@ -49,19 +50,18 @@ public class Maps.Marker : Shumate.Marker { bottom_margin = 3, left_margin = 3, }; - comment_textview.buffer.text = markerdata.notes; var scrolled = new Gtk.ScrolledWindow () { child = comment_textview, - height_request = 90, - hexpand = true, - vexpand = true, hscrollbar_policy = NEVER }; var frame = new Gtk.Frame (null) { - child = scrolled + child = scrolled, + height_request = 110, + hexpand = true, + vexpand = true }; var delete_button = new Gtk.Button.from_icon_name ("user-trash-symbolic") { From 9970c954c217944769b17823b8346399e0785e9a Mon Sep 17 00:00:00 2001 From: teamcons Date: Sun, 1 Nov 2026 16:15:16 +0100 Subject: [PATCH 12/19] MapWidget: Use a dedicated layer --- src/MapWidget.vala | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/MapWidget.vala b/src/MapWidget.vala index a9deedff..a4b18fa6 100644 --- a/src/MapWidget.vala +++ b/src/MapWidget.vala @@ -22,6 +22,8 @@ public class Maps.MapWidget : Gtk.Box { private Shumate.MarkerLayer pin_layer; // Displays the position of current location private Shumate.MarkerLayer location_layer; + // Displays the position of user-pinned layers + private Shumate.MarkerLayer user_layer; private GClue.Location? location = null; private Gtk.GestureClick gesture_click; @@ -58,7 +60,10 @@ public class Maps.MapWidget : Gtk.Box { location_layer = new Shumate.MarkerLayer (map_widget.viewport); map_widget.add_overlay_layer (location_layer); - marker_manager = new MarkerManager (pin_layer); + user_layer = new Shumate.MarkerLayer (map_widget.viewport); + map_widget.add_overlay_layer (user_layer); + + marker_manager = new MarkerManager (user_layer); marker_manager.populate_layer.begin (); } @@ -222,9 +227,8 @@ public class Maps.MapWidget : Gtk.Box { longitude = longitude }; - // We exit BEFORE adding, else new marker popups grabs focus and we can get into a weird limbo - marker_manager.add_marker (markerdata); on_exit_pin_mode (); + marker_manager.add_marker (markerdata); return; } From 305df1c9954d6ac03bc31e12cfe91f794b34fb0b Mon Sep 17 00:00:00 2001 From: teamcons Date: Sun, 1 Nov 2026 18:07:02 +0100 Subject: [PATCH 13/19] Marker: Do not keep a MarkerData (redundant) --- src/MapWidget.vala | 11 ++--- src/Objects/MarkerData.vala | 15 +++++-- src/Services/MarkerManager.vala | 26 +++++++---- src/Services/Storage.vala | 2 + src/Widgets/Marker.vala | 76 ++++++++++++++++++++------------- 5 files changed, 79 insertions(+), 51 deletions(-) diff --git a/src/MapWidget.vala b/src/MapWidget.vala index a4b18fa6..fb46d822 100644 --- a/src/MapWidget.vala +++ b/src/MapWidget.vala @@ -219,16 +219,11 @@ public class Maps.MapWidget : Gtk.Box { } private void on_click (int n_press, double x, double y) { + on_exit_pin_mode (); + double latitude, longitude; map_widget.viewport.widget_coords_to_location (map_widget, x, y, out latitude, out longitude); - - var markerdata = new MarkerData () { - latitude = latitude, - longitude = longitude - }; - - on_exit_pin_mode (); - marker_manager.add_marker (markerdata); + marker_manager.add_marker (new MarkerData (latitude, longitude)); return; } diff --git a/src/Objects/MarkerData.vala b/src/Objects/MarkerData.vala index 74ef876c..a31190d0 100644 --- a/src/Objects/MarkerData.vala +++ b/src/Objects/MarkerData.vala @@ -17,13 +17,20 @@ public class Maps.MarkerData : Object { public double latitude {get; set;} public double longitude {get; set;} public string name {get; set; default = _("New location");} - public string notes {get; set; default = "";} + public string comments {get; set; default = "";} + + public MarkerData (double latitude, double longitude) { + Object ( + latitude: latitude, + longitude: longitude + ); + } public MarkerData.from_json (Json.Object node) { latitude = node.get_double_member ("lat"); longitude = node.get_double_member ("long"); name = node.get_string_member ("name"); - notes = node.get_string_member ("notes"); + comments = node.get_string_member ("comments"); } public Json.Object to_json () { @@ -36,8 +43,8 @@ public class Maps.MarkerData : Object { builder.add_double_value (longitude); builder.set_member_name ("name"); builder.add_string_value (name); - builder.set_member_name ("notes"); - builder.add_string_value (notes); + builder.set_member_name ("comments"); + builder.add_string_value (comments); builder.end_object (); return builder.get_root ().get_object (); diff --git a/src/Services/MarkerManager.vala b/src/Services/MarkerManager.vala index 7bb21a60..d6b6622b 100644 --- a/src/Services/MarkerManager.vala +++ b/src/Services/MarkerManager.vala @@ -13,13 +13,13 @@ */ public class Maps.MarkerManager : Object { - private static bool popup_new_markers = false; + private static uint debounce_timer_id; + + public signal void pin_created (Marker marker); private Storage storage; private unowned Shumate.MarkerLayer markerlayer; - private static uint debounce_timer_id; - public MarkerManager (Shumate.MarkerLayer markerlayer) { this.markerlayer = markerlayer; } @@ -36,19 +36,18 @@ public class Maps.MarkerManager : Object { add_marker (markerdata); } - popup_new_markers = true; + // Only care about new pins after loading everything + pin_created.connect (on_pin_created); } public void add_marker (MarkerData markerdata) { var marker = new Marker (markerdata); markerlayer.add_marker (marker); - if (popup_new_markers) { - marker.popup (); - } - marker.changed.connect (save_debounce); marker.deletion_requested.connect (remove_marker); + + pin_created (marker); } public void remove_marker (Shumate.Marker marker) { @@ -75,10 +74,19 @@ public class Maps.MarkerManager : Object { var json_array = new Json.Array (); foreach (var marker in markerlayer.get_markers ()) { - var markerdata = ((Maps.Marker)marker).markerdata; + var markerdata = ((Maps.Marker)marker).to_markerdata (); json_array.add_object_element (markerdata.to_json ()); } storage.save (json_array); } + + private void on_pin_created (Marker marker) { + marker.popup (); + save_debounce (); + } + + ~MarkerManager () { + immediately_save (); + } } diff --git a/src/Services/Storage.vala b/src/Services/Storage.vala index e1431b46..13132042 100644 --- a/src/Services/Storage.vala +++ b/src/Services/Storage.vala @@ -7,6 +7,8 @@ /** * A wrapper for disk operations for the custom locations from {@link Maps.MarkerManager} + * + * It can save () and load () a {@link Json.Array} object containing {@link Maps.MarkerData} */ public class Maps.Storage : Object { diff --git a/src/Widgets/Marker.vala b/src/Widgets/Marker.vala index 48ffd9ff..1be1c94b 100644 --- a/src/Widgets/Marker.vala +++ b/src/Widgets/Marker.vala @@ -8,23 +8,45 @@ /** * Marker is a subclass of {@link Shumate.Marker} built from a {@link Maps.MarkerData} * - * It uses as child a MenuButton, whose popover displays the MarkerData informations. + * It displays a pointer on the map which when clicked shows a popover presenting the MarkerData informations. * - * The pointer is an image set as child of the MenuButton + * It is created by {@link Maps.MarkerManager} from the content of a {@link Maps.Storage} */ public class Maps.Marker : Shumate.Marker { - public MarkerData markerdata {get; construct;} - - private Gtk.Popover popover; public signal void changed (); public signal void deletion_requested (Shumate.Marker marker); + private Gtk.Popover popover; + private Gtk.Entry name_entry; + private Granite.HyperTextView comment_textview; + + public string loc_name { + owned get { + return name_entry.text.dup (); + } + set { + name_entry.text = value; + changed (); + } + } + + public string loc_comments { + owned get { + return comment_textview.buffer.text.dup (); + } + set { + comment_textview.buffer.text = value; + changed (); + } + } + public Marker (MarkerData markerdata) { Object ( - markerdata: markerdata, latitude: markerdata.latitude, - longitude: markerdata.longitude + longitude: markerdata.longitude, + loc_name: markerdata.name, + loc_comments: markerdata.comments ); } @@ -37,12 +59,11 @@ public class Maps.Marker : Shumate.Marker { width_request = 210 }; - var name_entry = new Gtk.Entry () { - text = markerdata.name, + name_entry = new Gtk.Entry () { placeholder_text = _("Name of the location") }; - var comment_textview = new Granite.HyperTextView () { + comment_textview = new Granite.HyperTextView () { accepts_tab = false, wrap_mode = WORD_CHAR, top_margin = 3, @@ -50,7 +71,6 @@ public class Maps.Marker : Shumate.Marker { bottom_margin = 3, left_margin = 3, }; - comment_textview.buffer.text = markerdata.notes; var scrolled = new Gtk.ScrolledWindow () { child = comment_textview, @@ -84,34 +104,17 @@ public class Maps.Marker : Shumate.Marker { var menubutton = new Gtk.MenuButton () { popover = popover, - icon_name = "pointer", child = image, has_frame = false, - tooltip_text = markerdata.name + tooltip_text = loc_name }; popover.position = Gtk.PositionType.TOP; child = menubutton; - name_entry.bind_property ("text", - markerdata, "name", - GLib.BindingFlags.DEFAULT); - - name_entry.bind_property ("text", - menubutton, "tooltip-text", - GLib.BindingFlags.DEFAULT); - - comment_textview.buffer.bind_property ("text", - markerdata, "notes", - GLib.BindingFlags.DEFAULT); - - delete_button.clicked.connect (delete_marker); name_entry.changed.connect (on_changed); comment_textview.buffer.changed.connect (on_changed); - } - - public void on_changed () { - changed (); + delete_button.clicked.connect (delete_marker); } public void delete_marker () { @@ -122,4 +125,17 @@ public class Maps.Marker : Shumate.Marker { public void popup () { popover.popup (); } + + // We use a dedicated handler as there may be a lot of markers + // To avoid the infamous vala lambda leak bug + public void on_changed () { + changed (); + } + + public MarkerData to_markerdata () { + return new MarkerData (latitude, longitude) { + name = loc_name, + comments = loc_comments + };; + } } From a28985b4de947c856f1a636dedbd7ee57da6efaf Mon Sep 17 00:00:00 2001 From: teamcons Date: Sun, 1 Nov 2026 19:07:41 +0100 Subject: [PATCH 14/19] Marker: Smarter grab behaviour on show --- src/Services/MarkerManager.vala | 1 + src/Widgets/Marker.vala | 68 +++++++++++++++++++++++---------- 2 files changed, 49 insertions(+), 20 deletions(-) diff --git a/src/Services/MarkerManager.vala b/src/Services/MarkerManager.vala index d6b6622b..89a158de 100644 --- a/src/Services/MarkerManager.vala +++ b/src/Services/MarkerManager.vala @@ -83,6 +83,7 @@ public class Maps.MarkerManager : Object { private void on_pin_created (Marker marker) { marker.popup (); + marker.grab_entry (); save_debounce (); } diff --git a/src/Widgets/Marker.vala b/src/Widgets/Marker.vala index 1be1c94b..7502b154 100644 --- a/src/Widgets/Marker.vala +++ b/src/Widgets/Marker.vala @@ -18,25 +18,25 @@ public class Maps.Marker : Shumate.Marker { public signal void deletion_requested (Shumate.Marker marker); private Gtk.Popover popover; - private Gtk.Entry name_entry; - private Granite.HyperTextView comment_textview; + private Gtk.Entry entry_name; + private Granite.HyperTextView textview_comments; public string loc_name { owned get { - return name_entry.text.dup (); + return entry_name.text.dup (); } set { - name_entry.text = value; + entry_name.text = value; changed (); } } public string loc_comments { owned get { - return comment_textview.buffer.text.dup (); + return textview_comments.buffer.text.dup (); } set { - comment_textview.buffer.text = value; + textview_comments.buffer.text = value; changed (); } } @@ -59,11 +59,15 @@ public class Maps.Marker : Shumate.Marker { width_request = 210 }; - name_entry = new Gtk.Entry () { - placeholder_text = _("Name of the location") + entry_name = new Gtk.Entry () { + show_emoji_icon = true }; - comment_textview = new Granite.HyperTextView () { + var box_name = new Granite.Box (VERTICAL, NONE); + box_name.append (new Granite.HeaderLabel (_("Location name"))); + box_name.append (entry_name); + + textview_comments = new Granite.HyperTextView () { accepts_tab = false, wrap_mode = WORD_CHAR, top_margin = 3, @@ -72,27 +76,35 @@ public class Maps.Marker : Shumate.Marker { left_margin = 3, }; - var scrolled = new Gtk.ScrolledWindow () { - child = comment_textview, + var scrolled_comments = new Gtk.ScrolledWindow () { + child = textview_comments, hscrollbar_policy = NEVER }; - var frame = new Gtk.Frame (null) { - child = scrolled, + var frame_comments = new Gtk.Frame (null) { + child = scrolled_comments, height_request = 110, hexpand = true, vexpand = true }; + var box_comments = new Granite.Box (VERTICAL, NONE); + box_comments.append (new Granite.HeaderLabel (_("Comments"))); + box_comments.append (frame_comments); + var delete_button = new Gtk.Button.from_icon_name ("user-trash-symbolic") { tooltip_text = _("Delete this location"), - halign = Gtk.Align.START, }; delete_button.add_css_class (Granite.CssClass.DESTRUCTIVE); - box.append (name_entry); - box.append (frame); - box.append (delete_button); + var box_toolbar = new Granite.Box (HORIZONTAL, HALF) { + halign = Gtk.Align.START, + }; + box_toolbar.append (delete_button); + + box.append (box_name); + box.append (box_comments); + box.append (box_toolbar); popover = new Gtk.Popover () { child = box @@ -106,15 +118,21 @@ public class Maps.Marker : Shumate.Marker { popover = popover, child = image, has_frame = false, - tooltip_text = loc_name }; popover.position = Gtk.PositionType.TOP; child = menubutton; + textview_comments.grab_focus (); - name_entry.changed.connect (on_changed); - comment_textview.buffer.changed.connect (on_changed); + bind_property ("loc-name", + menubutton, "tooltip-text", + GLib.BindingFlags.DEFAULT | GLib.BindingFlags.SYNC_CREATE + ); + + entry_name.changed.connect (on_changed); + textview_comments.buffer.changed.connect (on_changed); delete_button.clicked.connect (delete_marker); + popover.show.connect (on_show); } public void delete_marker () { @@ -132,6 +150,16 @@ public class Maps.Marker : Shumate.Marker { changed (); } + private void on_show () { + // Prevent entry_name from stealing the show by selecting its content + entry_name.select_region (-1, -1); + textview_comments.grab_focus (); + } + + public void grab_entry () { + entry_name.grab_focus (); + } + public MarkerData to_markerdata () { return new MarkerData (latitude, longitude) { name = loc_name, From 58ddf750e16059a991659504b6ec177a4a79d7bd Mon Sep 17 00:00:00 2001 From: teamcons Date: Sun, 1 Nov 2026 19:29:27 +0100 Subject: [PATCH 15/19] Make the linter happy --- src/Widgets/Marker.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Widgets/Marker.vala b/src/Widgets/Marker.vala index 7502b154..8df4e9fa 100644 --- a/src/Widgets/Marker.vala +++ b/src/Widgets/Marker.vala @@ -164,6 +164,6 @@ public class Maps.Marker : Shumate.Marker { return new MarkerData (latitude, longitude) { name = loc_name, comments = loc_comments - };; + }; } } From 3fc5c4784dc17fbc1842633fa23e6cf3f1e47c36 Mon Sep 17 00:00:00 2001 From: teamcons Date: Sun, 1 Nov 2026 19:38:28 +0100 Subject: [PATCH 16/19] Close popover on entry activate, and synchronize the latter with tooltip properly --- src/Widgets/Marker.vala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Widgets/Marker.vala b/src/Widgets/Marker.vala index 8df4e9fa..eb29d9a5 100644 --- a/src/Widgets/Marker.vala +++ b/src/Widgets/Marker.vala @@ -124,8 +124,8 @@ public class Maps.Marker : Shumate.Marker { child = menubutton; textview_comments.grab_focus (); - bind_property ("loc-name", - menubutton, "tooltip-text", + entry_name.bind_property ("text", + this, "tooltip-text", GLib.BindingFlags.DEFAULT | GLib.BindingFlags.SYNC_CREATE ); @@ -133,6 +133,7 @@ public class Maps.Marker : Shumate.Marker { textview_comments.buffer.changed.connect (on_changed); delete_button.clicked.connect (delete_marker); popover.show.connect (on_show); + entry_name.activate.connect (popover.popdown); } public void delete_marker () { From 2027ad9e9f8c9cddb8c28c5eb4cadfc8a04ca32c Mon Sep 17 00:00:00 2001 From: teamcons Date: Wed, 4 Nov 2026 14:59:21 +0100 Subject: [PATCH 17/19] Marker: Less height on frame --- src/Widgets/Marker.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Widgets/Marker.vala b/src/Widgets/Marker.vala index eb29d9a5..f76c31fe 100644 --- a/src/Widgets/Marker.vala +++ b/src/Widgets/Marker.vala @@ -83,7 +83,7 @@ public class Maps.Marker : Shumate.Marker { var frame_comments = new Gtk.Frame (null) { child = scrolled_comments, - height_request = 110, + height_request = 50, hexpand = true, vexpand = true }; From f688304f9eea08bc66d8e80e00a98012f6e769c5 Mon Sep 17 00:00:00 2001 From: teamcons Date: Wed, 4 Nov 2026 15:05:31 +0100 Subject: [PATCH 18/19] Marker: slightly more height --- src/Widgets/Marker.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Widgets/Marker.vala b/src/Widgets/Marker.vala index f76c31fe..99e0303a 100644 --- a/src/Widgets/Marker.vala +++ b/src/Widgets/Marker.vala @@ -83,7 +83,7 @@ public class Maps.Marker : Shumate.Marker { var frame_comments = new Gtk.Frame (null) { child = scrolled_comments, - height_request = 50, + height_request = 70, hexpand = true, vexpand = true }; From ec14c6d25c901e8533f8e8911b2b83b5dba448c9 Mon Sep 17 00:00:00 2001 From: teamcons Date: Thu, 5 Nov 2026 16:37:12 +0100 Subject: [PATCH 19/19] Marker: Follow Calendar EventEdition/InfoPanel --- src/Widgets/Marker.vala | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Widgets/Marker.vala b/src/Widgets/Marker.vala index 99e0303a..a26baf1d 100644 --- a/src/Widgets/Marker.vala +++ b/src/Widgets/Marker.vala @@ -63,8 +63,12 @@ public class Maps.Marker : Shumate.Marker { show_emoji_icon = true }; + var label_location = new Granite.HeaderLabel (_("Location name:")) { + mnemonic_widget = entry_name + }; + var box_name = new Granite.Box (VERTICAL, NONE); - box_name.append (new Granite.HeaderLabel (_("Location name"))); + box_name.append (label_location); box_name.append (entry_name); textview_comments = new Granite.HyperTextView () { @@ -88,8 +92,12 @@ public class Maps.Marker : Shumate.Marker { vexpand = true }; + var label_comments = new Granite.HeaderLabel (_("Comments:")) { + mnemonic_widget = textview_comments + }; + var box_comments = new Granite.Box (VERTICAL, NONE); - box_comments.append (new Granite.HeaderLabel (_("Comments"))); + box_comments.append (label_comments); box_comments.append (frame_comments); var delete_button = new Gtk.Button.from_icon_name ("user-trash-symbolic") {