Kaynağa Gözat

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.
thankyouverycool 2 yıl önce
ebeveyn
işleme
8231bd9bc3

+ 1 - 1
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 {

+ 3 - 0
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
 )
 

+ 1 - 0
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;

+ 136 - 0
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() } };
+}
+
+}

+ 81 - 0
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"
+    }
+}

+ 45 - 0
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 };
+};
+
+}