HexEditor: Store annotations in a Model

A model is necessary for displaying a list of them in the UI. We might
as well make that their home.
This commit is contained in:
Sam Atkins 2024-01-30 11:08:41 +00:00 committed by Sam Atkins
parent a54952795a
commit 8cac2e89a9
8 changed files with 157 additions and 46 deletions

View file

@ -0,0 +1,68 @@
/*
* Copyright (c) 2024, Sam Atkins <atkinssj@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "AnnotationsModel.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;
}

View file

@ -0,0 +1,70 @@
/*
* Copyright (c) 2024, Sam Atkins <atkinssj@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Optional.h>
#include <AK/Types.h>
#include <AK/Vector.h>
#include <LibGUI/Model.h>
struct Annotation {
size_t start_offset { 0 };
size_t end_offset { 0 };
Gfx::Color background_color { Color::from_argb(0xfffce94f) };
String comments {};
bool operator==(Annotation const& other) const = default;
};
class AnnotationsModel final : public GUI::Model {
public:
enum Column {
Start,
End,
Comments,
};
enum class CustomRole {
StartOffset = to_underlying(GUI::ModelRole::Custom) + 1,
EndOffset,
Comments,
};
virtual int row_count(GUI::ModelIndex const& index = GUI::ModelIndex()) const override
{
if (!index.is_valid())
return m_annotations.size();
return 0;
}
virtual int column_count(GUI::ModelIndex const& = GUI::ModelIndex()) const override
{
return 3;
}
virtual ErrorOr<String> column_name(int column) const override
{
switch (column) {
case Column::Start:
return "Start"_string;
case Column::End:
return "End"_string;
case Column::Comments:
return "Comments"_string;
}
VERIFY_NOT_REACHED();
}
virtual GUI::Variant data(GUI::ModelIndex const& index, GUI::ModelRole role = GUI::ModelRole::Display) const override;
void add_annotation(Annotation);
void delete_annotation(Annotation const&);
Optional<Annotation&> closest_annotation_at(size_t position);
private:
Vector<Annotation> m_annotations;
};

View file

@ -10,6 +10,7 @@ compile_gml(GoToOffsetWidget.gml GoToOffsetWidgetGML.cpp)
compile_gml(HexEditorWidget.gml HexEditorWidgetGML.cpp)
set(SOURCES
AnnotationsModel.cpp
EditAnnotationDialog.cpp
EditAnnotationWidgetGML.cpp
FindDialog.cpp

View file

@ -104,9 +104,11 @@ EditAnnotationDialog::EditAnnotationDialog(GUI::Window* parent_window, NonnullRe
};
if (m_annotation.has_value()) {
*m_annotation = move(result);
if (m_document)
m_document->annotations().invalidate();
} else {
if (m_document)
m_document->add_annotation(result);
m_document->annotations().add_annotation(result);
}
s_most_recent_color = m_background_color->color();
done(ExecResult::OK);

View file

@ -9,6 +9,11 @@
#include <AK/TypeCasts.h>
#include <LibCore/File.h>
HexDocument::HexDocument()
: m_annotations(make_ref_counted<AnnotationsModel>())
{
}
void HexDocument::set(size_t position, u8 value)
{
auto unchanged_value = get_unchanged(position);
@ -25,34 +30,6 @@ bool HexDocument::is_dirty() const
return m_changes.size() > 0;
}
void HexDocument::add_annotation(Annotation annotation)
{
m_annotations.append(move(annotation));
}
void HexDocument::delete_annotation(Annotation const& annotation)
{
m_annotations.remove_first_matching([&](auto& other) {
return other == annotation;
});
}
Optional<Annotation&> HexDocument::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;
}
HexDocumentMemory::HexDocumentMemory(ByteBuffer&& buffer)
: m_buffer(move(buffer))
{

View file

@ -7,6 +7,7 @@
#pragma once
#include "AnnotationsModel.h"
#include <AK/HashMap.h>
#include <AK/NonnullOwnPtr.h>
#include <AK/String.h>
@ -20,15 +21,6 @@
constexpr Duration COMMAND_COMMIT_TIME = Duration::from_milliseconds(400);
struct Annotation {
size_t start_offset { 0 };
size_t end_offset { 0 };
Gfx::Color background_color { Color::from_argb(0xfffce94f) };
String comments {};
bool operator==(Annotation const& other) const = default;
};
class HexDocument : public Weakable<HexDocument> {
public:
enum class Type {
@ -50,14 +42,13 @@ public:
virtual bool is_dirty() const;
virtual void clear_changes() = 0;
ReadonlySpan<Annotation> annotations() const { return m_annotations; }
void add_annotation(Annotation);
void delete_annotation(Annotation const&);
Optional<Annotation&> closest_annotation_at(size_t position);
AnnotationsModel& annotations() { return *m_annotations; }
protected:
HexDocument();
HashMap<size_t, u8> m_changes;
Vector<Annotation> m_annotations;
NonnullRefPtr<AnnotationsModel> m_annotations;
};
class HexDocumentMemory final : public HexDocument {

View file

@ -347,7 +347,7 @@ void HexEditor::mousemove_event(GUI::MouseEvent& event)
if (maybe_offset_data.has_value()) {
set_override_cursor(Gfx::StandardCursor::IBeam);
m_hovered_annotation = m_document->closest_annotation_at(maybe_offset_data->offset);
m_hovered_annotation = m_document->annotations().closest_annotation_at(maybe_offset_data->offset);
} else {
set_override_cursor(Gfx::StandardCursor::None);
m_hovered_annotation.clear();
@ -631,7 +631,7 @@ void HexEditor::paint_event(GUI::PaintEvent& event)
return;
auto const cell = m_document->get(byte_position);
auto const annotation = m_document->closest_annotation_at(byte_position);
auto const annotation = m_document->annotations().closest_annotation_at(byte_position);
Gfx::IntRect hex_display_rect_high_nibble {
frame_thickness() + offset_margin_width() + j * cell_width() + 2 * m_padding,
@ -933,7 +933,7 @@ void HexEditor::show_delete_annotation_dialog(Annotation& annotation)
{
auto result = GUI::MessageBox::show(window(), "Delete this annotation?"sv, "Delete annotation?"sv, GUI::MessageBox::Type::Question, GUI::MessageBox::InputType::YesNo);
if (result == GUI::Dialog::ExecResult::Yes) {
m_document->delete_annotation(annotation);
m_document->annotations().delete_annotation(annotation);
update();
}
}

View file

@ -72,6 +72,8 @@ public:
void show_edit_annotation_dialog(Annotation&);
void show_delete_annotation_dialog(Annotation&);
HexDocument& document() { return *m_document; }
protected:
HexEditor();