diff options
author | thankyouverycool <66646555+thankyouverycool@users.noreply.github.com> | 2022-11-28 17:58:17 -0500 |
---|---|---|
committer | Sam Atkins <atkinssj@gmail.com> | 2022-11-29 15:39:13 +0000 |
commit | 8231bd9bc3d725f8ba16508522c45e6d349a6a88 (patch) | |
tree | 8470fb96b1064b433fb0714110157a7a29b7c692 /Userland/Libraries/LibGUI | |
parent | 3c4a563415f053e1b51e52938c782137775f182b (diff) | |
download | serenity-8231bd9bc3d725f8ba16508522c45e6d349a6a88.zip |
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.
Diffstat (limited to 'Userland/Libraries/LibGUI')
-rw-r--r-- | Userland/Libraries/LibGUI/AbstractScrollableWidget.h | 2 | ||||
-rw-r--r-- | Userland/Libraries/LibGUI/CMakeLists.txt | 3 | ||||
-rw-r--r-- | Userland/Libraries/LibGUI/Forward.h | 1 | ||||
-rw-r--r-- | Userland/Libraries/LibGUI/IncrementalSearchBanner.cpp | 136 | ||||
-rw-r--r-- | Userland/Libraries/LibGUI/IncrementalSearchBanner.gml | 81 | ||||
-rw-r--r-- | Userland/Libraries/LibGUI/IncrementalSearchBanner.h | 45 |
6 files changed, 267 insertions, 1 deletions
diff --git a/Userland/Libraries/LibGUI/AbstractScrollableWidget.h b/Userland/Libraries/LibGUI/AbstractScrollableWidget.h index 7f16066093..6e33e2ceb4 100644 --- a/Userland/Libraries/LibGUI/AbstractScrollableWidget.h +++ b/Userland/Libraries/LibGUI/AbstractScrollableWidget.h @@ -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 { diff --git a/Userland/Libraries/LibGUI/CMakeLists.txt b/Userland/Libraries/LibGUI/CMakeLists.txt index 65fd641497..ae8fa20e23 100644 --- a/Userland/Libraries/LibGUI/CMakeLists.txt +++ b/Userland/Libraries/LibGUI/CMakeLists.txt @@ -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 ) diff --git a/Userland/Libraries/LibGUI/Forward.h b/Userland/Libraries/LibGUI/Forward.h index 2a11e53283..0ad380d344 100644 --- a/Userland/Libraries/LibGUI/Forward.h +++ b/Userland/Libraries/LibGUI/Forward.h @@ -37,6 +37,7 @@ class HorizontalSlider; class Icon; class IconView; class ImageWidget; +class IncrementalSearchBanner; class JsonArrayModel; class KeyEvent; class Label; diff --git a/Userland/Libraries/LibGUI/IncrementalSearchBanner.cpp b/Userland/Libraries/LibGUI/IncrementalSearchBanner.cpp new file mode 100644 index 0000000000..eeab9a573c --- /dev/null +++ b/Userland/Libraries/LibGUI/IncrementalSearchBanner.cpp @@ -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() } }; +} + +} diff --git a/Userland/Libraries/LibGUI/IncrementalSearchBanner.gml b/Userland/Libraries/LibGUI/IncrementalSearchBanner.gml new file mode 100644 index 0000000000..7019d44edd --- /dev/null +++ b/Userland/Libraries/LibGUI/IncrementalSearchBanner.gml @@ -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" + } +} diff --git a/Userland/Libraries/LibGUI/IncrementalSearchBanner.h b/Userland/Libraries/LibGUI/IncrementalSearchBanner.h new file mode 100644 index 0000000000..5b6e80a269 --- /dev/null +++ b/Userland/Libraries/LibGUI/IncrementalSearchBanner.h @@ -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 }; +}; + +} |