diff options
author | Brendan Coles <bcoles@gmail.com> | 2021-05-27 15:51:21 +0000 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2021-05-27 22:57:17 +0200 |
commit | 6aa766f8ca861af1f47935b99d14662c24d7cbd7 (patch) | |
tree | 41e5d7b7c0c20908c85058593661aa085ba891ab /Userland/Applications | |
parent | d7797c8bf8888f00d7e8e9360939112a0dde5b8d (diff) | |
download | serenity-6aa766f8ca861af1f47935b99d14662c24d7cbd7.zip |
HexEditor: Add 'Find All' option to Find Dialog to find all matches
Diffstat (limited to 'Userland/Applications')
-rw-r--r-- | Userland/Applications/HexEditor/FindDialog.cpp | 36 | ||||
-rw-r--r-- | Userland/Applications/HexEditor/FindDialog.gml | 9 | ||||
-rw-r--r-- | Userland/Applications/HexEditor/FindDialog.h | 7 | ||||
-rw-r--r-- | Userland/Applications/HexEditor/HexEditor.cpp | 39 | ||||
-rw-r--r-- | Userland/Applications/HexEditor/HexEditor.h | 3 | ||||
-rw-r--r-- | Userland/Applications/HexEditor/HexEditorWidget.cpp | 66 | ||||
-rw-r--r-- | Userland/Applications/HexEditor/HexEditorWidget.h | 4 | ||||
-rw-r--r-- | Userland/Applications/HexEditor/HexEditorWindow.gml | 18 | ||||
-rw-r--r-- | Userland/Applications/HexEditor/SearchResultsModel.h | 82 |
9 files changed, 236 insertions, 28 deletions
diff --git a/Userland/Applications/HexEditor/FindDialog.cpp b/Userland/Applications/HexEditor/FindDialog.cpp index 84ffbfb65c..1c334c14e0 100644 --- a/Userland/Applications/HexEditor/FindDialog.cpp +++ b/Userland/Applications/HexEditor/FindDialog.cpp @@ -29,7 +29,7 @@ static const Vector<Option> options = { { "Hex value", OPTION_HEX_VALUE, true, false }, }; -int FindDialog::show(GUI::Window* parent_window, String& out_text, ByteBuffer& out_buffer) +int FindDialog::show(GUI::Window* parent_window, String& out_text, ByteBuffer& out_buffer, bool& find_all) { auto dialog = FindDialog::construct(); @@ -39,12 +39,16 @@ int FindDialog::show(GUI::Window* parent_window, String& out_text, ByteBuffer& o if (!out_text.is_empty() && !out_text.is_null()) dialog->m_text_editor->set_text(out_text); + dialog->m_find_button->set_enabled(!dialog->m_text_editor->text().is_empty()); + dialog->m_find_all_button->set_enabled(!dialog->m_text_editor->text().is_empty()); + auto result = dialog->exec(); if (result != GUI::Dialog::ExecOK) return result; - auto processed = dialog->process_input(dialog->text_value(), dialog->selected_option()); + auto selected_option = dialog->selected_option(); + auto processed = dialog->process_input(dialog->text_value(), selected_option); out_text = dialog->text_value(); @@ -55,7 +59,9 @@ int FindDialog::show(GUI::Window* parent_window, String& out_text, ByteBuffer& o out_buffer = move(processed.value()); } - dbgln("Find: value={} option={}", dialog->text_value().characters(), (int)dialog->selected_option()); + find_all = dialog->find_all(); + + dbgln("Find: value={} option={} find_all={}", out_text.characters(), (int)selected_option, find_all); return result; } @@ -97,7 +103,8 @@ FindDialog::FindDialog() VERIFY_NOT_REACHED(); m_text_editor = *main_widget.find_descendant_of_type_named<GUI::TextBox>("text_editor"); - m_ok_button = *main_widget.find_descendant_of_type_named<GUI::Button>("ok_button"); + m_find_button = *main_widget.find_descendant_of_type_named<GUI::Button>("find_button"); + m_find_all_button = *main_widget.find_descendant_of_type_named<GUI::Button>("find_all_button"); m_cancel_button = *main_widget.find_descendant_of_type_named<GUI::Button>("cancel_button"); auto& radio_container = *main_widget.find_descendant_of_type_named<GUI::Widget>("radio_container"); @@ -117,13 +124,26 @@ FindDialog::FindDialog() } } + m_text_editor->on_change = [this]() { + m_find_button->set_enabled(!m_text_editor->text().is_empty()); + m_find_all_button->set_enabled(!m_text_editor->text().is_empty()); + }; + m_text_editor->on_return_pressed = [this] { - m_ok_button->click(); + m_find_button->click(); + }; + + m_find_button->on_click = [this](auto) { + auto text = m_text_editor->text(); + if (!text.is_empty()) { + m_text_value = text; + done(ExecResult::ExecOK); + } }; - m_ok_button->on_click = [this](auto) { - m_text_value = m_text_editor->text(); - done(ExecResult::ExecOK); + m_find_all_button->on_click = [this](auto) { + m_find_all = true; + m_find_button->click(); }; m_cancel_button->on_click = [this](auto) { diff --git a/Userland/Applications/HexEditor/FindDialog.gml b/Userland/Applications/HexEditor/FindDialog.gml index 5486dc7754..329189b1a8 100644 --- a/Userland/Applications/HexEditor/FindDialog.gml +++ b/Userland/Applications/HexEditor/FindDialog.gml @@ -34,8 +34,13 @@ layout: @GUI::HorizontalBoxLayout @GUI::Button { - name: "ok_button" - text: "OK" + name: "find_button" + text: "Find" + } + + @GUI::Button { + name: "find_all_button" + text: "Find All" } @GUI::Button { diff --git a/Userland/Applications/HexEditor/FindDialog.h b/Userland/Applications/HexEditor/FindDialog.h index 5c58b49cca..2cd9cd8182 100644 --- a/Userland/Applications/HexEditor/FindDialog.h +++ b/Userland/Applications/HexEditor/FindDialog.h @@ -20,21 +20,24 @@ class FindDialog : public GUI::Dialog { C_OBJECT(FindDialog); public: - static int show(GUI::Window* parent_window, String& out_tex, ByteBuffer& out_buffer); + static int show(GUI::Window* parent_window, String& out_tex, ByteBuffer& out_buffer, bool& find_all); private: Result<ByteBuffer, String> process_input(String text_value, OptionId opt); String text_value() const { return m_text_value; } OptionId selected_option() const { return m_selected_option; } + bool find_all() const { return m_find_all; } FindDialog(); virtual ~FindDialog() override; RefPtr<GUI::TextEditor> m_text_editor; - RefPtr<GUI::Button> m_ok_button; + RefPtr<GUI::Button> m_find_button; + RefPtr<GUI::Button> m_find_all_button; RefPtr<GUI::Button> m_cancel_button; + bool m_find_all { false }; String m_text_value; OptionId m_selected_option { OPTION_INVALID }; }; diff --git a/Userland/Applications/HexEditor/HexEditor.cpp b/Userland/Applications/HexEditor/HexEditor.cpp index 6304cbb8ee..19dc2e6a86 100644 --- a/Userland/Applications/HexEditor/HexEditor.cpp +++ b/Userland/Applications/HexEditor/HexEditor.cpp @@ -5,6 +5,7 @@ */ #include "HexEditor.h" +#include "SearchResultsModel.h" #include <AK/Debug.h> #include <AK/StringBuilder.h> #include <LibGUI/Action.h> @@ -573,6 +574,13 @@ void HexEditor::highlight(int start, int end) int HexEditor::find_and_highlight(ByteBuffer& needle, int start) { + auto end_of_match = find(needle, start); + highlight(end_of_match - needle.size(), end_of_match); + return end_of_match; +} + +int HexEditor::find(ByteBuffer& needle, int start) +{ if (m_buffer.is_empty()) return -1; @@ -581,10 +589,37 @@ int HexEditor::find_and_highlight(ByteBuffer& needle, int start) return -1; int relative_offset = static_cast<const u8*>(raw_offset) - m_buffer.data(); - dbgln("find_and_highlight: start={} raw_offset={} relative_offset={}", start, raw_offset, relative_offset); + dbgln("find: start={} raw_offset={} relative_offset={}", start, raw_offset, relative_offset); auto end_of_match = relative_offset + needle.size(); - highlight(relative_offset, end_of_match); return end_of_match; } + +Vector<Match> HexEditor::find_all(ByteBuffer& needle, int start) +{ + if (m_buffer.is_empty()) + return {}; + + Vector<Match> matches; + + size_t i = start; + while (i < m_buffer.size()) { + auto raw_offset = memmem(m_buffer.data() + i, m_buffer.size() - i, needle.data(), needle.size()); + if (raw_offset == NULL) + break; + + int relative_offset = static_cast<const u8*>(raw_offset) - m_buffer.data(); + dbgln("find_all: needle={} start={} raw_offset={} relative_offset={}", needle.data(), i, raw_offset, relative_offset); + matches.append({ relative_offset, String::formatted("{}", StringView { needle }.to_string().characters()) }); + i = relative_offset + needle.size(); + } + + if (matches.is_empty()) + return {}; + + auto first_match = matches.at(0); + highlight(first_match.offset, first_match.offset + first_match.value.length()); + + return matches; +} diff --git a/Userland/Applications/HexEditor/HexEditor.h b/Userland/Applications/HexEditor/HexEditor.h index 70da18a0f3..e586d1c25b 100644 --- a/Userland/Applications/HexEditor/HexEditor.h +++ b/Userland/Applications/HexEditor/HexEditor.h @@ -6,6 +6,7 @@ #pragma once +#include "SearchResultsModel.h" #include <AK/ByteBuffer.h> #include <AK/Function.h> #include <AK/HashMap.h> @@ -46,7 +47,9 @@ public: void set_position(int position); void highlight(int start, int end); + int find(ByteBuffer& needle, int start = 0); int find_and_highlight(ByteBuffer& needle, int start = 0); + Vector<Match> find_all(ByteBuffer& needle, int start = 0); Function<void(int, EditMode, int, int)> on_status_change; // position, edit mode, selection start, selection end Function<void()> on_change; diff --git a/Userland/Applications/HexEditor/HexEditorWidget.cpp b/Userland/Applications/HexEditor/HexEditorWidget.cpp index 5af21b9191..144e1e59ee 100644 --- a/Userland/Applications/HexEditor/HexEditorWidget.cpp +++ b/Userland/Applications/HexEditor/HexEditorWidget.cpp @@ -7,6 +7,7 @@ #include "HexEditorWidget.h" #include "FindDialog.h" #include "GoToOffsetDialog.h" +#include "SearchResultsModel.h" #include <AK/Optional.h> #include <AK/StringBuilder.h> #include <Applications/HexEditor/HexEditorWindowGML.h> @@ -20,7 +21,9 @@ #include <LibGUI/Menu.h> #include <LibGUI/Menubar.h> #include <LibGUI/MessageBox.h> +#include <LibGUI/Model.h> #include <LibGUI/Statusbar.h> +#include <LibGUI/TableView.h> #include <LibGUI/TextBox.h> #include <LibGUI/TextEditor.h> #include <LibGUI/Toolbar.h> @@ -40,6 +43,8 @@ HexEditorWidget::HexEditorWidget() m_toolbar_container = *find_descendant_of_type_named<GUI::ToolbarContainer>("toolbar_container"); m_editor = *find_descendant_of_type_named<HexEditor>("editor"); m_statusbar = *find_descendant_of_type_named<GUI::Statusbar>("statusbar"); + m_search_results = *find_descendant_of_type_named<GUI::TableView>("search_results"); + m_search_results_container = *find_descendant_of_type_named<GUI::Widget>("search_results_container"); m_editor->on_status_change = [this](int position, HexEditor::EditMode edit_mode, int selection_start, int selection_end) { m_statusbar->set_text(0, String::formatted("Offset: {:#08X}", position)); @@ -56,6 +61,16 @@ HexEditorWidget::HexEditorWidget() update_title(); }; + m_search_results->set_activates_on_selection(true); + m_search_results->on_activation = [this](const GUI::ModelIndex& index) { + if (!index.is_valid()) + return; + auto offset = index.data(GUI::ModelRole::Custom).to_i32(); + m_last_found_index = offset; + m_editor->set_position(offset); + m_editor->update(); + }; + m_new_action = GUI::Action::create("New", { Mod_Ctrl, Key_N }, Gfx::Bitmap::load_from_file("/res/icons/16x16/new.png"), [this](const GUI::Action&) { if (m_document_dirty) { if (GUI::MessageBox::show(window(), "Save changes to current file first?", "Warning", GUI::MessageBox::Type::Warning, GUI::MessageBox::InputType::OKCancel) != GUI::Dialog::ExecResult::ExecOK) @@ -117,22 +132,38 @@ HexEditorWidget::HexEditorWidget() m_find_action = GUI::Action::create("&Find", { Mod_Ctrl, Key_F }, Gfx::Bitmap::load_from_file("/res/icons/16x16/find.png"), [&](const GUI::Action&) { auto old_buffer = m_search_buffer; - if (FindDialog::show(window(), m_search_text, m_search_buffer) == GUI::InputBox::ExecOK) { + bool find_all = false; + if (FindDialog::show(window(), m_search_text, m_search_buffer, find_all) == GUI::InputBox::ExecOK) { + if (find_all) { + auto matches = m_editor->find_all(m_search_buffer, 0); + m_search_results->set_model(*new SearchResultsModel(move(matches))); + m_search_results->update(); + + if (matches.is_empty()) { + GUI::MessageBox::show(window(), String::formatted("Pattern \"{}\" not found in this file", m_search_text), "Not found", GUI::MessageBox::Type::Warning); + return; + } + + GUI::MessageBox::show(window(), String::formatted("Found {} matches for \"{}\" in this file", matches.size(), m_search_text), String::formatted("{} matches", matches.size()), GUI::MessageBox::Type::Warning); + set_search_results_visible(true); + } else { + bool same_buffers = false; + if (old_buffer.size() == m_search_buffer.size()) { + if (memcmp(old_buffer.data(), m_search_buffer.data(), old_buffer.size()) == 0) + same_buffers = true; + } - bool same_buffers = false; - if (old_buffer.size() == m_search_buffer.size()) { - if (memcmp(old_buffer.data(), m_search_buffer.data(), old_buffer.size()) == 0) - same_buffers = true; - } + auto result = m_editor->find_and_highlight(m_search_buffer, same_buffers ? last_found_index() : 0); - auto result = m_editor->find_and_highlight(m_search_buffer, same_buffers ? last_found_index() : 0); + if (result == -1) { + GUI::MessageBox::show(window(), String::formatted("Pattern \"{}\" not found in this file", m_search_text), "Not found", GUI::MessageBox::Type::Warning); + return; + } - if (result == -1) { - GUI::MessageBox::show(window(), String::formatted("Pattern \"{}\" not found in this file", m_search_text), "Not found", GUI::MessageBox::Type::Warning); - return; + m_last_found_index = result; } + m_editor->update(); - m_last_found_index = result; } }); @@ -156,6 +187,10 @@ HexEditorWidget::HexEditorWidget() m_config->sync(); }); + m_layout_search_results_action = GUI::Action::create_checkable("&Search Results", [&](auto& action) { + set_search_results_visible(action.is_checked()); + }); + m_toolbar->add_action(*m_new_action); m_toolbar->add_action(*m_open_action); m_toolbar->add_action(*m_save_action); @@ -230,8 +265,9 @@ void HexEditorWidget::initialize_menubar(GUI::Menubar& menubar) auto show_toolbar = m_config->read_bool_entry("Layout", "ShowToolbar", true); m_layout_toolbar_action->set_checked(show_toolbar); m_toolbar_container->set_visible(show_toolbar); - view_menu.add_action(*m_layout_toolbar_action); + view_menu.add_action(*m_layout_search_results_action); + view_menu.add_separator(); auto bytes_per_row = m_config->read_num_entry("Layout", "BytesPerRow", 16); m_editor->set_bytes_per_row(bytes_per_row); @@ -294,3 +330,9 @@ bool HexEditorWidget::request_close() auto result = GUI::MessageBox::show(window(), "The file has been modified. Quit without saving?", "Quit without saving?", GUI::MessageBox::Type::Warning, GUI::MessageBox::InputType::OKCancel); return result == GUI::MessageBox::ExecOK; } + +void HexEditorWidget::set_search_results_visible(bool visible) +{ + m_layout_search_results_action->set_checked(visible); + m_search_results_container->set_visible(visible); +} diff --git a/Userland/Applications/HexEditor/HexEditorWidget.h b/Userland/Applications/HexEditor/HexEditorWidget.h index 7344f276a2..14278ab37d 100644 --- a/Userland/Applications/HexEditor/HexEditorWidget.h +++ b/Userland/Applications/HexEditor/HexEditorWidget.h @@ -29,6 +29,7 @@ private: HexEditorWidget(); void set_path(const LexicalPath& file); void update_title(); + void set_search_results_visible(bool visible); RefPtr<Core::ConfigFile> m_config; @@ -50,12 +51,15 @@ private: RefPtr<GUI::Action> m_find_action; RefPtr<GUI::Action> m_goto_offset_action; RefPtr<GUI::Action> m_layout_toolbar_action; + RefPtr<GUI::Action> m_layout_search_results_action; GUI::ActionGroup m_bytes_per_row_actions; RefPtr<GUI::Statusbar> m_statusbar; RefPtr<GUI::Toolbar> m_toolbar; RefPtr<GUI::ToolbarContainer> m_toolbar_container; + RefPtr<GUI::TableView> m_search_results; + RefPtr<GUI::Widget> m_search_results_container; bool m_document_dirty { false }; }; diff --git a/Userland/Applications/HexEditor/HexEditorWindow.gml b/Userland/Applications/HexEditor/HexEditorWindow.gml index 6480759158..e51b8ea3ed 100644 --- a/Userland/Applications/HexEditor/HexEditorWindow.gml +++ b/Userland/Applications/HexEditor/HexEditorWindow.gml @@ -14,8 +14,22 @@ } } - @HexEditor::HexEditor { - name: "editor" + @GUI::HorizontalSplitter { + @HexEditor::HexEditor { + name: "editor" + } + + @GUI::Widget { + name: "search_results_container" + visible: false + + layout: @GUI::VerticalBoxLayout { + } + + @GUI::TableView { + name: "search_results" + } + } } @GUI::Statusbar { diff --git a/Userland/Applications/HexEditor/SearchResultsModel.h b/Userland/Applications/HexEditor/SearchResultsModel.h new file mode 100644 index 0000000000..c70e698bb1 --- /dev/null +++ b/Userland/Applications/HexEditor/SearchResultsModel.h @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2021, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include <AK/Hex.h> +#include <AK/NonnullRefPtr.h> +#include <AK/String.h> +#include <AK/Utf8View.h> +#include <AK/Vector.h> +#include <LibGUI/Model.h> + +struct Match { + int offset; + String value; +}; + +class SearchResultsModel final : public GUI::Model { +public: + enum Column { + Offset, + Value + }; + + explicit SearchResultsModel(const Vector<Match>&& matches) + : m_matches(move(matches)) + { + } + + virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override + { + return m_matches.size(); + } + + virtual int column_count(const GUI::ModelIndex&) const override + { + return 2; + } + + String column_name(int column) const override + { + switch (column) { + case Column::Offset: + return "Offset"; + case Column::Value: + return "Value"; + } + VERIFY_NOT_REACHED(); + } + + virtual GUI::Variant data(const GUI::ModelIndex& index, GUI::ModelRole role) const override + { + if (role == GUI::ModelRole::TextAlignment) + return Gfx::TextAlignment::CenterLeft; + if (role == GUI::ModelRole::Custom) { + auto& match = m_matches.at(index.row()); + return match.offset; + } + if (role == GUI::ModelRole::Display) { + auto& match = m_matches.at(index.row()); + switch (index.column()) { + case Column::Offset: + return String::formatted("{:#08X}", match.offset); + case Column::Value: { + Utf8View utf8_view(match.value); + if (!utf8_view.validate()) + return {}; + return StringView(match.value); + } + } + } + return {}; + } + + virtual void update() override { } + +private: + Vector<Match> m_matches; +}; |