Browse Source

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.
Sam Atkins 1 year ago
parent
commit
8cac2e89a9

+ 68 - 0
Userland/Applications/HexEditor/AnnotationsModel.cpp

@@ -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;
+}

+ 70 - 0
Userland/Applications/HexEditor/AnnotationsModel.h

@@ -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;
+};

+ 1 - 0
Userland/Applications/HexEditor/CMakeLists.txt

@@ -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

+ 3 - 1
Userland/Applications/HexEditor/EditAnnotationDialog.cpp

@@ -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);

+ 5 - 28
Userland/Applications/HexEditor/HexDocument.cpp

@@ -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))
 {

+ 5 - 14
Userland/Applications/HexEditor/HexDocument.h

@@ -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 {

+ 3 - 3
Userland/Applications/HexEditor/HexEditor.cpp

@@ -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();
     }
 }

+ 2 - 0
Userland/Applications/HexEditor/HexEditor.h

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