diff --git a/Userland/Applications/HexEditor/AnnotationsModel.cpp b/Userland/Applications/HexEditor/AnnotationsModel.cpp new file mode 100644 index 0000000000..c0b37ec2ce --- /dev/null +++ b/Userland/Applications/HexEditor/AnnotationsModel.cpp @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2024, Sam Atkins + * + * 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 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 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; +} diff --git a/Userland/Applications/HexEditor/AnnotationsModel.h b/Userland/Applications/HexEditor/AnnotationsModel.h new file mode 100644 index 0000000000..a9c450489a --- /dev/null +++ b/Userland/Applications/HexEditor/AnnotationsModel.h @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2024, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include + +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 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 closest_annotation_at(size_t position); + +private: + Vector m_annotations; +}; diff --git a/Userland/Applications/HexEditor/CMakeLists.txt b/Userland/Applications/HexEditor/CMakeLists.txt index e5b5b51b04..d63e3a1cb8 100644 --- a/Userland/Applications/HexEditor/CMakeLists.txt +++ b/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 diff --git a/Userland/Applications/HexEditor/EditAnnotationDialog.cpp b/Userland/Applications/HexEditor/EditAnnotationDialog.cpp index 399bcdfa6d..de1e3c42d8 100644 --- a/Userland/Applications/HexEditor/EditAnnotationDialog.cpp +++ b/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); diff --git a/Userland/Applications/HexEditor/HexDocument.cpp b/Userland/Applications/HexEditor/HexDocument.cpp index a8d10d8ab0..72e2c2181c 100644 --- a/Userland/Applications/HexEditor/HexDocument.cpp +++ b/Userland/Applications/HexEditor/HexDocument.cpp @@ -9,6 +9,11 @@ #include #include +HexDocument::HexDocument() + : m_annotations(make_ref_counted()) +{ +} + 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 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 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)) { diff --git a/Userland/Applications/HexEditor/HexDocument.h b/Userland/Applications/HexEditor/HexDocument.h index 87a62fd40e..3b0407c53b 100644 --- a/Userland/Applications/HexEditor/HexDocument.h +++ b/Userland/Applications/HexEditor/HexDocument.h @@ -7,6 +7,7 @@ #pragma once +#include "AnnotationsModel.h" #include #include #include @@ -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 { public: enum class Type { @@ -50,14 +42,13 @@ public: virtual bool is_dirty() const; virtual void clear_changes() = 0; - ReadonlySpan annotations() const { return m_annotations; } - void add_annotation(Annotation); - void delete_annotation(Annotation const&); - Optional closest_annotation_at(size_t position); + AnnotationsModel& annotations() { return *m_annotations; } protected: + HexDocument(); + HashMap m_changes; - Vector m_annotations; + NonnullRefPtr m_annotations; }; class HexDocumentMemory final : public HexDocument { diff --git a/Userland/Applications/HexEditor/HexEditor.cpp b/Userland/Applications/HexEditor/HexEditor.cpp index a545bc5a41..892929d048 100644 --- a/Userland/Applications/HexEditor/HexEditor.cpp +++ b/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(); } } diff --git a/Userland/Applications/HexEditor/HexEditor.h b/Userland/Applications/HexEditor/HexEditor.h index 33c4d89b3b..01daa120dc 100644 --- a/Userland/Applications/HexEditor/HexEditor.h +++ b/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();