Skip to content

Commit 6aa766f

Browse files
bcolesawesomekling
authored andcommitted
HexEditor: Add 'Find All' option to Find Dialog to find all matches
1 parent d7797c8 commit 6aa766f

File tree

9 files changed

+236
-28
lines changed

9 files changed

+236
-28
lines changed

Userland/Applications/HexEditor/FindDialog.cpp

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ static const Vector<Option> options = {
2929
{ "Hex value", OPTION_HEX_VALUE, true, false },
3030
};
3131

32-
int FindDialog::show(GUI::Window* parent_window, String& out_text, ByteBuffer& out_buffer)
32+
int FindDialog::show(GUI::Window* parent_window, String& out_text, ByteBuffer& out_buffer, bool& find_all)
3333
{
3434
auto dialog = FindDialog::construct();
3535

@@ -39,12 +39,16 @@ int FindDialog::show(GUI::Window* parent_window, String& out_text, ByteBuffer& o
3939
if (!out_text.is_empty() && !out_text.is_null())
4040
dialog->m_text_editor->set_text(out_text);
4141

42+
dialog->m_find_button->set_enabled(!dialog->m_text_editor->text().is_empty());
43+
dialog->m_find_all_button->set_enabled(!dialog->m_text_editor->text().is_empty());
44+
4245
auto result = dialog->exec();
4346

4447
if (result != GUI::Dialog::ExecOK)
4548
return result;
4649

47-
auto processed = dialog->process_input(dialog->text_value(), dialog->selected_option());
50+
auto selected_option = dialog->selected_option();
51+
auto processed = dialog->process_input(dialog->text_value(), selected_option);
4852

4953
out_text = dialog->text_value();
5054

@@ -55,7 +59,9 @@ int FindDialog::show(GUI::Window* parent_window, String& out_text, ByteBuffer& o
5559
out_buffer = move(processed.value());
5660
}
5761

58-
dbgln("Find: value={} option={}", dialog->text_value().characters(), (int)dialog->selected_option());
62+
find_all = dialog->find_all();
63+
64+
dbgln("Find: value={} option={} find_all={}", out_text.characters(), (int)selected_option, find_all);
5965
return result;
6066
}
6167

@@ -97,7 +103,8 @@ FindDialog::FindDialog()
97103
VERIFY_NOT_REACHED();
98104

99105
m_text_editor = *main_widget.find_descendant_of_type_named<GUI::TextBox>("text_editor");
100-
m_ok_button = *main_widget.find_descendant_of_type_named<GUI::Button>("ok_button");
106+
m_find_button = *main_widget.find_descendant_of_type_named<GUI::Button>("find_button");
107+
m_find_all_button = *main_widget.find_descendant_of_type_named<GUI::Button>("find_all_button");
101108
m_cancel_button = *main_widget.find_descendant_of_type_named<GUI::Button>("cancel_button");
102109

103110
auto& radio_container = *main_widget.find_descendant_of_type_named<GUI::Widget>("radio_container");
@@ -117,13 +124,26 @@ FindDialog::FindDialog()
117124
}
118125
}
119126

127+
m_text_editor->on_change = [this]() {
128+
m_find_button->set_enabled(!m_text_editor->text().is_empty());
129+
m_find_all_button->set_enabled(!m_text_editor->text().is_empty());
130+
};
131+
120132
m_text_editor->on_return_pressed = [this] {
121-
m_ok_button->click();
133+
m_find_button->click();
134+
};
135+
136+
m_find_button->on_click = [this](auto) {
137+
auto text = m_text_editor->text();
138+
if (!text.is_empty()) {
139+
m_text_value = text;
140+
done(ExecResult::ExecOK);
141+
}
122142
};
123143

124-
m_ok_button->on_click = [this](auto) {
125-
m_text_value = m_text_editor->text();
126-
done(ExecResult::ExecOK);
144+
m_find_all_button->on_click = [this](auto) {
145+
m_find_all = true;
146+
m_find_button->click();
127147
};
128148

129149
m_cancel_button->on_click = [this](auto) {

Userland/Applications/HexEditor/FindDialog.gml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,13 @@
3434
layout: @GUI::HorizontalBoxLayout
3535

3636
@GUI::Button {
37-
name: "ok_button"
38-
text: "OK"
37+
name: "find_button"
38+
text: "Find"
39+
}
40+
41+
@GUI::Button {
42+
name: "find_all_button"
43+
text: "Find All"
3944
}
4045

4146
@GUI::Button {

Userland/Applications/HexEditor/FindDialog.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,24 @@ class FindDialog : public GUI::Dialog {
2020
C_OBJECT(FindDialog);
2121

2222
public:
23-
static int show(GUI::Window* parent_window, String& out_tex, ByteBuffer& out_buffer);
23+
static int show(GUI::Window* parent_window, String& out_tex, ByteBuffer& out_buffer, bool& find_all);
2424

2525
private:
2626
Result<ByteBuffer, String> process_input(String text_value, OptionId opt);
2727

2828
String text_value() const { return m_text_value; }
2929
OptionId selected_option() const { return m_selected_option; }
30+
bool find_all() const { return m_find_all; }
3031

3132
FindDialog();
3233
virtual ~FindDialog() override;
3334

3435
RefPtr<GUI::TextEditor> m_text_editor;
35-
RefPtr<GUI::Button> m_ok_button;
36+
RefPtr<GUI::Button> m_find_button;
37+
RefPtr<GUI::Button> m_find_all_button;
3638
RefPtr<GUI::Button> m_cancel_button;
3739

40+
bool m_find_all { false };
3841
String m_text_value;
3942
OptionId m_selected_option { OPTION_INVALID };
4043
};

Userland/Applications/HexEditor/HexEditor.cpp

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
#include "HexEditor.h"
8+
#include "SearchResultsModel.h"
89
#include <AK/Debug.h>
910
#include <AK/StringBuilder.h>
1011
#include <LibGUI/Action.h>
@@ -572,6 +573,13 @@ void HexEditor::highlight(int start, int end)
572573
}
573574

574575
int HexEditor::find_and_highlight(ByteBuffer& needle, int start)
576+
{
577+
auto end_of_match = find(needle, start);
578+
highlight(end_of_match - needle.size(), end_of_match);
579+
return end_of_match;
580+
}
581+
582+
int HexEditor::find(ByteBuffer& needle, int start)
575583
{
576584
if (m_buffer.is_empty())
577585
return -1;
@@ -581,10 +589,37 @@ int HexEditor::find_and_highlight(ByteBuffer& needle, int start)
581589
return -1;
582590

583591
int relative_offset = static_cast<const u8*>(raw_offset) - m_buffer.data();
584-
dbgln("find_and_highlight: start={} raw_offset={} relative_offset={}", start, raw_offset, relative_offset);
592+
dbgln("find: start={} raw_offset={} relative_offset={}", start, raw_offset, relative_offset);
585593

586594
auto end_of_match = relative_offset + needle.size();
587-
highlight(relative_offset, end_of_match);
588595

589596
return end_of_match;
590597
}
598+
599+
Vector<Match> HexEditor::find_all(ByteBuffer& needle, int start)
600+
{
601+
if (m_buffer.is_empty())
602+
return {};
603+
604+
Vector<Match> matches;
605+
606+
size_t i = start;
607+
while (i < m_buffer.size()) {
608+
auto raw_offset = memmem(m_buffer.data() + i, m_buffer.size() - i, needle.data(), needle.size());
609+
if (raw_offset == NULL)
610+
break;
611+
612+
int relative_offset = static_cast<const u8*>(raw_offset) - m_buffer.data();
613+
dbgln("find_all: needle={} start={} raw_offset={} relative_offset={}", needle.data(), i, raw_offset, relative_offset);
614+
matches.append({ relative_offset, String::formatted("{}", StringView { needle }.to_string().characters()) });
615+
i = relative_offset + needle.size();
616+
}
617+
618+
if (matches.is_empty())
619+
return {};
620+
621+
auto first_match = matches.at(0);
622+
highlight(first_match.offset, first_match.offset + first_match.value.length());
623+
624+
return matches;
625+
}

Userland/Applications/HexEditor/HexEditor.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
#pragma once
88

9+
#include "SearchResultsModel.h"
910
#include <AK/ByteBuffer.h>
1011
#include <AK/Function.h>
1112
#include <AK/HashMap.h>
@@ -46,7 +47,9 @@ class HexEditor : public GUI::AbstractScrollableWidget {
4647

4748
void set_position(int position);
4849
void highlight(int start, int end);
50+
int find(ByteBuffer& needle, int start = 0);
4951
int find_and_highlight(ByteBuffer& needle, int start = 0);
52+
Vector<Match> find_all(ByteBuffer& needle, int start = 0);
5053
Function<void(int, EditMode, int, int)> on_status_change; // position, edit mode, selection start, selection end
5154
Function<void()> on_change;
5255

Userland/Applications/HexEditor/HexEditorWidget.cpp

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "HexEditorWidget.h"
88
#include "FindDialog.h"
99
#include "GoToOffsetDialog.h"
10+
#include "SearchResultsModel.h"
1011
#include <AK/Optional.h>
1112
#include <AK/StringBuilder.h>
1213
#include <Applications/HexEditor/HexEditorWindowGML.h>
@@ -20,7 +21,9 @@
2021
#include <LibGUI/Menu.h>
2122
#include <LibGUI/Menubar.h>
2223
#include <LibGUI/MessageBox.h>
24+
#include <LibGUI/Model.h>
2325
#include <LibGUI/Statusbar.h>
26+
#include <LibGUI/TableView.h>
2427
#include <LibGUI/TextBox.h>
2528
#include <LibGUI/TextEditor.h>
2629
#include <LibGUI/Toolbar.h>
@@ -40,6 +43,8 @@ HexEditorWidget::HexEditorWidget()
4043
m_toolbar_container = *find_descendant_of_type_named<GUI::ToolbarContainer>("toolbar_container");
4144
m_editor = *find_descendant_of_type_named<HexEditor>("editor");
4245
m_statusbar = *find_descendant_of_type_named<GUI::Statusbar>("statusbar");
46+
m_search_results = *find_descendant_of_type_named<GUI::TableView>("search_results");
47+
m_search_results_container = *find_descendant_of_type_named<GUI::Widget>("search_results_container");
4348

4449
m_editor->on_status_change = [this](int position, HexEditor::EditMode edit_mode, int selection_start, int selection_end) {
4550
m_statusbar->set_text(0, String::formatted("Offset: {:#08X}", position));
@@ -56,6 +61,16 @@ HexEditorWidget::HexEditorWidget()
5661
update_title();
5762
};
5863

64+
m_search_results->set_activates_on_selection(true);
65+
m_search_results->on_activation = [this](const GUI::ModelIndex& index) {
66+
if (!index.is_valid())
67+
return;
68+
auto offset = index.data(GUI::ModelRole::Custom).to_i32();
69+
m_last_found_index = offset;
70+
m_editor->set_position(offset);
71+
m_editor->update();
72+
};
73+
5974
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&) {
6075
if (m_document_dirty) {
6176
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()
117132

118133
m_find_action = GUI::Action::create("&Find", { Mod_Ctrl, Key_F }, Gfx::Bitmap::load_from_file("/res/icons/16x16/find.png"), [&](const GUI::Action&) {
119134
auto old_buffer = m_search_buffer;
120-
if (FindDialog::show(window(), m_search_text, m_search_buffer) == GUI::InputBox::ExecOK) {
135+
bool find_all = false;
136+
if (FindDialog::show(window(), m_search_text, m_search_buffer, find_all) == GUI::InputBox::ExecOK) {
137+
if (find_all) {
138+
auto matches = m_editor->find_all(m_search_buffer, 0);
139+
m_search_results->set_model(*new SearchResultsModel(move(matches)));
140+
m_search_results->update();
141+
142+
if (matches.is_empty()) {
143+
GUI::MessageBox::show(window(), String::formatted("Pattern \"{}\" not found in this file", m_search_text), "Not found", GUI::MessageBox::Type::Warning);
144+
return;
145+
}
146+
147+
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);
148+
set_search_results_visible(true);
149+
} else {
150+
bool same_buffers = false;
151+
if (old_buffer.size() == m_search_buffer.size()) {
152+
if (memcmp(old_buffer.data(), m_search_buffer.data(), old_buffer.size()) == 0)
153+
same_buffers = true;
154+
}
121155

122-
bool same_buffers = false;
123-
if (old_buffer.size() == m_search_buffer.size()) {
124-
if (memcmp(old_buffer.data(), m_search_buffer.data(), old_buffer.size()) == 0)
125-
same_buffers = true;
126-
}
156+
auto result = m_editor->find_and_highlight(m_search_buffer, same_buffers ? last_found_index() : 0);
127157

128-
auto result = m_editor->find_and_highlight(m_search_buffer, same_buffers ? last_found_index() : 0);
158+
if (result == -1) {
159+
GUI::MessageBox::show(window(), String::formatted("Pattern \"{}\" not found in this file", m_search_text), "Not found", GUI::MessageBox::Type::Warning);
160+
return;
161+
}
129162

130-
if (result == -1) {
131-
GUI::MessageBox::show(window(), String::formatted("Pattern \"{}\" not found in this file", m_search_text), "Not found", GUI::MessageBox::Type::Warning);
132-
return;
163+
m_last_found_index = result;
133164
}
165+
134166
m_editor->update();
135-
m_last_found_index = result;
136167
}
137168
});
138169

@@ -156,6 +187,10 @@ HexEditorWidget::HexEditorWidget()
156187
m_config->sync();
157188
});
158189

190+
m_layout_search_results_action = GUI::Action::create_checkable("&Search Results", [&](auto& action) {
191+
set_search_results_visible(action.is_checked());
192+
});
193+
159194
m_toolbar->add_action(*m_new_action);
160195
m_toolbar->add_action(*m_open_action);
161196
m_toolbar->add_action(*m_save_action);
@@ -230,8 +265,9 @@ void HexEditorWidget::initialize_menubar(GUI::Menubar& menubar)
230265
auto show_toolbar = m_config->read_bool_entry("Layout", "ShowToolbar", true);
231266
m_layout_toolbar_action->set_checked(show_toolbar);
232267
m_toolbar_container->set_visible(show_toolbar);
233-
234268
view_menu.add_action(*m_layout_toolbar_action);
269+
view_menu.add_action(*m_layout_search_results_action);
270+
view_menu.add_separator();
235271

236272
auto bytes_per_row = m_config->read_num_entry("Layout", "BytesPerRow", 16);
237273
m_editor->set_bytes_per_row(bytes_per_row);
@@ -294,3 +330,9 @@ bool HexEditorWidget::request_close()
294330
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);
295331
return result == GUI::MessageBox::ExecOK;
296332
}
333+
334+
void HexEditorWidget::set_search_results_visible(bool visible)
335+
{
336+
m_layout_search_results_action->set_checked(visible);
337+
m_search_results_container->set_visible(visible);
338+
}

Userland/Applications/HexEditor/HexEditorWidget.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class HexEditorWidget final : public GUI::Widget {
2929
HexEditorWidget();
3030
void set_path(const LexicalPath& file);
3131
void update_title();
32+
void set_search_results_visible(bool visible);
3233

3334
RefPtr<Core::ConfigFile> m_config;
3435

@@ -50,12 +51,15 @@ class HexEditorWidget final : public GUI::Widget {
5051
RefPtr<GUI::Action> m_find_action;
5152
RefPtr<GUI::Action> m_goto_offset_action;
5253
RefPtr<GUI::Action> m_layout_toolbar_action;
54+
RefPtr<GUI::Action> m_layout_search_results_action;
5355

5456
GUI::ActionGroup m_bytes_per_row_actions;
5557

5658
RefPtr<GUI::Statusbar> m_statusbar;
5759
RefPtr<GUI::Toolbar> m_toolbar;
5860
RefPtr<GUI::ToolbarContainer> m_toolbar_container;
61+
RefPtr<GUI::TableView> m_search_results;
62+
RefPtr<GUI::Widget> m_search_results_container;
5963

6064
bool m_document_dirty { false };
6165
};

Userland/Applications/HexEditor/HexEditorWindow.gml

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,22 @@
1414
}
1515
}
1616

17-
@HexEditor::HexEditor {
18-
name: "editor"
17+
@GUI::HorizontalSplitter {
18+
@HexEditor::HexEditor {
19+
name: "editor"
20+
}
21+
22+
@GUI::Widget {
23+
name: "search_results_container"
24+
visible: false
25+
26+
layout: @GUI::VerticalBoxLayout {
27+
}
28+
29+
@GUI::TableView {
30+
name: "search_results"
31+
}
32+
}
1933
}
2034

2135
@GUI::Statusbar {

0 commit comments

Comments
 (0)