Maps: Add favorites panel with favorite places management

This commit is contained in:
Bastiaan van der Plaat 2023-10-24 21:57:35 +02:00 committed by Andrew Kaster
parent 3acbffabf9
commit 5a7f43ad38
10 changed files with 421 additions and 13 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 B

View file

@ -24,11 +24,14 @@ Double-clicking anywhere on the map zooms-in to that location. Double-clicking w
Right-click on a location to:
* Copy its coordinates to the Clipboard
* Save it to your favorites
* Open it in various mapping services
* Center the map on it
Show and hide the search panel by clicking on the leftmost magnifying glass in the toolbar. Type your query, press `Return` and then click on a result to focus on it in the map. Navigate the search results with the `Up` and `Down` arrow keys.
Show and hide the favorites panel by clicking on the leftmost heart in the toolbar. You can add favorites with the right click contextmenu. You can edit and delete your favorites by right clicking on them. Navigate your favorites with the `Up` and `Down` arrow keys.
The default map tile provider can be changed in `Maps Settings`, enabling maps with labels in other languages, different types of map (e.g. topographical) and even setting a custom map. Other tile providers can be found [here](https://wiki.openstreetmap.org/wiki/Raster_tile_providers).
To see an overlay of where in the world SerenityOS users are, click on the Ladyball icon (the SerenityOS logo) or enable `View → Show SerenityOS Users`.

View file

@ -4,10 +4,15 @@ serenity_component(
TARGETS Maps
)
compile_gml(FavoritesEditDialog.gml FavoritesEditDialogGML.cpp)
compile_gml(FavoritesPanel.gml FavoritesPanelGML.cpp)
compile_gml(SearchPanel.gml SearchPanelGML.cpp)
set(SOURCES
main.cpp
FavoritesEditDialogGML.cpp
FavoritesPanelGML.cpp
FavoritesPanel.cpp
MapWidget.cpp
SearchPanelGML.cpp
SearchPanel.cpp

View file

@ -0,0 +1,42 @@
@Maps::FavoritesEditDialog {
fixed_width: 260
fixed_height: 61
fill_with_background_color: true
layout: @GUI::VerticalBoxLayout {
margins: [4]
}
@GUI::Widget {
fixed_height: 24
layout: @GUI::HorizontalBoxLayout {}
@GUI::Label {
text: "Name:"
text_alignment: "CenterLeft"
fixed_width: 30
}
@GUI::TextBox {
name: "name_textbox"
}
}
@GUI::Widget {
fixed_height: 24
layout: @GUI::HorizontalBoxLayout {}
@GUI::Layout::Spacer {}
@GUI::DialogButton {
name: "ok_button"
text: "OK"
fixed_width: 75
}
@GUI::DialogButton {
name: "cancel_button"
text: "Cancel"
fixed_width: 75
}
}
}

View file

@ -0,0 +1,19 @@
/*
* Copyright (c) 2023, Bastiaan van der Plaat <bastiaan.v.d.plaat@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibGUI/Widget.h>
namespace Maps {
class FavoritesEditDialog final : public GUI::Widget {
C_OBJECT(FavoritesEditDialog)
public:
static ErrorOr<NonnullRefPtr<FavoritesEditDialog>> try_create();
};
}

View file

@ -0,0 +1,154 @@
/*
* Copyright (c) 2023, Bastiaan van der Plaat <bastiaan.v.d.plaat@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "FavoritesPanel.h"
#include "FavoritesEditDialog.h"
#include <LibCore/StandardPaths.h>
#include <LibGUI/Button.h>
#include <LibGUI/JsonArrayModel.h>
#include <LibGUI/Menu.h>
#include <LibGUI/TextBox.h>
#include <LibGUI/Window.h>
namespace Maps {
ErrorOr<NonnullRefPtr<FavoritesPanel>> FavoritesPanel::create()
{
auto widget = TRY(try_create());
TRY(widget->setup());
return widget;
}
ErrorOr<void> FavoritesPanel::setup()
{
m_empty_container = *find_descendant_of_type_named<GUI::Frame>("empty_container");
m_favorites_list = *find_descendant_of_type_named<GUI::ListView>("favorites_list");
m_favorites_list->set_item_height(m_favorites_list->font().preferred_line_height() * 2 + m_favorites_list->vertical_padding());
m_favorites_list->on_selection_change = [this]() {
auto const& index = m_favorites_list->selection().first();
if (!index.is_valid())
return;
auto& model = *m_favorites_list->model();
on_selected_favorite_change({ MUST(String::from_deprecated_string(model.index(index.row(), 0).data().to_deprecated_string())),
{ model.index(index.row(), 1).data().as_double(),
model.index(index.row(), 2).data().as_double() },
model.index(index.row(), 3).data().to_i32() });
};
m_favorites_list->on_context_menu_request = [this](auto const& index, auto const& event) {
m_context_menu = GUI::Menu::construct();
m_context_menu->add_action(GUI::Action::create(
"&Edit...", MUST(Gfx::Bitmap::load_from_file("/res/icons/16x16/rename.png"sv)), [this, index](auto&) {
MUST(edit_favorite(index.row()));
},
this));
m_context_menu->add_action(GUI::CommonActions::make_delete_action(
[this, index](auto&) {
auto& model = *static_cast<GUI::JsonArrayModel*>(m_favorites_list->model());
MUST(model.remove(index.row()));
MUST(model.store());
favorites_changed();
},
this));
m_context_menu->popup(event.screen_position());
};
return {};
}
void FavoritesPanel::load_favorites()
{
Vector<GUI::JsonArrayModel::FieldSpec> favorites_fields;
favorites_fields.empend("name", "Name"_string, Gfx::TextAlignment::CenterLeft, [](JsonObject const& object) -> GUI::Variant {
DeprecatedString name = object.get_deprecated_string("name"sv).release_value();
double latitude = object.get_double("latitude"sv).release_value();
double longitude = object.get_double("longitude"sv).release_value();
return DeprecatedString::formatted("{}\n{:.5}, {:.5}", name, latitude, longitude);
});
favorites_fields.empend("latitude", "Latitude"_string, Gfx::TextAlignment::CenterLeft);
favorites_fields.empend("longitude", "Longitude"_string, Gfx::TextAlignment::CenterLeft);
favorites_fields.empend("zoom", "Zoom"_string, Gfx::TextAlignment::CenterLeft);
m_favorites_list->set_model(*GUI::JsonArrayModel::create(DeprecatedString::formatted("{}/MapsFavorites.json", Core::StandardPaths::config_directory()), move(favorites_fields)));
m_favorites_list->model()->invalidate();
favorites_changed();
}
ErrorOr<void> FavoritesPanel::add_favorite(Favorite const& favorite)
{
auto& model = *static_cast<GUI::JsonArrayModel*>(m_favorites_list->model());
Vector<JsonValue> favorite_json;
favorite_json.append(favorite.name.to_deprecated_string());
favorite_json.append(favorite.latlng.latitude);
favorite_json.append(favorite.latlng.longitude);
favorite_json.append(favorite.zoom);
TRY(model.add(move(favorite_json)));
TRY(model.store());
favorites_changed();
return {};
}
void FavoritesPanel::reset()
{
m_favorites_list->selection().clear();
m_favorites_list->scroll_to_top();
}
ErrorOr<void> FavoritesPanel::edit_favorite(int row)
{
auto& model = *static_cast<GUI::JsonArrayModel*>(m_favorites_list->model());
auto edit_dialog = TRY(GUI::Dialog::try_create(window()));
edit_dialog->set_title("Edit Favorite");
edit_dialog->resize(260, 61);
edit_dialog->set_resizable(false);
auto widget = TRY(Maps::FavoritesEditDialog::try_create());
edit_dialog->set_main_widget(widget);
auto& name_textbox = *widget->find_descendant_of_type_named<GUI::TextBox>("name_textbox");
name_textbox.set_text(model.index(row, 0).data().to_deprecated_string().split('\n').at(0));
name_textbox.set_focus(true);
name_textbox.select_all();
auto& ok_button = *widget->find_descendant_of_type_named<GUI::Button>("ok_button");
ok_button.on_click = [&](auto) {
Vector<JsonValue> favorite_json;
favorite_json.append(name_textbox.text());
favorite_json.append(model.index(row, 1).data().as_double());
favorite_json.append(model.index(row, 2).data().as_double());
favorite_json.append(model.index(row, 3).data().to_i32());
MUST(model.set(row, move(favorite_json)));
MUST(model.store());
favorites_changed();
edit_dialog->done(GUI::Dialog::ExecResult::OK);
};
ok_button.set_default(true);
auto& cancel_button = *widget->find_descendant_of_type_named<GUI::Button>("cancel_button");
cancel_button.on_click = [edit_dialog](auto) {
edit_dialog->done(GUI::Dialog::ExecResult::Cancel);
};
edit_dialog->exec();
return {};
}
void FavoritesPanel::favorites_changed()
{
auto& model = *static_cast<GUI::JsonArrayModel*>(m_favorites_list->model());
m_empty_container->set_visible(model.row_count() == 0);
m_favorites_list->set_visible(model.row_count() > 0);
Vector<Favorite> favorites;
for (int index = 0; index < model.row_count(); index++)
favorites.append({ MUST(String::from_deprecated_string(model.index(index, 0).data().to_deprecated_string())),
{ model.index(index, 1).data().as_double(),
model.index(index, 2).data().as_double() },
model.index(index, 3).data().to_i32() });
on_favorites_change(favorites);
}
}

View file

@ -0,0 +1,28 @@
@Maps::FavoritesPanel {
min_width: 100
preferred_width: 200
max_width: 350
layout: @GUI::VerticalBoxLayout {}
// Empty and favorites are toggled in visibility
@GUI::Frame {
name: "empty_container"
frame_style: "SunkenPanel"
layout: @GUI::VerticalBoxLayout {
margins: [4]
}
@GUI::Label {
text: "You don't have any favorite places"
text_alignment: "CenterLeft"
}
}
@GUI::ListView {
name: "favorites_list"
horizontal_padding: 6
vertical_padding: 4
should_hide_unnecessary_scrollbars: true
alternating_row_colors: false
}
}

View file

@ -0,0 +1,50 @@
/*
* Copyright (c) 2023, Bastiaan van der Plaat <bastiaan.v.d.plaat@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include "MapWidget.h"
#include <LibGUI/Dialog.h>
#include <LibGUI/ItemListModel.h>
#include <LibGUI/ListView.h>
namespace Maps {
class FavoritesPanel final : public GUI::Widget {
C_OBJECT(FavoritesPanel)
public:
struct Favorite {
String name;
MapWidget::LatLng latlng;
int zoom;
};
static ErrorOr<NonnullRefPtr<FavoritesPanel>> create();
void load_favorites();
void reset();
ErrorOr<void> add_favorite(Favorite const& favorite);
Function<void(Vector<Favorite> const&)> on_favorites_change;
Function<void(Favorite const&)> on_selected_favorite_change;
protected:
FavoritesPanel() = default;
static ErrorOr<NonnullRefPtr<FavoritesPanel>> try_create();
ErrorOr<void> setup();
private:
ErrorOr<void> edit_favorite(int row);
void favorites_changed();
RefPtr<GUI::Frame> m_empty_container;
RefPtr<GUI::ListView> m_favorites_list;
RefPtr<GUI::Menu> m_context_menu;
};
}

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "FavoritesPanel.h"
#include "SearchPanel.h"
#include "UsersMapWidget.h"
#include <LibConfig/Client.h>
@ -23,11 +24,12 @@ static int constexpr MAP_ZOOM_DEFAULT = 3;
ErrorOr<int> serenity_main(Main::Arguments arguments)
{
TRY(Core::System::pledge("stdio recvfd sendfd rpath unix proc exec"));
TRY(Core::System::pledge("stdio recvfd sendfd rpath wpath cpath unix proc exec"));
auto app = TRY(GUI::Application::create(arguments));
TRY(Core::System::unveil("/bin/MapsSettings", "x"));
TRY(Core::System::unveil("/home", "rwc"));
TRY(Core::System::unveil("/res", "r"));
TRY(Core::System::unveil("/tmp/session/%sid/portal/config", "rw"));
TRY(Core::System::unveil("/tmp/session/%sid/portal/launch", "rw"));
@ -65,11 +67,15 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
map_widget.set_frame_style(Gfx::FrameStyle::SunkenContainer);
map_widget.set_show_users(Config::read_bool("Maps"sv, "MapView"sv, "ShowUsers"sv, false));
// Panels
String init_panel_open_name = TRY(String::from_deprecated_string(Config::read_string("Maps"sv, "Panel"sv, "OpenName"sv, ""sv)));
int panel_width = Config::read_i32("Maps"sv, "Panel"sv, "Width"sv, INT_MIN);
// Search panel
auto search_panel = TRY(Maps::SearchPanel::create());
search_panel->on_places_change = [&map_widget](auto) { map_widget.remove_markers_with_name("search"sv); };
search_panel->on_selected_place_change = [&map_widget](auto const& place) {
// Remove old search markers
// Remove old search marker
map_widget.remove_markers_with_name("search"sv);
// Add new marker and zoom into it
@ -77,8 +83,66 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
map_widget.set_center(place.latlng);
map_widget.set_zoom(place.zoom);
};
if (Config::read_bool("Maps"sv, "SearchPanel"sv, "Show"sv, false))
main_widget.insert_child_before(search_panel, map_widget);
main_widget.insert_child_before(search_panel, map_widget);
auto show_search_panel = [&]() {
if (panel_width != INT_MIN)
search_panel->set_preferred_width(panel_width);
search_panel->set_visible(true);
};
auto hide_search_panel = [&](bool save_width = true) {
if (save_width)
panel_width = search_panel->width();
search_panel->set_visible(false);
map_widget.remove_markers_with_name("search"sv);
search_panel->reset();
};
if (init_panel_open_name == "search") {
show_search_panel();
} else {
hide_search_panel(false);
}
// Favorites panel
auto marker_red_image = TRY(Gfx::Bitmap::load_from_file("/res/graphics/maps/marker-red.png"sv));
auto favorites_panel = TRY(Maps::FavoritesPanel::create());
favorites_panel->on_favorites_change = [&map_widget, marker_red_image](auto const& favorites) {
// Sync all favorites markers
map_widget.remove_markers_with_name("favorites"sv);
for (auto const& favorite : favorites)
map_widget.add_marker({ favorite.latlng, favorite.name, marker_red_image, "favorites"_string });
};
favorites_panel->on_selected_favorite_change = [&map_widget](auto const& favorite) {
// Zoom into favorite marker
map_widget.set_center(favorite.latlng);
map_widget.set_zoom(favorite.zoom);
};
favorites_panel->load_favorites();
main_widget.insert_child_before(favorites_panel, map_widget);
auto favorites_icon = TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/app-hearts.png"sv));
map_widget.add_context_menu_action(GUI::Action::create(
"Add to &Favorites", favorites_icon, [favorites_panel, &map_widget](auto&) {
MUST(favorites_panel->add_favorite({ "Unnamed place"_string, map_widget.context_menu_latlng(), map_widget.zoom() }));
},
window));
auto show_favorites_panel = [&]() {
if (panel_width != INT_MIN)
favorites_panel->set_preferred_width(panel_width);
favorites_panel->set_visible(true);
};
auto hide_favorites_panel = [&](bool save_width = true) {
if (save_width)
panel_width = favorites_panel->width();
favorites_panel->set_visible(false);
favorites_panel->reset();
};
if (init_panel_open_name == "favorites") {
show_favorites_panel();
} else {
hide_favorites_panel(false);
}
// Main menu actions
auto file_menu = window->add_menu("&File"_string);
@ -90,20 +154,40 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
file_menu->add_action(GUI::CommonActions::make_quit_action([](auto&) { GUI::Application::the()->quit(); }));
auto view_menu = window->add_menu("&View"_string);
RefPtr<GUI::Action> show_favorites_panel_action;
auto show_search_panel_action = GUI::Action::create_checkable(
"Show search panel", TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/find.png"sv)), [&main_widget, search_panel, &map_widget](auto& action) {
"Show &search panel", TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/find.png"sv)), [&](auto& action) {
if (favorites_panel->is_visible()) {
show_favorites_panel_action->set_checked(false);
hide_favorites_panel();
}
if (action.is_checked()) {
main_widget.insert_child_before(search_panel, map_widget);
show_search_panel();
} else {
map_widget.remove_markers_with_name("search"sv);
search_panel->reset();
main_widget.remove_child(search_panel);
hide_search_panel();
}
},
window);
show_search_panel_action->set_checked(Config::read_bool("Maps"sv, "SearchPanel"sv, "Show"sv, false));
show_search_panel_action->set_checked(search_panel->is_visible());
show_favorites_panel_action = GUI::Action::create_checkable(
"Show &favorites panel", favorites_icon, [&](auto& action) {
if (search_panel->is_visible()) {
show_search_panel_action->set_checked(false);
hide_search_panel();
}
if (action.is_checked()) {
show_favorites_panel();
} else {
hide_favorites_panel();
}
},
window);
show_favorites_panel_action->set_checked(favorites_panel->is_visible());
auto show_users_action = GUI::Action::create_checkable(
"Show SerenityOS users", TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/ladyball.png"sv)), [&map_widget](auto& action) { map_widget.set_show_users(action.is_checked()); }, window);
"Show SerenityOS &users", TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/ladyball.png"sv)), [&map_widget](auto& action) { map_widget.set_show_users(action.is_checked()); }, window);
show_users_action->set_checked(map_widget.show_users());
auto zoom_in_action = GUI::CommonActions::make_zoom_in_action([&map_widget](auto&) { map_widget.set_zoom(map_widget.zoom() + 1); }, window);
auto zoom_out_action = GUI::CommonActions::make_zoom_out_action([&map_widget](auto&) { map_widget.set_zoom(map_widget.zoom() - 1); }, window);
@ -115,6 +199,7 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
},
window);
view_menu->add_action(show_search_panel_action);
view_menu->add_action(adopt_ref(*show_favorites_panel_action));
view_menu->add_separator();
view_menu->add_action(show_users_action);
view_menu->add_separator();
@ -133,6 +218,7 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
// Main toolbar actions
toolbar.add_action(show_search_panel_action);
toolbar.add_action(adopt_ref(*show_favorites_panel_action));
toolbar.add_separator();
toolbar.add_action(show_users_action);
toolbar.add_separator();
@ -146,7 +232,18 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
// Remember last window state
int exec = app->exec();
Config::write_bool("Maps"sv, "SearchPanel"sv, "Show"sv, show_search_panel_action->is_checked());
if (search_panel->is_visible()) {
Config::write_string("Maps"sv, "Panel"sv, "OpenName"sv, "search"sv);
Config::write_i32("Maps"sv, "Panel"sv, "Width"sv, search_panel->width());
} else if (favorites_panel->is_visible()) {
Config::write_string("Maps"sv, "Panel"sv, "OpenName"sv, "favorites"sv);
Config::write_i32("Maps"sv, "Panel"sv, "Width"sv, favorites_panel->width());
} else {
Config::remove_key("Maps"sv, "Panel"sv, "OpenName"sv);
Config::remove_key("Maps"sv, "Panel"sv, "Width"sv);
}
Config::write_string("Maps"sv, "MapView"sv, "CenterLatitude"sv, TRY(String::number(map_widget.center().latitude)));
Config::write_string("Maps"sv, "MapView"sv, "CenterLongitude"sv, TRY(String::number(map_widget.center().longitude)));
Config::write_i32("Maps"sv, "MapView"sv, "Zoom"sv, map_widget.zoom());

View file

@ -16,6 +16,13 @@ namespace GUI {
class JsonArrayModel final : public Model {
public:
struct FieldSpec {
FieldSpec(DeprecatedString const& a_json_field_name, String const& a_column_name, Gfx::TextAlignment a_text_alignment)
: json_field_name(a_json_field_name)
, column_name(a_column_name)
, text_alignment(a_text_alignment)
{
}
FieldSpec(String const& a_column_name, Gfx::TextAlignment a_text_alignment, Function<Variant(JsonObject const&)>&& a_massage_for_display, Function<Variant(JsonObject const&)>&& a_massage_for_sort = {}, Function<Variant(JsonObject const&)>&& a_massage_for_custom = {})
: column_name(a_column_name)
, text_alignment(a_text_alignment)
@ -25,10 +32,13 @@ public:
{
}
FieldSpec(DeprecatedString const& a_json_field_name, String const& a_column_name, Gfx::TextAlignment a_text_alignment)
FieldSpec(DeprecatedString const& a_json_field_name, String const& a_column_name, Gfx::TextAlignment a_text_alignment, Function<Variant(JsonObject const&)>&& a_massage_for_display, Function<Variant(JsonObject const&)>&& a_massage_for_sort = {}, Function<Variant(JsonObject const&)>&& a_massage_for_custom = {})
: json_field_name(a_json_field_name)
, column_name(a_column_name)
, text_alignment(a_text_alignment)
, massage_for_display(move(a_massage_for_display))
, massage_for_sort(move(a_massage_for_sort))
, massage_for_custom(move(a_massage_for_custom))
{
}