From 56caee44e3191577bb9bdd0425b403cbc77fb7f2 Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Tue, 30 Jan 2024 12:33:36 +0000 Subject: [PATCH] HexEditor: Save and load annotations to/from JSON files This is a fairly simple JSON format: A single array, containing objects, with the Annotation fields as key:value pairs. When reading a file, we let invalid or missing keys fall back to the default values. This is mostly intended to set a pattern so that if we add new fields in the future, we won't fail to load old annotations files. If loading the file fails though, we keep the previously loaded set of annotations. --- .../HexEditor/AnnotationsModel.cpp | 56 +++++++++++++++++++ .../Applications/HexEditor/AnnotationsModel.h | 3 + .../HexEditor/HexEditorWidget.cpp | 47 ++++++++++++++++ .../Applications/HexEditor/HexEditorWidget.h | 4 ++ 4 files changed, 110 insertions(+) diff --git a/Userland/Applications/HexEditor/AnnotationsModel.cpp b/Userland/Applications/HexEditor/AnnotationsModel.cpp index c0b37ec2ce..c717b0d169 100644 --- a/Userland/Applications/HexEditor/AnnotationsModel.cpp +++ b/Userland/Applications/HexEditor/AnnotationsModel.cpp @@ -5,6 +5,8 @@ */ #include "AnnotationsModel.h" +#include +#include GUI::Variant AnnotationsModel::data(GUI::ModelIndex const& index, GUI::ModelRole role) const { @@ -66,3 +68,57 @@ Optional AnnotationsModel::closest_annotation_at(size_t position) return result; } + +ErrorOr AnnotationsModel::save_to_file(Core::File& file) const +{ + JsonArray array {}; + array.ensure_capacity(m_annotations.size()); + + for (auto const& annotation : m_annotations) { + JsonObject object; + object.set("start_offset", annotation.start_offset); + object.set("end_offset", annotation.end_offset); + object.set("background_color", annotation.background_color.to_byte_string()); + object.set("comments", annotation.comments.to_byte_string()); + TRY(array.append(object)); + } + + auto json_string = array.to_byte_string(); + TRY(file.write_until_depleted(json_string.bytes())); + + return {}; +} + +ErrorOr AnnotationsModel::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 annotations from file: Not a JSON array."); + auto& json_array = json.as_array(); + + Vector new_annotations; + TRY(new_annotations.try_ensure_capacity(json_array.size())); + TRY(json_array.try_for_each([&](JsonValue const& json_value) -> ErrorOr { + if (!json_value.is_object()) + return Error::from_string_literal("Failed to read annotation from file: Annotation not a JSON object."); + auto& json_object = json_value.as_object(); + Annotation annotation; + if (auto start_offset = json_object.get_u64("start_offset"sv); start_offset.has_value()) + annotation.start_offset = start_offset.value(); + if (auto end_offset = json_object.get_u64("end_offset"sv); end_offset.has_value()) + annotation.end_offset = end_offset.value(); + if (auto background_color = json_object.get_byte_string("background_color"sv).map([](auto& string) { return Color::from_string(string); }); background_color.has_value()) + annotation.background_color = background_color->value(); + if (auto comments = json_object.get_byte_string("comments"sv); comments.has_value()) + annotation.comments = MUST(String::from_byte_string(comments.value())); + new_annotations.append(annotation); + + return {}; + })); + + m_annotations = move(new_annotations); + invalidate(); + return {}; +} diff --git a/Userland/Applications/HexEditor/AnnotationsModel.h b/Userland/Applications/HexEditor/AnnotationsModel.h index a9c450489a..d40f7fee53 100644 --- a/Userland/Applications/HexEditor/AnnotationsModel.h +++ b/Userland/Applications/HexEditor/AnnotationsModel.h @@ -65,6 +65,9 @@ public: void delete_annotation(Annotation const&); Optional closest_annotation_at(size_t position); + ErrorOr save_to_file(Core::File&) const; + ErrorOr load_from_file(Core::File&); + private: Vector m_annotations; }; diff --git a/Userland/Applications/HexEditor/HexEditorWidget.cpp b/Userland/Applications/HexEditor/HexEditorWidget.cpp index 33fbbc3136..459640e11a 100644 --- a/Userland/Applications/HexEditor/HexEditorWidget.cpp +++ b/Userland/Applications/HexEditor/HexEditorWidget.cpp @@ -175,6 +175,48 @@ ErrorOr HexEditorWidget::setup() dbgln("Wrote document to {}", file.filename()); }); + m_open_annotations_action = GUI::Action::create("Load Annotations...", Gfx::Bitmap::load_from_file("/res/icons/16x16/open.png"sv).release_value_but_fixme_should_propagate_errors(), [this](auto&) { + auto response = FileSystemAccessClient::Client::the().open_file(window(), + { .window_title = "Load annotations file"sv, + .requested_access = Core::File::OpenMode::Read, + .allowed_file_types = { { GUI::FileTypeFilter { "Annotations files", { { "annotations" } } }, GUI::FileTypeFilter::all_files() } } }); + if (response.is_error()) + return; + + auto result = m_editor->document().annotations().load_from_file(response.value().stream()); + if (result.is_error()) { + GUI::MessageBox::show(window(), ByteString::formatted("Unable to load annotations: {}\n"sv, result.error()), "Error"sv, GUI::MessageBox::Type::Error); + return; + } + m_annotations_path = response.value().filename(); + }); + m_open_annotations_action->set_status_tip("Load annotations from a file"_string); + + m_save_annotations_action = GUI::Action::create("Save Annotations", Gfx::Bitmap::load_from_file("/res/icons/16x16/save.png"sv).release_value_but_fixme_should_propagate_errors(), [&](auto&) { + if (m_annotations_path.is_empty()) + return m_save_annotations_as_action->activate(); + + auto response = FileSystemAccessClient::Client::the().request_file(window(), m_annotations_path, Core::File::OpenMode::Write | Core::File::OpenMode::Truncate); + if (response.is_error()) + return; + auto file = response.release_value(); + if (auto result = m_editor->document().annotations().save_to_file(file.stream()); result.is_error()) { + GUI::MessageBox::show(window(), ByteString::formatted("Unable to save annotations file: {}\n"sv, result.error()), "Error"sv, GUI::MessageBox::Type::Error); + } + }); + m_save_annotations_action->set_status_tip("Save annotations to a file"_string); + + m_save_annotations_as_action = GUI::Action::create("Save Annotations As...", Gfx::Bitmap::load_from_file("/res/icons/16x16/save-as.png"sv).release_value_but_fixme_should_propagate_errors(), [&](auto&) { + auto response = FileSystemAccessClient::Client::the().save_file(window(), m_name, "annotations"sv, Core::File::OpenMode::Write | Core::File::OpenMode::Truncate); + if (response.is_error()) + return; + auto file = response.release_value(); + if (auto result = m_editor->document().annotations().save_to_file(file.stream()); result.is_error()) { + GUI::MessageBox::show(window(), ByteString::formatted("Unable to save annotations file: {}\n"sv, result.error()), "Error"sv, GUI::MessageBox::Type::Error); + } + }); + m_save_annotations_as_action->set_status_tip("Save annotations to a file with a new name"_string); + m_undo_action = GUI::CommonActions::make_undo_action([&](auto&) { m_editor->undo(); }); @@ -446,6 +488,10 @@ ErrorOr HexEditorWidget::initialize_menubar(GUI::Window& window) file_menu->add_action(*m_save_action); file_menu->add_action(*m_save_as_action); file_menu->add_separator(); + file_menu->add_action(*m_open_annotations_action); + file_menu->add_action(*m_save_annotations_action); + file_menu->add_action(*m_save_annotations_as_action); + file_menu->add_separator(); file_menu->add_recent_files_list([&](auto& action) { auto path = action.text(); auto response = FileSystemAccessClient::Client::the().request_file_read_only_approved(&window, path); @@ -626,6 +672,7 @@ void HexEditorWidget::open_file(ByteString const& filename, NonnullOwnPtropen_file(move(file)); set_path(filename); initialize_annotations_model(); + m_annotations_path = ""; GUI::Application::the()->set_most_recently_open_file(filename); } diff --git a/Userland/Applications/HexEditor/HexEditorWidget.h b/Userland/Applications/HexEditor/HexEditorWidget.h index 9227647e73..176143c8c9 100644 --- a/Userland/Applications/HexEditor/HexEditorWidget.h +++ b/Userland/Applications/HexEditor/HexEditorWidget.h @@ -53,6 +53,7 @@ private: ByteString m_path; ByteString m_name; ByteString m_extension; + ByteString m_annotations_path; int m_goto_history { 0 }; String m_search_text; @@ -64,6 +65,9 @@ private: RefPtr m_open_action; RefPtr m_save_action; RefPtr m_save_as_action; + RefPtr m_open_annotations_action; + RefPtr m_save_annotations_action; + RefPtr m_save_annotations_as_action; RefPtr m_undo_action; RefPtr m_redo_action;