mirror of
https://github.com/SerenityOS/serenity.git
synced 2025-01-23 18:02:05 -05:00
LibGUI: Add IncrementalSearchBanner
Compared to traditional modal search, incremental search begins matching as soon as the user starts typing, highlighting results immediately. This refactors Itamar's work for HackStudio into a common LibGUI widget to be used in all multi-line TextEditors.
This commit is contained in:
parent
3c4a563415
commit
8231bd9bc3
6 changed files with 267 additions and 1 deletions
|
@ -56,6 +56,7 @@ public:
|
|||
|
||||
void scroll_to_top();
|
||||
void scroll_to_bottom();
|
||||
void update_scrollbar_ranges();
|
||||
|
||||
void set_automatic_scrolling_timer(bool active);
|
||||
virtual Gfx::IntPoint automatic_scroll_delta_from_position(Gfx::IntPoint const&) const;
|
||||
|
@ -89,7 +90,6 @@ protected:
|
|||
virtual void on_automatic_scrolling_timer_fired() {};
|
||||
int autoscroll_threshold() const { return m_autoscroll_threshold; }
|
||||
void update_scrollbar_visibility();
|
||||
void update_scrollbar_ranges();
|
||||
|
||||
private:
|
||||
class AbstractScrollableWidgetScrollbar final : public Scrollbar {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
compile_gml(EmojiInputDialog.gml EmojiInputDialogGML.h emoji_input_dialog_gml)
|
||||
compile_gml(FontPickerDialog.gml FontPickerDialogGML.h font_picker_dialog_gml)
|
||||
compile_gml(FilePickerDialog.gml FilePickerDialogGML.h file_picker_dialog_gml)
|
||||
compile_gml(IncrementalSearchBanner.gml IncrementalSearchBannerGML.h incremental_search_banner_gml)
|
||||
compile_gml(PasswordInputDialog.gml PasswordInputDialogGML.h password_input_dialog_gml)
|
||||
|
||||
set(SOURCES
|
||||
|
@ -57,6 +58,7 @@ set(SOURCES
|
|||
Icon.cpp
|
||||
IconView.cpp
|
||||
ImageWidget.cpp
|
||||
IncrementalSearchBanner.cpp
|
||||
INILexer.cpp
|
||||
INISyntaxHighlighter.cpp
|
||||
InputBox.cpp
|
||||
|
@ -133,6 +135,7 @@ set(GENERATED_SOURCES
|
|||
EmojiInputDialogGML.h
|
||||
FilePickerDialogGML.h
|
||||
FontPickerDialogGML.h
|
||||
IncrementalSearchBannerGML.h
|
||||
PasswordInputDialogGML.h
|
||||
)
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ class HorizontalSlider;
|
|||
class Icon;
|
||||
class IconView;
|
||||
class ImageWidget;
|
||||
class IncrementalSearchBanner;
|
||||
class JsonArrayModel;
|
||||
class KeyEvent;
|
||||
class Label;
|
||||
|
|
136
Userland/Libraries/LibGUI/IncrementalSearchBanner.cpp
Normal file
136
Userland/Libraries/LibGUI/IncrementalSearchBanner.cpp
Normal file
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Itamar S. <itamar8910@gmail.com>
|
||||
* Copyright (c) 2022, the SerenityOS developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibGUI/Button.h>
|
||||
#include <LibGUI/IncrementalSearchBanner.h>
|
||||
#include <LibGUI/IncrementalSearchBannerGML.h>
|
||||
#include <LibGUI/Label.h>
|
||||
#include <LibGUI/Layout.h>
|
||||
#include <LibGUI/Painter.h>
|
||||
#include <LibGUI/TextBox.h>
|
||||
#include <LibGfx/Palette.h>
|
||||
|
||||
namespace GUI {
|
||||
|
||||
IncrementalSearchBanner::IncrementalSearchBanner(TextEditor& editor)
|
||||
: m_editor(editor)
|
||||
{
|
||||
load_from_gml(incremental_search_banner_gml);
|
||||
m_index_label = find_descendant_of_type_named<Label>("index_label");
|
||||
|
||||
m_wrap_search_button = find_descendant_of_type_named<Button>("wrap_search_button");
|
||||
m_wrap_search_button->on_checked = [this](auto is_checked) {
|
||||
m_wrap_search = is_checked
|
||||
? TextDocument::SearchShouldWrap::Yes
|
||||
: TextDocument::SearchShouldWrap::No;
|
||||
};
|
||||
|
||||
m_match_case_button = find_descendant_of_type_named<Button>("match_case_button");
|
||||
m_match_case_button->on_checked = [this](auto is_checked) {
|
||||
m_match_case = is_checked;
|
||||
m_editor->reset_search_results();
|
||||
search(TextEditor::SearchDirection::Forward);
|
||||
};
|
||||
|
||||
m_close_button = find_descendant_of_type_named<Button>("close_button");
|
||||
m_close_button->set_text("\xE2\x9D\x8C");
|
||||
m_close_button->on_click = [this](auto) {
|
||||
hide();
|
||||
};
|
||||
|
||||
m_next_button = find_descendant_of_type_named<Button>("next_button");
|
||||
m_next_button->on_click = [this](auto) {
|
||||
search(TextEditor::SearchDirection::Forward);
|
||||
};
|
||||
|
||||
m_previous_button = find_descendant_of_type_named<Button>("previous_button");
|
||||
m_previous_button->on_click = [this](auto) {
|
||||
search(TextEditor::SearchDirection::Backward);
|
||||
};
|
||||
|
||||
m_search_textbox = find_descendant_of_type_named<TextBox>("search_textbox");
|
||||
m_search_textbox->on_change = [this]() {
|
||||
m_editor->reset_search_results();
|
||||
search(TextEditor::SearchDirection::Forward);
|
||||
};
|
||||
|
||||
m_search_textbox->on_return_pressed = [this]() {
|
||||
search(TextEditor::SearchDirection::Forward);
|
||||
};
|
||||
|
||||
m_search_textbox->on_shift_return_pressed = [this]() {
|
||||
search(TextEditor::SearchDirection::Backward);
|
||||
};
|
||||
|
||||
m_search_textbox->on_escape_pressed = [this]() {
|
||||
hide();
|
||||
};
|
||||
}
|
||||
|
||||
void IncrementalSearchBanner::show()
|
||||
{
|
||||
set_visible(true);
|
||||
m_editor->do_layout();
|
||||
m_editor->update_scrollbar_ranges();
|
||||
m_search_textbox->set_focus(true);
|
||||
}
|
||||
|
||||
void IncrementalSearchBanner::hide()
|
||||
{
|
||||
set_visible(false);
|
||||
m_editor->do_layout();
|
||||
m_editor->update_scrollbar_ranges();
|
||||
m_editor->reset_search_results();
|
||||
m_editor->set_focus(true);
|
||||
}
|
||||
|
||||
void IncrementalSearchBanner::search(TextEditor::SearchDirection direction)
|
||||
{
|
||||
auto needle = m_search_textbox->text();
|
||||
if (needle.is_empty()) {
|
||||
m_editor->reset_search_results();
|
||||
m_index_label->set_text(String::empty());
|
||||
return;
|
||||
}
|
||||
|
||||
auto index = m_editor->search_result_index().value_or(0) + 1;
|
||||
if (m_wrap_search == TextDocument::SearchShouldWrap::No) {
|
||||
auto forward = direction == TextEditor::SearchDirection::Forward;
|
||||
if ((index == m_editor->search_results().size() && forward) || (index == 1 && !forward))
|
||||
return;
|
||||
}
|
||||
|
||||
auto result = m_editor->find_text(needle, direction, m_wrap_search, false, m_match_case);
|
||||
index = m_editor->search_result_index().value_or(0) + 1;
|
||||
if (result.is_valid())
|
||||
m_index_label->set_text(String::formatted("{} of {}", index, m_editor->search_results().size()));
|
||||
else
|
||||
m_index_label->set_text(String::empty());
|
||||
}
|
||||
|
||||
void IncrementalSearchBanner::paint_event(PaintEvent& event)
|
||||
{
|
||||
Widget::paint_event(event);
|
||||
|
||||
Painter painter(*this);
|
||||
painter.add_clip_rect(event.rect());
|
||||
painter.draw_line({ 0, rect().bottom() - 1 }, { width(), rect().bottom() - 1 }, palette().threed_shadow1());
|
||||
painter.draw_line({ 0, rect().bottom() }, { width(), rect().bottom() }, palette().threed_shadow2());
|
||||
}
|
||||
|
||||
Optional<UISize> IncrementalSearchBanner::calculated_min_size() const
|
||||
{
|
||||
auto textbox_width = m_search_textbox->effective_min_size().width().as_int();
|
||||
auto textbox_height = m_search_textbox->effective_min_size().height().as_int();
|
||||
auto button_width = m_next_button->effective_min_size().width().as_int();
|
||||
VERIFY(layout());
|
||||
auto margins = layout()->margins();
|
||||
auto spacing = layout()->spacing();
|
||||
return { { margins.left() + textbox_width + spacing + button_width * 2 + margins.right(), textbox_height + margins.top() + margins.bottom() } };
|
||||
}
|
||||
|
||||
}
|
81
Userland/Libraries/LibGUI/IncrementalSearchBanner.gml
Normal file
81
Userland/Libraries/LibGUI/IncrementalSearchBanner.gml
Normal file
|
@ -0,0 +1,81 @@
|
|||
@GUI::Widget {
|
||||
fill_with_background_color: true
|
||||
visible: false
|
||||
layout: @GUI::HorizontalBoxLayout {
|
||||
margins: [4]
|
||||
}
|
||||
|
||||
@GUI::TextBox {
|
||||
name: "search_textbox"
|
||||
max_width: 250
|
||||
preferred_width: "grow"
|
||||
placeholder: "Find"
|
||||
}
|
||||
|
||||
@GUI::Widget {
|
||||
preferred_width: "shrink"
|
||||
layout: @GUI::HorizontalBoxLayout {
|
||||
spacing: 0
|
||||
}
|
||||
|
||||
@GUI::Button {
|
||||
name: "next_button"
|
||||
icon: "/res/icons/16x16/go-down.png"
|
||||
fixed_width: 18
|
||||
button_style: "Coolbar"
|
||||
focus_policy: "NoFocus"
|
||||
}
|
||||
|
||||
@GUI::Button {
|
||||
name: "previous_button"
|
||||
icon: "/res/icons/16x16/go-up.png"
|
||||
fixed_width: 18
|
||||
button_style: "Coolbar"
|
||||
focus_policy: "NoFocus"
|
||||
}
|
||||
}
|
||||
|
||||
@GUI::Label {
|
||||
name: "index_label"
|
||||
text_alignment: "CenterLeft"
|
||||
}
|
||||
|
||||
@GUI::Layout::Spacer {}
|
||||
|
||||
@GUI::Widget {
|
||||
preferred_width: "shrink"
|
||||
layout: @GUI::HorizontalBoxLayout {
|
||||
spacing: 0
|
||||
}
|
||||
|
||||
@GUI::Button {
|
||||
name: "wrap_search_button"
|
||||
fixed_width: 24
|
||||
icon: "/res/icons/16x16/reload.png"
|
||||
tooltip: "Wrap Search"
|
||||
checkable: true
|
||||
checked: true
|
||||
button_style: "Coolbar"
|
||||
focus_policy: "NoFocus"
|
||||
}
|
||||
|
||||
@GUI::Button {
|
||||
name: "match_case_button"
|
||||
fixed_width: 24
|
||||
icon: "/res/icons/16x16/app-font-editor.png"
|
||||
tooltip: "Match Case"
|
||||
checkable: true
|
||||
button_style: "Coolbar"
|
||||
focus_policy: "NoFocus"
|
||||
}
|
||||
}
|
||||
|
||||
@GUI::VerticalSeparator {}
|
||||
|
||||
@GUI::Button {
|
||||
name: "close_button"
|
||||
fixed_size: [15, 16]
|
||||
button_style: "Coolbar"
|
||||
focus_policy: "NoFocus"
|
||||
}
|
||||
}
|
45
Userland/Libraries/LibGUI/IncrementalSearchBanner.h
Normal file
45
Userland/Libraries/LibGUI/IncrementalSearchBanner.h
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright (c) 2022, the SerenityOS developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibGUI/TextEditor.h>
|
||||
#include <LibGUI/Widget.h>
|
||||
|
||||
namespace GUI {
|
||||
|
||||
class IncrementalSearchBanner final : public Widget {
|
||||
C_OBJECT(IncrementalSearchBanner);
|
||||
|
||||
public:
|
||||
virtual ~IncrementalSearchBanner() override = default;
|
||||
|
||||
void show();
|
||||
void hide();
|
||||
|
||||
protected:
|
||||
explicit IncrementalSearchBanner(TextEditor&);
|
||||
|
||||
virtual void paint_event(PaintEvent&) override;
|
||||
virtual Optional<UISize> calculated_min_size() const override;
|
||||
|
||||
private:
|
||||
void search(TextEditor::SearchDirection);
|
||||
|
||||
NonnullRefPtr<TextEditor> m_editor;
|
||||
RefPtr<Button> m_close_button;
|
||||
RefPtr<Button> m_next_button;
|
||||
RefPtr<Button> m_previous_button;
|
||||
RefPtr<Button> m_wrap_search_button;
|
||||
RefPtr<Button> m_match_case_button;
|
||||
RefPtr<Label> m_index_label;
|
||||
RefPtr<TextBox> m_search_textbox;
|
||||
|
||||
TextDocument::SearchShouldWrap m_wrap_search { true };
|
||||
bool m_match_case { false };
|
||||
};
|
||||
|
||||
}
|
Loading…
Add table
Reference in a new issue