mirror of
https://github.com/RGBCube/serenity
synced 2025-07-25 16:37:35 +00:00
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.
This commit is contained in:
parent
b2bb7d919d
commit
56caee44e3
4 changed files with 110 additions and 0 deletions
|
@ -5,6 +5,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "AnnotationsModel.h"
|
#include "AnnotationsModel.h"
|
||||||
|
#include <AK/JsonArray.h>
|
||||||
|
#include <AK/JsonObject.h>
|
||||||
|
|
||||||
GUI::Variant AnnotationsModel::data(GUI::ModelIndex const& index, GUI::ModelRole role) const
|
GUI::Variant AnnotationsModel::data(GUI::ModelIndex const& index, GUI::ModelRole role) const
|
||||||
{
|
{
|
||||||
|
@ -66,3 +68,57 @@ Optional<Annotation&> AnnotationsModel::closest_annotation_at(size_t position)
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ErrorOr<void> 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<void> 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<Annotation> new_annotations;
|
||||||
|
TRY(new_annotations.try_ensure_capacity(json_array.size()));
|
||||||
|
TRY(json_array.try_for_each([&](JsonValue const& json_value) -> ErrorOr<void> {
|
||||||
|
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 {};
|
||||||
|
}
|
||||||
|
|
|
@ -65,6 +65,9 @@ public:
|
||||||
void delete_annotation(Annotation const&);
|
void delete_annotation(Annotation const&);
|
||||||
Optional<Annotation&> closest_annotation_at(size_t position);
|
Optional<Annotation&> closest_annotation_at(size_t position);
|
||||||
|
|
||||||
|
ErrorOr<void> save_to_file(Core::File&) const;
|
||||||
|
ErrorOr<void> load_from_file(Core::File&);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Vector<Annotation> m_annotations;
|
Vector<Annotation> m_annotations;
|
||||||
};
|
};
|
||||||
|
|
|
@ -175,6 +175,48 @@ ErrorOr<void> HexEditorWidget::setup()
|
||||||
dbgln("Wrote document to {}", file.filename());
|
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_undo_action = GUI::CommonActions::make_undo_action([&](auto&) {
|
||||||
m_editor->undo();
|
m_editor->undo();
|
||||||
});
|
});
|
||||||
|
@ -446,6 +488,10 @@ ErrorOr<void> HexEditorWidget::initialize_menubar(GUI::Window& window)
|
||||||
file_menu->add_action(*m_save_action);
|
file_menu->add_action(*m_save_action);
|
||||||
file_menu->add_action(*m_save_as_action);
|
file_menu->add_action(*m_save_as_action);
|
||||||
file_menu->add_separator();
|
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) {
|
file_menu->add_recent_files_list([&](auto& action) {
|
||||||
auto path = action.text();
|
auto path = action.text();
|
||||||
auto response = FileSystemAccessClient::Client::the().request_file_read_only_approved(&window, path);
|
auto response = FileSystemAccessClient::Client::the().request_file_read_only_approved(&window, path);
|
||||||
|
@ -626,6 +672,7 @@ void HexEditorWidget::open_file(ByteString const& filename, NonnullOwnPtr<Core::
|
||||||
m_editor->open_file(move(file));
|
m_editor->open_file(move(file));
|
||||||
set_path(filename);
|
set_path(filename);
|
||||||
initialize_annotations_model();
|
initialize_annotations_model();
|
||||||
|
m_annotations_path = "";
|
||||||
GUI::Application::the()->set_most_recently_open_file(filename);
|
GUI::Application::the()->set_most_recently_open_file(filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,6 +53,7 @@ private:
|
||||||
ByteString m_path;
|
ByteString m_path;
|
||||||
ByteString m_name;
|
ByteString m_name;
|
||||||
ByteString m_extension;
|
ByteString m_extension;
|
||||||
|
ByteString m_annotations_path;
|
||||||
|
|
||||||
int m_goto_history { 0 };
|
int m_goto_history { 0 };
|
||||||
String m_search_text;
|
String m_search_text;
|
||||||
|
@ -64,6 +65,9 @@ private:
|
||||||
RefPtr<GUI::Action> m_open_action;
|
RefPtr<GUI::Action> m_open_action;
|
||||||
RefPtr<GUI::Action> m_save_action;
|
RefPtr<GUI::Action> m_save_action;
|
||||||
RefPtr<GUI::Action> m_save_as_action;
|
RefPtr<GUI::Action> m_save_as_action;
|
||||||
|
RefPtr<GUI::Action> m_open_annotations_action;
|
||||||
|
RefPtr<GUI::Action> m_save_annotations_action;
|
||||||
|
RefPtr<GUI::Action> m_save_annotations_as_action;
|
||||||
|
|
||||||
RefPtr<GUI::Action> m_undo_action;
|
RefPtr<GUI::Action> m_undo_action;
|
||||||
RefPtr<GUI::Action> m_redo_action;
|
RefPtr<GUI::Action> m_redo_action;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue