diff --git a/Base/home/anon/.config/MapsTileProviders.json b/Base/home/anon/.config/MapsTileProviders.json new file mode 100644 index 0000000000..c702eb0b83 --- /dev/null +++ b/Base/home/anon/.config/MapsTileProviders.json @@ -0,0 +1,32 @@ +[ + { + "name": "OpenStreetMap (Local labels)", + "url_format": "https://tile.openstreetmap.org/{}/{}/{}.png", + "attribution_text": "© OpenStreetMap contributors", + "attribution_url": "https://www.openstreetmap.org/copyright" + }, + { + "name": "OpenStreetMap (German labels)", + "url_format": "https://tile.openstreetmap.de/{}/{}/{}.png", + "attribution_text": "© OpenStreetMap contributors", + "attribution_url": "https://www.openstreetmap.org/copyright" + }, + { + "name": "OpenStreetMap (French labels)", + "url_format": "https://a.tile.openstreetmap.fr/osmfr/{}/{}/{}.png", + "attribution_text": "© OpenStreetMap contributors", + "attribution_url": "https://www.openstreetmap.fr/copyright" + }, + { + "name": "OpenStreetMap (Humanitarian map style)", + "url_format": "https://a.tile.openstreetmap.fr/hot/{}/{}/{}.png", + "attribution_text": "© OpenStreetMap contributors", + "attribution_url": "https://www.openstreetmap.fr/copyright" + }, + { + "name": "OpenStreetMap (Topographical map style)", + "url_format": "https://a.tile.opentopomap.org/{}/{}/{}.png", + "attribution_text": "© OpenStreetMap contributors", + "attribution_url": "https://www.opentopomap.org/credits" + } +] diff --git a/Base/res/apps/MapsSettings.af b/Base/res/apps/MapsSettings.af new file mode 100644 index 0000000000..f49f36049d --- /dev/null +++ b/Base/res/apps/MapsSettings.af @@ -0,0 +1,6 @@ +[App] +Name=Maps Settings +Executable=/bin/MapsSettings +Category=Settings +Description=Configure the Maps application +ExcludeFromSystemMenu=true diff --git a/Userland/Applications/CMakeLists.txt b/Userland/Applications/CMakeLists.txt index 39191522cd..41884484ce 100644 --- a/Userland/Applications/CMakeLists.txt +++ b/Userland/Applications/CMakeLists.txt @@ -26,6 +26,7 @@ add_subdirectory(Magnifier) add_subdirectory(Mail) add_subdirectory(MailSettings) add_subdirectory(Maps) +add_subdirectory(MapsSettings) add_subdirectory(MouseSettings) add_subdirectory(NetworkSettings) add_subdirectory(PartitionEditor) diff --git a/Userland/Applications/Maps/MapWidget.cpp b/Userland/Applications/Maps/MapWidget.cpp index cfd9e3d6ac..1e576164f0 100644 --- a/Userland/Applications/Maps/MapWidget.cpp +++ b/Userland/Applications/Maps/MapWidget.cpp @@ -7,6 +7,8 @@ #include "MapWidget.h" #include +#include +#include #include #include #include @@ -51,7 +53,7 @@ double MapWidget::LatLng::distance_to(LatLng const& other) const // MapWidget class MapWidget::MapWidget(Options const& options) - : m_tile_layer_url(options.tile_layer_url) + : m_tile_provider(options.tile_provider) , m_center(options.center) , m_zoom(options.zoom) , m_context_menu_enabled(options.context_menu_enabled) @@ -60,9 +62,13 @@ MapWidget::MapWidget(Options const& options) , m_attribution_enabled(options.attribution_enabled) { m_request_client = Protocol::RequestClient::try_create().release_value_but_fixme_should_propagate_errors(); - if (options.attribution_enabled) - add_panel({ options.attribution_text, Panel::Position::BottomRight, options.attribution_url, true }); + if (options.attribution_enabled) { + auto attribution_text = options.attribution_text.value_or(MUST(String::from_deprecated_string(Config::read_string("Maps"sv, "MapWidget"sv, "TileProviderAttributionText"sv, Maps::default_tile_provider_attribution_text)))); + URL attribution_url = options.attribution_url.value_or(URL(Config::read_string("Maps"sv, "MapWidget"sv, "TileProviderAttributionUrl"sv, Maps::default_tile_provider_attribution_url))); + add_panel({ attribution_text, Panel::Position::BottomRight, attribution_url, "attribution"_string }); + } m_marker_image = Gfx::Bitmap::load_from_file("/res/graphics/maps/marker-blue.png"sv).release_value_but_fixme_should_propagate_errors(); + m_default_tile_provider = MUST(String::from_deprecated_string(Config::read_string("Maps"sv, "MapWidget"sv, "TileProviderUrlFormat"sv, Maps::default_tile_provider_url_format))); } void MapWidget::set_zoom(int zoom) @@ -72,6 +78,42 @@ void MapWidget::set_zoom(int zoom) update(); } +void MapWidget::config_string_did_change(StringView domain, StringView group, StringView key, StringView value) +{ + if (domain != "Maps" || group != "MapWidget") + return; + + if (key == "TileProviderUrlFormat") { + // When config tile provider changes clear all active requests and loaded tiles + m_default_tile_provider = MUST(String::from_utf8(value)); + m_first_image_loaded = false; + m_active_requests.clear(); + m_tiles.clear(); + update(); + } + + if (key == "TileProviderAttributionText") { + // Update attribution panel text when it exists + for (auto& panel : m_panels) { + if (panel.name == "attribution") { + panel.text = MUST(String::from_utf8(value)); + return; + } + } + update(); + } + + if (key == "TileProviderAttributionUrl") { + // Update attribution panel url when it exists + for (auto& panel : m_panels) { + if (panel.name == "attribution") { + panel.url = URL(value); + return; + } + } + } +} + void MapWidget::doubleclick_event(GUI::MouseEvent& event) { int new_zoom = event.shift() ? m_zoom - 1 : m_zoom + 1; @@ -261,7 +303,7 @@ void MapWidget::process_tile_queue() HashMap headers; headers.set("User-Agent", "SerenityOS Maps"); headers.set("Accept", "image/png"); - URL url(MUST(String::formatted(m_tile_layer_url, tile_key.zoom, tile_key.x, tile_key.y))); + URL url(MUST(String::formatted(m_tile_provider.value_or(m_default_tile_provider), tile_key.zoom, tile_key.x, tile_key.y))); auto request = m_request_client->start_request("GET", url, headers, {}); VERIFY(!request.is_null()); diff --git a/Userland/Applications/Maps/MapWidget.h b/Userland/Applications/Maps/MapWidget.h index 8da48b7600..6149a84370 100644 --- a/Userland/Applications/Maps/MapWidget.h +++ b/Userland/Applications/Maps/MapWidget.h @@ -8,13 +8,15 @@ #pragma once #include +#include #include #include #include #include #include -class MapWidget : public GUI::Frame { +class MapWidget : public GUI::Frame + , public Config::Listener { C_OBJECT(MapWidget); public: @@ -26,15 +28,15 @@ public: }; struct Options { - String tile_layer_url { "https://tile.openstreetmap.org/{}/{}/{}.png"_string }; + Optional tile_provider {}; LatLng center; int zoom; bool context_menu_enabled { true }; bool scale_enabled { true }; int scale_max_width { 100 }; bool attribution_enabled { true }; - String attribution_text { "© OpenStreetMap contributors"_string }; - URL attribution_url { "https://www.openstreetmap.org/copyright"sv }; + Optional attribution_text {}; + Optional attribution_url {}; }; LatLng center() const { return m_center; } @@ -76,7 +78,7 @@ public: String text; Position position; Optional url {}; - bool persistent { false }; + Optional name {}; Gfx::IntRect rect { 0, 0, 0, 0 }; }; void add_panel(Panel const& panel) @@ -84,9 +86,9 @@ public: m_panels.append(panel); update(); } - void clear_panels() + void remove_panels_with_name(StringView name) { - m_panels.remove_all_matching([](auto const& panel) { return !panel.persistent; }); + m_panels.remove_all_matching([name](auto const& panel) { return panel.name == name; }); update(); } @@ -117,6 +119,7 @@ protected: RefPtr request_client() const { return m_request_client; } private: + virtual void config_string_did_change(StringView domain, StringView group, StringView key, StringView value) override; virtual void doubleclick_event(GUI::MouseEvent&) override; virtual void mousemove_event(GUI::MouseEvent&) override; virtual void mousedown_event(GUI::MouseEvent&) override; @@ -154,7 +157,8 @@ private: Vector, TILES_DOWNLOAD_PARALLEL_MAX> m_active_requests; Queue m_tile_queue; RefPtr m_marker_image; - String m_tile_layer_url; + Optional m_tile_provider; + String m_default_tile_provider; LatLng m_center; int m_zoom {}; bool m_context_menu_enabled {}; diff --git a/Userland/Applications/Maps/UsersMapWidget.cpp b/Userland/Applications/Maps/UsersMapWidget.cpp index 6363e1e1df..22178366fc 100644 --- a/Userland/Applications/Maps/UsersMapWidget.cpp +++ b/Userland/Applications/Maps/UsersMapWidget.cpp @@ -73,5 +73,6 @@ void UsersMapWidget::add_users_to_map() add_panel({ MUST(String::formatted("{} users are already registered", m_users.value().size())), Panel::Position::TopRight, - { { "https://github.com/SerenityOS/user-map" } } }); + { { "https://github.com/SerenityOS/user-map" } }, + "users"_string }); } diff --git a/Userland/Applications/Maps/UsersMapWidget.h b/Userland/Applications/Maps/UsersMapWidget.h index 876361a616..931ce6ce45 100644 --- a/Userland/Applications/Maps/UsersMapWidget.h +++ b/Userland/Applications/Maps/UsersMapWidget.h @@ -24,7 +24,7 @@ public: } } else { clear_markers(); - clear_panels(); + remove_panels_with_name("users"sv); } } diff --git a/Userland/Applications/Maps/main.cpp b/Userland/Applications/Maps/main.cpp index fcb7bdadac..318523daf1 100644 --- a/Userland/Applications/Maps/main.cpp +++ b/Userland/Applications/Maps/main.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -19,16 +20,19 @@ static int constexpr MAP_ZOOM_DEFAULT = 3; ErrorOr serenity_main(Main::Arguments arguments) { - TRY(Core::System::pledge("stdio recvfd sendfd rpath unix")); + TRY(Core::System::pledge("stdio recvfd sendfd rpath unix proc exec")); auto app = TRY(GUI::Application::create(arguments)); + TRY(Core::System::unveil("/bin/MapsSettings", "x")); 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")); TRY(Core::System::unveil("/tmp/session/%sid/portal/request", "rw")); TRY(Core::System::unveil(nullptr, nullptr)); + Config::monitor_domain("Maps"); + auto app_icon = TRY(GUI::Icon::try_create_default_icon("app-maps"sv)); auto window = GUI::Window::construct(); window->set_title("Maps"); @@ -56,6 +60,11 @@ ErrorOr serenity_main(Main::Arguments arguments) // Main menu actions auto file_menu = window->add_menu("&File"_string); + auto open_settings_action = GUI::Action::create("Maps &Settings", { Mod_Ctrl, Key_Comma }, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/app-settings.png"sv)), [window](GUI::Action const&) { + GUI::Process::spawn_or_show_error(window, "/bin/MapsSettings"sv); + }); + file_menu->add_action(open_settings_action); + file_menu->add_separator(); file_menu->add_action(GUI::CommonActions::make_quit_action([](auto&) { GUI::Application::the()->quit(); })); auto view_menu = window->add_menu("&View"_string); @@ -89,6 +98,8 @@ ErrorOr serenity_main(Main::Arguments arguments) toolbar->add_action(zoom_in_action); toolbar->add_action(zoom_out_action); toolbar->add_action(reset_zoom_action); + toolbar->add_separator(); + toolbar->add_action(open_settings_action); window->show(); diff --git a/Userland/Applications/MapsSettings/CMakeLists.txt b/Userland/Applications/MapsSettings/CMakeLists.txt new file mode 100644 index 0000000000..488f36e596 --- /dev/null +++ b/Userland/Applications/MapsSettings/CMakeLists.txt @@ -0,0 +1,16 @@ +serenity_component( + MapsSettings + RECOMMENDED + TARGETS MapsSettings +) + +compile_gml(MapsSettingsWidget.gml MapsSettingsWidgetGML.cpp) + +set(SOURCES + main.cpp + MapsSettingsWidgetGML.cpp + MapsSettingsWidget.cpp +) + +serenity_app(MapsSettings ICON app-maps) +target_link_libraries(MapsSettings PRIVATE LibConfig LibCore LibGfx LibGUI LibMain) diff --git a/Userland/Applications/MapsSettings/Defaults.h b/Userland/Applications/MapsSettings/Defaults.h new file mode 100644 index 0000000000..4a46d177c9 --- /dev/null +++ b/Userland/Applications/MapsSettings/Defaults.h @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2023, Bastiaan van der Plaat + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace Maps { + +static constexpr StringView default_tile_provider_url_format = "https://tile.openstreetmap.org/{}/{}/{}.png"sv; +static constexpr StringView default_tile_provider_attribution_text = "© OpenStreetMap contributors"sv; +static constexpr StringView default_tile_provider_attribution_url = "https://www.openstreetmap.org/copyright"sv; + +} diff --git a/Userland/Applications/MapsSettings/MapsSettingsWidget.cpp b/Userland/Applications/MapsSettings/MapsSettingsWidget.cpp new file mode 100644 index 0000000000..ec50f49640 --- /dev/null +++ b/Userland/Applications/MapsSettings/MapsSettingsWidget.cpp @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2023, Bastiaan van der Plaat + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "MapsSettingsWidget.h" +#include "Defaults.h" +#include +#include +#include +#include +#include + +namespace MapsSettings { + +ErrorOr> MapsSettingsWidget::create() +{ + auto widget = TRY(try_create()); + TRY(widget->setup()); + return widget; +} + +void MapsSettingsWidget::apply_settings() +{ + // Tile Provider + if (m_is_custom_tile_provider) { + Config::write_string("Maps"sv, "MapWidget"sv, "TileProviderUrlFormat"sv, m_custom_tile_provider_textbox->text()); + Config::remove_key("Maps"sv, "MapWidget"sv, "TileProviderAttributionText"sv); + Config::remove_key("Maps"sv, "MapWidget"sv, "TileProviderAttributionUrl"sv); + } else { + auto tile_provider_url_format = m_tile_provider_combobox->model()->index(m_tile_provider_combobox->selected_index(), 1).data().to_deprecated_string(); + Config::write_string("Maps"sv, "MapWidget"sv, "TileProviderUrlFormat"sv, tile_provider_url_format); + auto tile_provider_attribution_text = m_tile_provider_combobox->model()->index(m_tile_provider_combobox->selected_index(), 2).data().to_deprecated_string(); + Config::write_string("Maps"sv, "MapWidget"sv, "TileProviderAttributionText"sv, tile_provider_attribution_text); + auto tile_provider_attribution_url = m_tile_provider_combobox->model()->index(m_tile_provider_combobox->selected_index(), 3).data().to_deprecated_string(); + Config::write_string("Maps"sv, "MapWidget"sv, "TileProviderAttributionUrl"sv, tile_provider_attribution_url); + } +} + +void MapsSettingsWidget::reset_default_values() +{ + set_tile_provider(Maps::default_tile_provider_url_format); +} + +ErrorOr MapsSettingsWidget::setup() +{ + // Tile Provider + Vector tile_provider_fields; + tile_provider_fields.empend("name", "Name"_string, Gfx::TextAlignment::CenterLeft); + tile_provider_fields.empend("url_format", "URL format"_string, Gfx::TextAlignment::CenterLeft); + tile_provider_fields.empend("attribution_text", "Attribution text"_string, Gfx::TextAlignment::CenterLeft); + tile_provider_fields.empend("attribution_url", "Attribution URL"_string, Gfx::TextAlignment::CenterLeft); + auto tile_providers = GUI::JsonArrayModel::create(DeprecatedString::formatted("{}/MapsTileProviders.json", Core::StandardPaths::config_directory()), move(tile_provider_fields)); + tile_providers->invalidate(); + + Vector custom_tile_provider; + custom_tile_provider.append("Custom..."); + custom_tile_provider.append(""); + custom_tile_provider.append(""); + custom_tile_provider.append(""); + TRY(tile_providers->add(move(custom_tile_provider))); + + m_tile_provider_combobox = *find_descendant_of_type_named("tile_provider_combobox"); + m_tile_provider_combobox->set_model(move(tile_providers)); + m_tile_provider_combobox->set_only_allow_values_from_model(true); + + m_custom_tile_provider_group = *find_descendant_of_type_named("custom_tile_provider_group"); + + m_custom_tile_provider_textbox = *find_descendant_of_type_named("custom_tile_provider_textbox"); + m_custom_tile_provider_textbox->set_placeholder(Maps::default_tile_provider_url_format); + m_custom_tile_provider_textbox->on_change = [&]() { set_modified(true); }; + + m_tile_provider_combobox->on_change = [&](DeprecatedString const&, GUI::ModelIndex const& index) { + auto tile_provider_url_format = m_tile_provider_combobox->model()->index(index.row(), 1).data().to_deprecated_string(); + m_is_custom_tile_provider = tile_provider_url_format.is_empty(); + m_custom_tile_provider_group->set_enabled(m_is_custom_tile_provider); + set_modified(true); + }; + set_tile_provider(Config::read_string("Maps"sv, "MapWidget"sv, "TileProviderUrlFormat"sv, Maps::default_tile_provider_url_format)); + + return {}; +} + +void MapsSettingsWidget::set_tile_provider(StringView tile_provider_url_format) +{ + bool found = false; + for (int index = 0; index < m_tile_provider_combobox->model()->row_count(); index++) { + auto url_format = m_tile_provider_combobox->model()->index(index, 1).data().to_deprecated_string(); + if (url_format == tile_provider_url_format) { + m_tile_provider_combobox->set_selected_index(index, GUI::AllowCallback::No); + found = true; + break; + } + } + + if (!found) { + m_is_custom_tile_provider = true; + m_custom_tile_provider_textbox->set_text(tile_provider_url_format, GUI::AllowCallback::No); + m_tile_provider_combobox->set_selected_index(m_tile_provider_combobox->model()->row_count() - 1, GUI::AllowCallback::No); + m_custom_tile_provider_group->set_enabled(true); + } else { + m_custom_tile_provider_group->set_enabled(false); + } +} + +} diff --git a/Userland/Applications/MapsSettings/MapsSettingsWidget.gml b/Userland/Applications/MapsSettings/MapsSettingsWidget.gml new file mode 100644 index 0000000000..c794073dfd --- /dev/null +++ b/Userland/Applications/MapsSettings/MapsSettingsWidget.gml @@ -0,0 +1,58 @@ +@MapsSettings::MapsSettingsWidget { + fill_with_background_color: true + layout: @GUI::VerticalBoxLayout { + margins: [8] + } + + @GUI::GroupBox { + title: "Tile Provider" + fixed_height: 104 + layout: @GUI::VerticalBoxLayout { + margins: [16, 8, 8] + spacing: 2 + } + + @GUI::Widget { + layout: @GUI::HorizontalBoxLayout { + spacing: 16 + } + + @GUI::ImageWidget { + fixed_width: 32 + fixed_height: 32 + bitmap: "/res/icons/32x32/search-engine.png" + } + + @GUI::Label { + text: "Tile Provider:" + text_alignment: "CenterLeft" + fixed_width: 110 + } + + @GUI::ComboBox { + name: "tile_provider_combobox" + } + } + + @GUI::Widget { + layout: @GUI::HorizontalBoxLayout { + spacing: 16 + } + name: "custom_tile_provider_group" + + @GUI::Widget { + fixed_width: 32 + } + + @GUI::Label { + text: "Enter URL template:" + text_alignment: "CenterLeft" + fixed_width: 110 + } + + @GUI::TextBox { + name: "custom_tile_provider_textbox" + } + } + } +} diff --git a/Userland/Applications/MapsSettings/MapsSettingsWidget.h b/Userland/Applications/MapsSettings/MapsSettingsWidget.h new file mode 100644 index 0000000000..78ca46ec3f --- /dev/null +++ b/Userland/Applications/MapsSettings/MapsSettingsWidget.h @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023, Bastiaan van der Plaat + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace MapsSettings { + +class MapsSettingsWidget final : public GUI::SettingsWindow::Tab { + C_OBJECT_ABSTRACT(MapsSettingsWidget) + +public: + static ErrorOr> create(); + + virtual void apply_settings() override; + virtual void reset_default_values() override; + +private: + MapsSettingsWidget() = default; + static ErrorOr> try_create(); + + ErrorOr setup(); + void set_tile_provider(StringView url); + + RefPtr m_tile_provider_combobox; + RefPtr m_custom_tile_provider_group; + RefPtr m_custom_tile_provider_textbox; + bool m_is_custom_tile_provider { false }; +}; + +} diff --git a/Userland/Applications/MapsSettings/main.cpp b/Userland/Applications/MapsSettings/main.cpp new file mode 100644 index 0000000000..23e36c1911 --- /dev/null +++ b/Userland/Applications/MapsSettings/main.cpp @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023, Bastiaan van der Plaat + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "MapsSettingsWidget.h" +#include +#include +#include +#include +#include +#include + +ErrorOr serenity_main(Main::Arguments arguments) +{ + TRY(Core::System::pledge("stdio recvfd sendfd rpath unix")); + + auto app = TRY(GUI::Application::create(arguments)); + + TRY(Core::System::unveil("/res", "r")); + TRY(Core::System::unveil("/home", "r")); + TRY(Core::System::unveil("/tmp/session/%sid/portal/config", "rw")); + TRY(Core::System::unveil(nullptr, nullptr)); + + auto app_icon = GUI::Icon::default_icon("app-maps"sv); + auto window = TRY(GUI::SettingsWindow::create("Maps Settings", GUI::SettingsWindow::ShowDefaultsButton::Yes)); + window->set_icon(app_icon.bitmap_for_size(16)); + + (void)TRY(window->add_tab(TRY(MapsSettings::MapsSettingsWidget::create()), "Maps"_string, "maps"sv)); + + window->show(); + return app->exec(); +}