From bd86beb7e41ab1fc8359b53d108fbe6468c67eea Mon Sep 17 00:00:00 2001 From: Bastiaan van der Plaat Date: Wed, 28 Feb 2024 22:02:01 +0100 Subject: [PATCH] Maps: Add FavoritesModel and remove hacky misusage of JSONArrayModel --- Userland/Applications/Maps/CMakeLists.txt | 1 + Userland/Applications/Maps/FavoritesModel.cpp | 121 ++++++++++++++++++ Userland/Applications/Maps/FavoritesModel.h | 53 ++++++++ Userland/Applications/Maps/FavoritesPanel.cpp | 104 ++++++--------- Userland/Applications/Maps/FavoritesPanel.h | 15 +-- Userland/Applications/Maps/MapWidget.h | 2 + Userland/Applications/Maps/main.cpp | 2 +- 7 files changed, 224 insertions(+), 74 deletions(-) create mode 100644 Userland/Applications/Maps/FavoritesModel.cpp create mode 100644 Userland/Applications/Maps/FavoritesModel.h diff --git a/Userland/Applications/Maps/CMakeLists.txt b/Userland/Applications/Maps/CMakeLists.txt index 0700bb4c0b..6494d152ac 100644 --- a/Userland/Applications/Maps/CMakeLists.txt +++ b/Userland/Applications/Maps/CMakeLists.txt @@ -11,6 +11,7 @@ compile_gml(SearchPanel.gml SearchPanelGML.cpp) set(SOURCES main.cpp FavoritesEditDialogGML.cpp + FavoritesModel.cpp FavoritesPanelGML.cpp FavoritesPanel.cpp MapWidget.cpp diff --git a/Userland/Applications/Maps/FavoritesModel.cpp b/Userland/Applications/Maps/FavoritesModel.cpp new file mode 100644 index 0000000000..943606e7b1 --- /dev/null +++ b/Userland/Applications/Maps/FavoritesModel.cpp @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2024, Bastiaan van der Plaat + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "FavoritesModel.h" +#include +#include + +namespace Maps { + +GUI::Variant FavoritesModel::data(GUI::ModelIndex const& index, GUI::ModelRole role) const +{ + if (index.row() < 0 || index.row() >= row_count()) + return {}; + + if (role == GUI::ModelRole::TextAlignment) + return Gfx::TextAlignment::CenterLeft; + + auto const& favorite = m_favorites.at(index.row()); + if (role == GUI::ModelRole::Display) + return ByteString::formatted("{}\n{:.5}, {:.5}", favorite.name, favorite.latlng.latitude, favorite.latlng.longitude); + + return {}; +} + +Optional FavoritesModel::get_favorite(GUI::ModelIndex const& index) +{ + if (index.row() < 0 || index.row() >= row_count()) + return {}; + return m_favorites.at(index.row()); +} + +void FavoritesModel::add_favorite(Favorite favorite) +{ + m_favorites.append(move(favorite)); + invalidate(); +} + +void FavoritesModel::update_favorite(GUI::ModelIndex const& index, Favorite favorite) +{ + if (index.row() < 0 || index.row() >= row_count()) + return; + m_favorites[index.row()] = move(favorite); + invalidate(); +} + +void FavoritesModel::delete_favorite(Favorite const& favorite) +{ + m_favorites.remove_first_matching([&](auto& other) { + return other == favorite; + }); + invalidate(); +} + +ErrorOr FavoritesModel::save_to_file(Core::File& file) const +{ + JsonArray array {}; + array.ensure_capacity(m_favorites.size()); + + for (auto const& favorite : m_favorites) { + JsonObject object; + object.set("name", favorite.name.to_byte_string()); + object.set("latitude", favorite.latlng.latitude); + object.set("longitude", favorite.latlng.longitude); + object.set("zoom", favorite.zoom); + TRY(array.append(object)); + } + + auto json_string = array.to_byte_string(); + TRY(file.write_until_depleted(json_string.bytes())); + return {}; +} + +ErrorOr FavoritesModel::load_from_file(Core::File& file) +{ + auto json_bytes = TRY(file.read_until_eof()); + StringView json_string { json_bytes }; + auto json = TRY(JsonValue::from_string(json_string)); + if (!json.is_array()) + return Error::from_string_literal("Failed to read favorites from file: Not a JSON array."); + auto& json_array = json.as_array(); + + Vector new_favorites; + TRY(new_favorites.try_ensure_capacity(json_array.size())); + TRY(json_array.try_for_each([&](JsonValue const& json_value) -> ErrorOr { + if (!json_value.is_object()) + return {}; + auto& json_object = json_value.as_object(); + + Favorite favorite; + + auto name = json_object.get_byte_string("name"sv); + if (!name.has_value()) + return {}; + favorite.name = MUST(String::from_byte_string(*name)); + + auto latitude = json_object.get_double_with_precision_loss("latitude"sv); + if (!latitude.has_value()) + return {}; + auto longitude = json_object.get_double_with_precision_loss("longitude"sv); + if (!longitude.has_value()) + return {}; + favorite.latlng = { *latitude, *longitude }; + + auto zoom = json_object.get_i32("zoom"sv); + if (!zoom.has_value()) + return {}; + favorite.zoom = *zoom; + + new_favorites.append(favorite); + return {}; + })); + + m_favorites = move(new_favorites); + invalidate(); + return {}; +} + +} diff --git a/Userland/Applications/Maps/FavoritesModel.h b/Userland/Applications/Maps/FavoritesModel.h new file mode 100644 index 0000000000..5b4196187d --- /dev/null +++ b/Userland/Applications/Maps/FavoritesModel.h @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024, Bastiaan van der Plaat + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include "MapWidget.h" +#include + +namespace Maps { + +struct Favorite { + String name; + MapWidget::LatLng latlng; + int zoom; + + bool operator==(Favorite const& other) const = default; +}; + +class FavoritesModel final : public GUI::Model { +public: + static NonnullRefPtr create() + { + return adopt_ref(*new FavoritesModel()); + } + + virtual int row_count(GUI::ModelIndex const& index = GUI::ModelIndex()) const override + { + if (!index.is_valid()) + return m_favorites.size(); + return 0; + } + + virtual int column_count(GUI::ModelIndex const&) const override { return 1; } + + virtual GUI::Variant data(GUI::ModelIndex const& index, GUI::ModelRole role = GUI::ModelRole::Display) const override; + + Vector const& favorites() const { return m_favorites; } + Optional get_favorite(GUI::ModelIndex const&); + void add_favorite(Favorite); + void update_favorite(GUI::ModelIndex const&, Favorite); + void delete_favorite(Favorite const&); + + ErrorOr save_to_file(Core::File&) const; + ErrorOr load_from_file(Core::File&); + +private: + Vector m_favorites; +}; + +} diff --git a/Userland/Applications/Maps/FavoritesPanel.cpp b/Userland/Applications/Maps/FavoritesPanel.cpp index 465eced251..b87ff5f063 100644 --- a/Userland/Applications/Maps/FavoritesPanel.cpp +++ b/Userland/Applications/Maps/FavoritesPanel.cpp @@ -8,6 +8,7 @@ #include "FavoritesEditDialog.h" #include #include +#include #include #include #include @@ -20,31 +21,27 @@ ErrorOr FavoritesPanel::initialize() m_empty_container = *find_descendant_of_type_named("empty_container"); m_favorites_list = *find_descendant_of_type_named("favorites_list"); + m_model = FavoritesModel::create(); + m_favorites_list->set_model(m_model); 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_byte_string(model.index(index.row(), 0).data().to_byte_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() }); + if (auto favorite = m_model->get_favorite(m_favorites_list->selection().first()); favorite.has_value()) + on_selected_favorite_change(*favorite); }; 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())); + MUST(edit_favorite(index)); }, 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(); + if (auto favorite = m_model->get_favorite(index); favorite.has_value()) { + m_model->delete_favorite(*favorite); + favorites_changed(); + } }, this)); m_context_menu->popup(event.screen_position()); @@ -54,48 +51,36 @@ ErrorOr FavoritesPanel::initialize() void FavoritesPanel::load_favorites() { - Vector favorites_fields; - favorites_fields.empend("name", "Name"_string, Gfx::TextAlignment::CenterLeft, [](JsonObject const& object) -> GUI::Variant { - ByteString name = object.get_byte_string("name"sv).release_value(); - double latitude = object.get_double_with_precision_loss("latitude"sv).release_value(); - double longitude = object.get_double_with_precision_loss("longitude"sv).release_value(); - return ByteString::formatted("{}\n{:.5}, {:.5}", name, latitude, longitude); - }); - favorites_fields.empend("latitude", "Latitude"_string, Gfx::TextAlignment::CenterLeft, [](JsonObject const& object) -> GUI::Variant { - return object.get_double_with_precision_loss("latitude"sv).release_value(); - }); - favorites_fields.empend("longitude", "Longitude"_string, Gfx::TextAlignment::CenterLeft, [](JsonObject const& object) -> GUI::Variant { - return object.get_double_with_precision_loss("longitude"sv).release_value(); - }); - favorites_fields.empend("zoom", "Zoom"_string, Gfx::TextAlignment::CenterLeft); - m_favorites_list->set_model(*GUI::JsonArrayModel::create(ByteString::formatted("{}/MapsFavorites.json", Core::StandardPaths::config_directory()), move(favorites_fields))); - m_favorites_list->model()->invalidate(); + if (auto maybe_file = Core::File::open(MUST(String::formatted("{}/MapsFavorites.json", Core::StandardPaths::config_directory())), Core::File::OpenMode::Read); !maybe_file.is_error()) { + auto file = maybe_file.release_value(); + (void)m_model->load_from_file(*file); + } 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_byte_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) +void FavoritesPanel::add_favorite(Favorite favorite) { - auto& model = *static_cast(m_favorites_list->model()); + m_model->add_favorite(move(favorite)); + favorites_changed(); +} + +void FavoritesPanel::delete_favorite(Favorite const& favorite) +{ + m_model->delete_favorite(favorite); + favorites_changed(); +} + +ErrorOr FavoritesPanel::edit_favorite(GUI::ModelIndex const& index) +{ + auto favorite = m_model->get_favorite(index); + if (!favorite.has_value()) + return {}; auto edit_dialog = TRY(GUI::Dialog::try_create(window())); edit_dialog->set_title("Edit Favorite"); @@ -106,19 +91,14 @@ ErrorOr FavoritesPanel::edit_favorite(int row) 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_byte_string().split('\n').at(0)); + name_textbox.set_text(favorite->name); 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()); + favorite->name = MUST(String::from_byte_string(name_textbox.text())); + m_model->update_favorite(index, *favorite); favorites_changed(); edit_dialog->done(GUI::Dialog::ExecResult::OK); }; @@ -135,18 +115,16 @@ ErrorOr FavoritesPanel::edit_favorite(int row) 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); + // Update UI + m_empty_container->set_visible(m_model->row_count() == 0); + m_favorites_list->set_visible(m_model->row_count() > 0); + on_favorites_change(m_model->favorites()); - Vector favorites; - for (int index = 0; index < model.row_count(); index++) { - favorites.append({ MUST(String::from_byte_string(model.index(index, 0).data().to_byte_string())), - { model.index(index, 1).data().as_double(), - model.index(index, 2).data().as_double() }, - model.index(index, 3).data().to_i32() }); + // Save favorites + if (auto maybe_file = Core::File::open(MUST(String::formatted("{}/MapsFavorites.json", Core::StandardPaths::config_directory())), Core::File::OpenMode::Write); !maybe_file.is_error()) { + auto file = maybe_file.release_value(); + MUST(m_model->save_to_file(*file)); } - on_favorites_change(favorites); } } diff --git a/Userland/Applications/Maps/FavoritesPanel.h b/Userland/Applications/Maps/FavoritesPanel.h index dc4c183d06..82ab82cce0 100644 --- a/Userland/Applications/Maps/FavoritesPanel.h +++ b/Userland/Applications/Maps/FavoritesPanel.h @@ -6,9 +6,7 @@ #pragma once -#include "MapWidget.h" -#include -#include +#include "FavoritesModel.h" #include namespace Maps { @@ -17,17 +15,13 @@ class FavoritesPanel final : public GUI::Widget { C_OBJECT(FavoritesPanel) public: - struct Favorite { - String name; - MapWidget::LatLng latlng; - int zoom; - }; static ErrorOr> try_create(); ErrorOr initialize(); void load_favorites(); void reset(); - ErrorOr add_favorite(Favorite const& favorite); + void add_favorite(Favorite favorite); + void delete_favorite(Favorite const& favorite); Function const&)> on_favorites_change; Function on_selected_favorite_change; @@ -36,11 +30,12 @@ protected: FavoritesPanel() = default; private: - ErrorOr edit_favorite(int row); + ErrorOr edit_favorite(GUI::ModelIndex const& index); void favorites_changed(); RefPtr m_empty_container; RefPtr m_favorites_list; + RefPtr m_model; RefPtr m_context_menu; }; diff --git a/Userland/Applications/Maps/MapWidget.h b/Userland/Applications/Maps/MapWidget.h index e2c9e3cb92..ae721bd4c6 100644 --- a/Userland/Applications/Maps/MapWidget.h +++ b/Userland/Applications/Maps/MapWidget.h @@ -27,6 +27,8 @@ public: double latitude; double longitude; + bool operator==(LatLng const& other) const = default; + double distance_to(LatLng const& other) const; }; diff --git a/Userland/Applications/Maps/main.cpp b/Userland/Applications/Maps/main.cpp index eeed1c9304..8ea143950f 100644 --- a/Userland/Applications/Maps/main.cpp +++ b/Userland/Applications/Maps/main.cpp @@ -123,7 +123,7 @@ ErrorOr serenity_main(Main::Arguments arguments) 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() })); + favorites_panel->add_favorite({ "Unnamed place"_string, map_widget.context_menu_latlng(), map_widget.zoom() }); }, window));