mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-11-13 01:59:14 +03:00
56eb45ccbc
The Annotations panel is the most obvious place to perform actions related to annotations, so let's make that possible. :^) The toolbar gets open/save/save-as actions for annotations, and one for adding an annotation. The table itself gets a context menu for editing or deleting the selected annotation.
132 lines
4.7 KiB
C++
132 lines
4.7 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;
|
|
}
|
|
|
|
Optional<Annotation&> AnnotationsModel::get_annotation(GUI::ModelIndex const& index)
|
|
{
|
|
if (index.row() < 0 || index.row() >= row_count())
|
|
return {};
|
|
return m_annotations.at(index.row());
|
|
}
|
|
|
|
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 {};
|
|
}
|