From 5a7f43ad38670672761b96f63191d0489df1a5cd Mon Sep 17 00:00:00 2001 From: Bastiaan van der Plaat Date: Tue, 24 Oct 2023 21:57:35 +0200 Subject: [PATCH] Maps: Add favorites panel with favorite places management --- Base/res/graphics/maps/marker-red.png | Bin 0 -> 382 bytes Base/usr/share/man/man1/Applications/Maps.md | 3 + Userland/Applications/Maps/CMakeLists.txt | 5 + .../Applications/Maps/FavoritesEditDialog.gml | 42 +++++ .../Applications/Maps/FavoritesEditDialog.h | 19 +++ Userland/Applications/Maps/FavoritesPanel.cpp | 154 ++++++++++++++++++ Userland/Applications/Maps/FavoritesPanel.gml | 28 ++++ Userland/Applications/Maps/FavoritesPanel.h | 50 ++++++ Userland/Applications/Maps/main.cpp | 121 ++++++++++++-- Userland/Libraries/LibGUI/JsonArrayModel.h | 12 +- 10 files changed, 421 insertions(+), 13 deletions(-) create mode 100644 Base/res/graphics/maps/marker-red.png create mode 100644 Userland/Applications/Maps/FavoritesEditDialog.gml create mode 100644 Userland/Applications/Maps/FavoritesEditDialog.h create mode 100644 Userland/Applications/Maps/FavoritesPanel.cpp create mode 100644 Userland/Applications/Maps/FavoritesPanel.gml create mode 100644 Userland/Applications/Maps/FavoritesPanel.h diff --git a/Base/res/graphics/maps/marker-red.png b/Base/res/graphics/maps/marker-red.png new file mode 100644 index 0000000000000000000000000000000000000000..7d2ec39b8666a3eda041b846e7dc8cd225c09026 GIT binary patch literal 382 zcmeAS@N?(olHy`uVBq!ia0y~yU=U_tU{GLTW?*1onh^Jgfq}swz$e6&fq`L_jm>-` zvz5kHv-C|iSlBH$G2d?Q`tRSrx7Tl6nzwLQdiELbfHU^ar;LnGsH&ck`OD3~z`#)w z`uD$cj%S}^U|{&_>Eaktam)1lLO*6lfny*0U)wTp#WLSuV_^$y)z(xn zP?sukoG9Som|wvpB*(xaCp1At^vptz^s;r!R_?l6I(7Tp*2dO(+=9|` znN`cQPkLCjZt?ZKthZs}#4@HOQ=*lY=Pw&?e5;sToqeX|tw8d# z@Z@_p`V92AXX?lFPT2YC@Qn4Fq>>)oom;u*`<>&CFT2lwkB|SdA+##}=I{T$OZek< zvrRfJP!qeGo1r=2xIm2j0lx{d;;{@bb&f^+e<>b*RzyqIJb1+;Q&F8_zAX_ela_Wp q4qBPCP&aIqENAY + * + * 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)) { }