Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f43ef06
Initial draft: Introduce MarkerManager, Storage, Marker and MarkerData
Oct 22, 2026
f219753
Make the linter happy, ensure popover on top
Oct 22, 2026
dcfc57b
Test out storage and clean up behind us
Oct 22, 2026
de9e5a0
Add bespoke pin icon, and use trash-symbolic instead of a label
Oct 22, 2026
272171a
map_widget, mainwindow: Functional pin mode
Oct 30, 2026
74088a8
CSS: Hide the menubutton
Oct 30, 2026
e24f32a
MarkerManager: Add a debouncer
Oct 30, 2026
bbee6bf
MainWindow: Use a ToggleButton
Oct 30, 2026
b877adb
Document for ease of maintenance
Oct 30, 2026
ef557ac
Make the linter happy
Oct 30, 2026
c02fb9a
Add missing to POTFILES and widen the popup
Oct 30, 2026
9970c95
MapWidget: Use a dedicated layer
Nov 1, 2026
305df1c
Marker: Do not keep a MarkerData (redundant)
Nov 1, 2026
a28985b
Marker: Smarter grab behaviour on show
Nov 1, 2026
9c48920
Introduce a SearchResultMarker
Nov 1, 2026
f7048b5
SearchResultMarker: Fix a whoopsie
Nov 1, 2026
58ddf75
Make the linter happy
Nov 1, 2026
3fc5c47
Close popover on entry activate, and synchronize the latter with tool…
Nov 1, 2026
5c4b6f8
Merge branch 'main' into search_suggest_pin
teamcons Nov 2, 2026
4b7539d
SearchResultMarker: Allow displaying more info in temporary pin
Nov 2, 2026
171b074
Neater fallback
Nov 2, 2026
524e0ff
SearchResultMarker: Minimum viable product
Nov 2, 2026
002046e
Include more information
Nov 2, 2026
acf779f
Make new pin creation suggested
Nov 2, 2026
3398c6b
SearchResultMarker: include the place icon, and popup on creation
Nov 4, 2026
4999190
SearchResultMarker: button wording
Nov 4, 2026
2027ad9
Marker: Less height on frame
Nov 4, 2026
4257399
Update branch from markermanager
teamcons Nov 4, 2026
f688304
Marker: slightly more height
Nov 4, 2026
ec14c6d
Marker: Follow Calendar EventEdition/InfoPanel
Nov 5, 2026
df2bdfd
Merge pull request #2 from teamcons/markermanager
teamcons Nov 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions data/Application.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
3 changes: 3 additions & 0 deletions po/POTFILES
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
src/Application.vala
src/MainWindow.vala
src/MapWidget.vala
src/Objects/MarkerData.vala
src/SearchResultItem.vala
src/Widgets/Marker.vala
src/Widgets/SearchResultMarker.vala
60 changes: 59 additions & 1 deletion src/MainWindow.vala
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,45 @@ 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;
private Cancellable? search_cancellable = null;

private Gtk.Button current_location;
private Gtk.ToggleButton pin_location;
private Gtk.Spinner spinner;
private Gtk.SearchEntry search_entry;
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") {
tooltip_text = _("Move to the current location"),
valign = CENTER
};

pin_location = new Gtk.ToggleButton () {
tooltip_text = _("Pin a location"),
icon_name = "view-pin-symbolic",
action_name = "win.pin",
valign = CENTER
};

spinner = new Gtk.Spinner ();

search_entry = new Gtk.SearchEntry () {
Expand Down Expand Up @@ -160,6 +177,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);

Expand Down Expand Up @@ -239,7 +257,7 @@ public class Maps.MainWindow : Adw.ApplicationWindow {

search_listview.activate.connect ((pos) => {
var place = (Geocode.Place) selection_model.get_item (pos);
map_widget.go_to_location (place.location);
map_widget.go_to_place (place);
search_res_popover.popdown ();
});

Expand Down Expand Up @@ -365,4 +383,44 @@ public class Maps.MainWindow : Adw.ApplicationWindow {
var search_result_item = (SearchResultItem) list_item.child;
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.active = true;

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.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 ();
on_exit_pin_mode ();
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) {
if (keyval == Gdk.Key.Escape) {
map_widget.on_exit_pin_mode ();
on_exit_pin_mode ();
}
}
}
69 changes: 49 additions & 20 deletions src/MapWidget.vala
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,20 @@ public class Maps.MapWidget : Gtk.Box {
private Shumate.SimpleMap map_widget;
private Shumate.Map base_map;
// Displays the "pin" icon at a specified place by search
private Shumate.MarkerLayer pin_layer;
private Shumate.MarkerLayer search_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;

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;
Expand All @@ -40,16 +46,25 @@ 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;
}

public void init_marker_layers () {
pin_layer = new Shumate.MarkerLayer (map_widget.viewport);
map_widget.add_overlay_layer (pin_layer);
search_layer = new Shumate.MarkerLayer (map_widget.viewport);
map_widget.add_overlay_layer (search_layer);

location_layer = new Shumate.MarkerLayer (map_widget.viewport);
map_widget.add_overlay_layer (location_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 ();
}

// Set the initial location of the map widget.
Expand Down Expand Up @@ -146,13 +161,21 @@ public class Maps.MapWidget : Gtk.Box {
// we get an uri that looks like geo:///lat,lon, remove slashes
location.set_from_uri (uri.replace ("///", ""));

go_to_location (location);
var place = new Geocode.Place.with_location (_("Location"), Geocode.PlaceType.UNKNOWN, location);
go_to_place (place);
}

public void go_to_location (Geocode.Location location) {
pin_layer.remove_all ();
mark_pin_at (location.latitude, location.longitude);
base_map.go_to_full (location.latitude, location.longitude, DEFAULT_ZOOM_LEVEL);
public void go_to_place (Geocode.Place place) {
search_layer.remove_all ();
var search_result_marker = new SearchResultMarker (place);

search_result_marker.create_pin.connect ((markerdata) => {
search_layer.remove_all ();
marker_manager.add_marker (markerdata);
});

search_layer.add_marker (search_result_marker);
base_map.go_to_full (place.location.latitude, place.location.longitude, DEFAULT_ZOOM_LEVEL);
}

// Saves the latest state of the map.
Expand All @@ -177,18 +200,24 @@ public class Maps.MapWidget : Gtk.Box {
location_layer.add_marker (marker);
}

private void mark_pin_at (double latitude, double longitude) {
var image = new Gtk.Image.from_icon_name ("pointer") {
icon_size = Gtk.IconSize.LARGE
};
public void enter_pin_mode () {
map_widget.map.set_cursor_from_name ("crosshair");
gesture_click.released.connect (on_click);
}

var marker = new Shumate.Marker () {
latitude = latitude,
longitude = longitude,
child = image,
selectable = true
};
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) {
on_exit_pin_mode ();

double latitude, longitude;
map_widget.viewport.widget_coords_to_location (map_widget, x, y, out latitude, out longitude);
marker_manager.add_marker (new MarkerData (latitude, longitude));

pin_layer.add_marker (marker);
return;
}
}
52 changes: 52 additions & 0 deletions src/Objects/MarkerData.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io)
* 2018-2025 Ryo Nakano <ryonakaknock3@gmail.com>
* 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;}
public double longitude {get; set;}
public string name {get; set; default = _("New location");}
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");
comments = node.get_string_member ("comments");
}

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 ("comments");
builder.add_string_value (comments);
builder.end_object ();

return builder.get_root ().get_object ();
}
}
1 change: 1 addition & 0 deletions src/SearchResultItem.vala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public class Maps.SearchResultItem : Granite.Bin {
image.icon_name = place.icon.to_string () + "-symbolic";
name_label.label = place.name;
info_label.label = "%s, %s, %s".printf (street, postal_code, town);
place.street_address = info_label.label;

// Not add because this widget gets recycled
css_classes = {placetype_to_cssclass (place.place_type)};
Expand Down
93 changes: 93 additions & 0 deletions src/Services/MarkerManager.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io)
* 2018-2025 Ryo Nakano <ryonakaknock3@gmail.com>
* 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 uint debounce_timer_id;

public signal void pin_created (Marker marker);

private Storage storage;
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);
}

// 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);

marker.changed.connect (save_debounce);
marker.deletion_requested.connect (remove_marker);

pin_created (marker);
}

public void remove_marker (Shumate.Marker marker) {
markerlayer.remove_marker (marker);
save_debounce ();
}

public void save_debounce () {
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 () {
debug ("Saving");
var json_array = new Json.Array ();

foreach (var marker in markerlayer.get_markers ()) {
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 ();
marker.grab_entry ();
save_debounce ();
}

~MarkerManager () {
immediately_save ();
}
}
Loading
Loading