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;