1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-16 17:55:06 +00:00
serenity/Userland/Applications/HexEditor/AnnotationsModel.cpp
Sam Atkins 56caee44e3 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.
2024-01-31 17:38:56 +00:00

124 lines
4.5 KiB
C++

/*
* Copyright (c) 2024, Sam Atkins <atkinssj@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "AnnotationsModel.h"
#include <AK/JsonArray.h>
#include <AK/JsonObject.h>
GUI::Variant AnnotationsModel::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& annotation = m_annotations.at(index.row());
if (role == GUI::ModelRole::Display) {
switch (index.column()) {
case Column::Start:
return MUST(String::formatted("{:#08X}", annotation.start_offset));
case Column::End:
return MUST(String::formatted("{:#08X}", annotation.end_offset));
case Column::Comments:
return annotation.comments;
}
}
switch (to_underlying(role)) {
case to_underlying(CustomRole::StartOffset):
return annotation.start_offset;
case to_underlying(CustomRole::EndOffset):
return annotation.end_offset;
case to_underlying(CustomRole::Comments):
return annotation.comments;
}
return {};
}
void AnnotationsModel::add_annotation(Annotation annotation)
{
m_annotations.append(move(annotation));
invalidate();
}
void AnnotationsModel::delete_annotation(Annotation const& annotation)
{
m_annotations.remove_first_matching([&](auto& other) {
return other == annotation;
});
invalidate();
}
Optional<Annotation&> AnnotationsModel::closest_annotation_at(size_t position)
{
// FIXME: If we end up with a lot of annotations, we'll need to store them and query them in a smarter way.
Optional<Annotation&> result;
for (auto& annotation : m_annotations) {
if (annotation.start_offset <= position && position <= annotation.end_offset) {
// If multiple annotations cover this position, use whichever starts latest. This would be the innermost one
// if they overlap fully rather than partially.
if (!result.has_value() || result->start_offset < annotation.start_offset)
result = annotation;
}
}
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 {};
}