diff --git a/Base/res/graphics/maps/marker-red.png b/Base/res/graphics/maps/marker-red.png new file mode 100644 index 0000000000..7d2ec39b86 Binary files /dev/null and b/Base/res/graphics/maps/marker-red.png differ diff --git a/Base/usr/share/man/man1/Applications/Maps.md b/Base/usr/share/man/man1/Applications/Maps.md index 07981e6140..f181a99fd6 100644 --- a/Base/usr/share/man/man1/Applications/Maps.md +++ b/Base/usr/share/man/man1/Applications/Maps.md @@ -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`. diff --git a/Userland/Applications/Maps/CMakeLists.txt b/Userland/Applications/Maps/CMakeLists.txt index e5852d73c6..0700bb4c0b 100644 --- a/Userland/Applications/Maps/CMakeLists.txt +++ b/Userland/Applications/Maps/CMakeLists.txt @@ -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 diff --git a/Userland/Applications/Maps/FavoritesEditDialog.gml b/Userland/Applications/Maps/FavoritesEditDialog.gml new file mode 100644 index 0000000000..14e2131f7d --- /dev/null +++ b/Userland/Applications/Maps/FavoritesEditDialog.gml @@ -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 + } + } +} diff --git a/Userland/Applications/Maps/FavoritesEditDialog.h b/Userland/Applications/Maps/FavoritesEditDialog.h new file mode 100644 index 0000000000..1df7d28514 --- /dev/null +++ b/Userland/Applications/Maps/FavoritesEditDialog.h @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2023, Bastiaan van der Plaat + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace Maps { + +class FavoritesEditDialog final : public GUI::Widget { + C_OBJECT(FavoritesEditDialog) +public: + static ErrorOr> try_create(); +}; + +} diff --git a/Userland/Applications/Maps/FavoritesPanel.cpp b/Userland/Applications/Maps/FavoritesPanel.cpp new file mode 100644 index 0000000000..d80bc60ce1 --- /dev/null +++ b/Userland/Applications/Maps/FavoritesPanel.cpp @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2023, Bastiaan van der Plaat + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "FavoritesPanel.h" +#include "FavoritesEditDialog.h" +#include +#include +#include +#include +#include +#include + +namespace Maps { + +ErrorOr> FavoritesPanel::create() +{ + auto widget = TRY(try_create()); + TRY(widget->setup()); + return widget; +} + +ErrorOr FavoritesPanel::setup() +{ + m_empty_container = *find_descendant_of_type_named("empty_container"); + m_favorites_list = *find_descendant_of_type_named("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(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 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 FavoritesPanel::add_favorite(Favorite const& favorite) +{ + auto& model = *static_cast(m_favorites_list->model()); + Vector 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 FavoritesPanel::edit_favorite(int row) +{ + auto& model = *static_cast(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("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("ok_button"); + ok_button.on_click = [&](auto) { + Vector 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("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(m_favorites_list->model()); + m_empty_container->set_visible(model.row_count() == 0); + m_favorites_list->set_visible(model.row_count() > 0); + + Vector 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); +} + +} diff --git a/Userland/Applications/Maps/FavoritesPanel.gml b/Userland/Applications/Maps/FavoritesPanel.gml new file mode 100644 index 0000000000..4b1e8f449b --- /dev/null +++ b/Userland/Applications/Maps/FavoritesPanel.gml @@ -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 + } +} diff --git a/Userland/Applications/Maps/FavoritesPanel.h b/Userland/Applications/Maps/FavoritesPanel.h new file mode 100644 index 0000000000..bda6718779 --- /dev/null +++ b/Userland/Applications/Maps/FavoritesPanel.h @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023, Bastiaan van der Plaat + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include "MapWidget.h" +#include +#include +#include + +namespace Maps { + +class FavoritesPanel final : public GUI::Widget { + C_OBJECT(FavoritesPanel) + +public: + struct Favorite { + String name; + MapWidget::LatLng latlng; + int zoom; + }; + static ErrorOr> create(); + + void load_favorites(); + void reset(); + ErrorOr add_favorite(Favorite const& favorite); + + Function const&)> on_favorites_change; + Function on_selected_favorite_change; + +protected: + FavoritesPanel() = default; + + static ErrorOr> try_create(); + + ErrorOr setup(); + +private: + ErrorOr edit_favorite(int row); + void favorites_changed(); + + RefPtr m_empty_container; + RefPtr m_favorites_list; + RefPtr m_context_menu; +}; + +} diff --git a/Userland/Applications/Maps/main.cpp b/Userland/Applications/Maps/main.cpp index a31e2bc89d..941858adb2 100644 --- a/Userland/Applications/Maps/main.cpp +++ b/Userland/Applications/Maps/main.cpp @@ -4,6 +4,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include "FavoritesPanel.h" #include "SearchPanel.h" #include "UsersMapWidget.h" #include @@ -23,11 +24,12 @@ static int constexpr MAP_ZOOM_DEFAULT = 3; ErrorOr 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 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 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 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 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 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 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 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()); diff --git a/Userland/Libraries/LibGUI/JsonArrayModel.h b/Userland/Libraries/LibGUI/JsonArrayModel.h index 922a6e2c95..840fd1535b 100644 --- a/Userland/Libraries/LibGUI/JsonArrayModel.h +++ b/Userland/Libraries/LibGUI/JsonArrayModel.h @@ -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&& a_massage_for_display, Function&& a_massage_for_sort = {}, Function&& 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&& a_massage_for_display, Function&& a_massage_for_sort = {}, Function&& 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)) { }