diff --git a/Libraries/LibGUI/AbstractView.cpp b/Libraries/LibGUI/AbstractView.cpp index d51656ff0e..a60f404e31 100644 --- a/Libraries/LibGUI/AbstractView.cpp +++ b/Libraries/LibGUI/AbstractView.cpp @@ -25,7 +25,9 @@ */ #include +#include #include +#include #include #include #include @@ -33,6 +35,7 @@ #include #include #include +#include namespace GUI { @@ -44,6 +47,8 @@ AbstractView::AbstractView() AbstractView::~AbstractView() { + if (m_searching_timer) + m_searching_timer->stop(); if (m_model) m_model->unregister_view({}, *this); } @@ -421,9 +426,13 @@ void AbstractView::set_cursor(ModelIndex index, SelectionUpdate selection_update { if (!model() || !index.is_valid()) { m_cursor_index = {}; + cancel_searching(); return; } + if (!m_cursor_index.is_valid() || model()->parent_index(m_cursor_index) != model()->parent_index(index)) + cancel_searching(); + if (model()->is_valid(index)) { if (selection_update == SelectionUpdate::Set) set_selection(index); @@ -517,7 +526,152 @@ void AbstractView::keydown_event(KeyEvent& event) return; } + if (is_searchable()) { + if (event.key() == KeyCode::Key_Backspace) { + if (is_searching()) { + //if (event.modifiers() == Mod_Ctrl) { + // TODO: delete last word + //} + Utf8View view(m_searching); + size_t n_code_points = view.length(); + if (n_code_points > 1) { + n_code_points--; + StringBuilder sb; + for (auto it = view.begin(); it != view.end(); ++it) { + if (n_code_points == 0) + break; + n_code_points--; + sb.append_code_point(*it); + } + do_search(sb.to_string()); + start_searching_timer(); + } else { + cancel_searching(); + } + + event.accept(); + return; + } + } else if (event.key() == KeyCode::Key_Escape) { + if (is_searching()) { + cancel_searching(); + + event.accept(); + return; + } + } else if (!event.ctrl() && !event.alt() && event.code_point() != 0) { + StringBuilder sb; + sb.append(m_searching); + sb.append_code_point(event.code_point()); + do_search(sb.to_string()); + start_searching_timer(); + + event.accept(); + return; + } + } + Widget::keydown_event(event); } +void AbstractView::cancel_searching() +{ + m_searching = nullptr; + if (m_searching_timer) + m_searching_timer->stop(); + if (m_highlighted_search_index.is_valid()) { + m_highlighted_search_index = {}; + update(); + } +} + +void AbstractView::start_searching_timer() +{ + if (!m_searching_timer) { + m_searching_timer = add(); + m_searching_timer->set_single_shot(true); + m_searching_timer->on_timeout = [this] { + cancel_searching(); + }; + } + m_searching_timer->set_interval(5 * 1000); + m_searching_timer->restart(); +} + +void AbstractView::do_search(String&& searching) +{ + if (searching.is_empty() || !model()) { + cancel_searching(); + return; + } + + auto found_indexes = model()->matches(searching, Model::MatchesFlag::FirstMatchOnly | Model::MatchesFlag::MatchAtStart | Model::MatchesFlag::CaseInsensitive, model()->parent_index(cursor_index())); + if (!found_indexes.is_empty() && found_indexes[0].is_valid()) { + auto& index = found_indexes[0]; + m_highlighted_search_index = index; + m_searching = move(searching); + set_selection(index); + scroll_into_view(index); + update(); + } +} + +bool AbstractView::is_searchable() const +{ + if (!m_searchable || !model()) + return false; + return model()->is_searchable(); +} + +void AbstractView::set_searchable(bool searchable) +{ + if (m_searchable == searchable) + return; + m_searchable = searchable; + if (!m_searchable) + cancel_searching(); +} + +bool AbstractView::is_highlighting_searching(const ModelIndex& index) const +{ + return index == m_highlighted_search_index; +} + +void AbstractView::draw_item_text(Gfx::Painter& painter, const ModelIndex& index, bool is_selected, const Gfx::IntRect& text_rect, const StringView& item_text, const Gfx::Font& font, Gfx::TextAlignment alignment, Gfx::TextElision elision) +{ + Color text_color; + if (is_selected) + text_color = is_focused() ? palette().selection_text() : palette().inactive_selection_text(); + else + text_color = index.data(ModelRole::ForegroundColor).to_color(palette().color(foreground_role())); + if (is_highlighting_searching(index)) { + Utf8View searching_text(searching()); + auto searching_length = searching_text.length(); + + // Highlight the text background first + painter.draw_text([&](const Gfx::IntRect& rect, u32) { + if (searching_length > 0) { + searching_length--; + painter.fill_rect(rect.inflated(0, 2), palette().highlight_searching()); + } + }, + text_rect, item_text, font, alignment, elision); + + // Then draw the text + auto highlight_text_color = palette().highlight_searching_text(); + searching_length = searching_text.length(); + painter.draw_text([&](const Gfx::IntRect& rect, u32 code_point) { + if (searching_length > 0) { + searching_length--; + painter.draw_glyph_or_emoji(rect.location(), code_point, font, highlight_text_color); + } else { + painter.draw_glyph_or_emoji(rect.location(), code_point, font, text_color); + } + }, + text_rect, item_text, font, alignment, elision); + } else { + painter.draw_text(text_rect, item_text, font, alignment, text_color, elision); + } +} + } diff --git a/Libraries/LibGUI/AbstractView.h b/Libraries/LibGUI/AbstractView.h index fd3ec89f50..bf676ff099 100644 --- a/Libraries/LibGUI/AbstractView.h +++ b/Libraries/LibGUI/AbstractView.h @@ -29,6 +29,7 @@ #include #include #include +#include namespace GUI { @@ -68,6 +69,11 @@ public: bool is_editable() const { return m_editable; } void set_editable(bool editable) { m_editable = editable; } + bool is_searching() const { return !m_searching.is_null(); } + + bool is_searchable() const; + void set_searchable(bool); + enum EditTrigger { None = 0, DoubleClicked = 1 << 0, @@ -138,13 +144,22 @@ protected: virtual void remove_selection(const ModelIndex&); virtual void toggle_selection(const ModelIndex&); + void draw_item_text(Gfx::Painter&, const ModelIndex&, bool, const Gfx::IntRect&, const StringView&, const Gfx::Font&, Gfx::TextAlignment, Gfx::TextElision); + virtual void did_scroll() override; void set_hovered_index(const ModelIndex&); void activate(const ModelIndex&); void activate_selected(); void update_edit_widget_position(); + StringView searching() const { return m_searching; } + void cancel_searching(); + void start_searching_timer(); + void do_search(String&&); + bool is_highlighting_searching(const ModelIndex&) const; + bool m_editable { false }; + bool m_searchable { true }; ModelIndex m_edit_index; RefPtr m_edit_widget; Gfx::IntRect m_edit_widget_content_rect; @@ -154,6 +169,7 @@ protected: bool m_might_drag { false }; ModelIndex m_hovered_index; + ModelIndex m_highlighted_search_index; int m_key_column { -1 }; SortOrder m_sort_order; @@ -161,6 +177,8 @@ protected: private: RefPtr m_model; ModelSelection m_selection; + String m_searching; + RefPtr m_searching_timer; ModelIndex m_cursor_index; unsigned m_edit_triggers { EditTrigger::DoubleClicked | EditTrigger::EditKeyPressed }; bool m_activates_on_selection { false }; diff --git a/Libraries/LibGUI/ColumnsView.cpp b/Libraries/LibGUI/ColumnsView.cpp index 780ec69feb..f1951d0a8a 100644 --- a/Libraries/LibGUI/ColumnsView.cpp +++ b/Libraries/LibGUI/ColumnsView.cpp @@ -141,8 +141,7 @@ void ColumnsView::paint_event(PaintEvent& event) icon_rect.right() + 1 + icon_spacing(), row * item_height(), column.width - icon_spacing() - icon_size() - icon_spacing() - icon_spacing() - s_arrow_bitmap_width - icon_spacing(), item_height() }; - auto text = index.data().to_string(); - painter.draw_text(text_rect, text, Gfx::TextAlignment::CenterLeft, text_color); + draw_item_text(painter, index, is_selected_row, text_rect, index.data().to_string(), font_for_index(index), Gfx::TextAlignment::CenterLeft, Gfx::TextElision::None); bool expandable = model()->row_count(index) > 0; if (expandable) { diff --git a/Libraries/LibGUI/FileSystemModel.cpp b/Libraries/LibGUI/FileSystemModel.cpp index f4ed772bd6..dad8a513cc 100644 --- a/Libraries/LibGUI/FileSystemModel.cpp +++ b/Libraries/LibGUI/FileSystemModel.cpp @@ -627,4 +627,21 @@ void FileSystemModel::set_data(const ModelIndex& index, const Variant& data) } } +Vector FileSystemModel::matches(const StringView& searching, unsigned flags, const ModelIndex& index) +{ + Node& node = const_cast(this->node(index)); + node.reify_if_needed(); + Vector found_indexes; + for (auto& child : node.children) { + if (string_matches(child.name, searching, flags)) { + const_cast(child).reify_if_needed(); + found_indexes.append(child.index(Column::Name)); + if (flags & FirstMatchOnly) + break; + } + } + + return found_indexes; +} + } diff --git a/Libraries/LibGUI/FileSystemModel.h b/Libraries/LibGUI/FileSystemModel.h index 2009db5873..203ae3cd98 100644 --- a/Libraries/LibGUI/FileSystemModel.h +++ b/Libraries/LibGUI/FileSystemModel.h @@ -150,7 +150,9 @@ public: virtual bool accepts_drag(const ModelIndex&, const StringView& data_type) override; virtual bool is_column_sortable(int column_index) const override { return column_index != Column::Icon; } virtual bool is_editable(const ModelIndex&) const override; + virtual bool is_searchable() const override { return true; } virtual void set_data(const ModelIndex&, const Variant&) override; + virtual Vector matches(const StringView&, unsigned = MatchesFlag::AllMatching, const ModelIndex& = ModelIndex()) override; static String timestamp_string(time_t timestamp) { diff --git a/Libraries/LibGUI/FilteringProxyModel.cpp b/Libraries/LibGUI/FilteringProxyModel.cpp index a1ca2960cb..622af645aa 100644 --- a/Libraries/LibGUI/FilteringProxyModel.cpp +++ b/Libraries/LibGUI/FilteringProxyModel.cpp @@ -119,4 +119,17 @@ ModelIndex FilteringProxyModel::map(const ModelIndex& index) const return {}; } +bool FilteringProxyModel::is_searchable() const +{ + return m_model.is_searchable(); +} + +Vector FilteringProxyModel::matches(const StringView& searching, unsigned flags, const ModelIndex& index) +{ + auto found_indexes = m_model.matches(searching, flags, index); + for (size_t i = 0; i < found_indexes.size(); i++) + found_indexes[i] = map(found_indexes[i]); + return found_indexes; +} + } diff --git a/Libraries/LibGUI/FilteringProxyModel.h b/Libraries/LibGUI/FilteringProxyModel.h index 422cec1b05..94f53e7d94 100644 --- a/Libraries/LibGUI/FilteringProxyModel.h +++ b/Libraries/LibGUI/FilteringProxyModel.h @@ -48,6 +48,8 @@ public: virtual Variant data(const ModelIndex&, ModelRole = ModelRole::Display) const override; virtual void update() override; virtual ModelIndex index(int row, int column = 0, const ModelIndex& parent = ModelIndex()) const override; + virtual bool is_searchable() const override; + virtual Vector matches(const StringView&, unsigned = MatchesFlag::AllMatching, const ModelIndex& = ModelIndex()) override; void set_filter_term(const StringView& term); diff --git a/Libraries/LibGUI/IconView.cpp b/Libraries/LibGUI/IconView.cpp index 442a7c9fa9..c1679f4391 100644 --- a/Libraries/LibGUI/IconView.cpp +++ b/Libraries/LibGUI/IconView.cpp @@ -25,6 +25,7 @@ */ #include +#include #include #include #include @@ -464,13 +465,8 @@ void IconView::paint_event(PaintEvent& event) } } - Color text_color; - if (item_data.selected) - text_color = is_focused() ? palette().selection_text() : palette().inactive_selection_text(); - else - text_color = item_data.index.data(ModelRole::ForegroundColor).to_color(palette().color(foreground_role())); painter.fill_rect(item_data.text_rect, background_color); - painter.draw_text(item_data.text_rect, item_text.to_string(), font_for_index(item_data.index), Gfx::TextAlignment::Center, text_color, Gfx::TextElision::Right); + draw_item_text(painter, item_data.index, item_data.selected, item_data.text_rect, item_text.to_string(), font_for_index(item_data.index), Gfx::TextAlignment::Center, Gfx::TextElision::Right); if (item_data.index == m_drop_candidate_index) { // FIXME: This visualization is not great, as it's also possible to drop things on the text label.. diff --git a/Libraries/LibGUI/Model.h b/Libraries/LibGUI/Model.h index 7aec5b231a..ed73a16a66 100644 --- a/Libraries/LibGUI/Model.h +++ b/Libraries/LibGUI/Model.h @@ -59,6 +59,13 @@ public: InvalidateAllIndexes = 1 << 0, }; + enum MatchesFlag { + AllMatching = 0, + FirstMatchOnly = 1 << 0, + CaseInsensitive = 1 << 1, + MatchAtStart = 1 << 2, + }; + virtual ~Model(); virtual int row_count(const ModelIndex& = ModelIndex()) const = 0; @@ -70,9 +77,11 @@ public: virtual ModelIndex parent_index(const ModelIndex&) const { return {}; } virtual ModelIndex index(int row, int column = 0, const ModelIndex& parent = ModelIndex()) const; virtual bool is_editable(const ModelIndex&) const { return false; } + virtual bool is_searchable() const { return false; } virtual void set_data(const ModelIndex&, const Variant&) { } virtual int tree_column() const { return 0; } virtual bool accepts_drag(const ModelIndex&, const StringView& data_type); + virtual Vector matches(const StringView&, unsigned = MatchesFlag::AllMatching, const ModelIndex& = ModelIndex()) { return {}; } virtual bool is_column_sortable([[maybe_unused]] int column_index) const { return true; } virtual void sort([[maybe_unused]] int column, SortOrder) { } @@ -97,6 +106,14 @@ protected: void for_each_view(Function); void did_update(unsigned flags = UpdateFlag::InvalidateAllIndexes); + static bool string_matches(const StringView& str, const StringView& needle, unsigned flags) + { + auto case_sensitivity = (flags & CaseInsensitive) ? CaseSensitivity::CaseInsensitive : CaseSensitivity::CaseSensitive; + if (flags & MatchAtStart) + return str.starts_with(needle, case_sensitivity); + return str.contains(needle, case_sensitivity); + } + ModelIndex create_index(int row, int column, const void* data = nullptr) const; private: diff --git a/Libraries/LibGUI/SortingProxyModel.cpp b/Libraries/LibGUI/SortingProxyModel.cpp index 596938a2d8..0ebe169374 100644 --- a/Libraries/LibGUI/SortingProxyModel.cpp +++ b/Libraries/LibGUI/SortingProxyModel.cpp @@ -295,4 +295,17 @@ void SortingProxyModel::set_data(const ModelIndex& proxy_index, const Variant& d source().set_data(map_to_source(proxy_index), data); } +bool SortingProxyModel::is_searchable() const +{ + return source().is_searchable(); +} + +Vector SortingProxyModel::matches(const StringView& searching, unsigned flags, const ModelIndex& proxy_index) +{ + auto found_indexes = source().matches(searching, flags, map_to_source(proxy_index)); + for (size_t i = 0; i < found_indexes.size(); i++) + found_indexes[i] = map_to_proxy(found_indexes[i]); + return found_indexes; +} + } diff --git a/Libraries/LibGUI/SortingProxyModel.h b/Libraries/LibGUI/SortingProxyModel.h index 3b192df6e6..c82e29dc50 100644 --- a/Libraries/LibGUI/SortingProxyModel.h +++ b/Libraries/LibGUI/SortingProxyModel.h @@ -46,7 +46,9 @@ public: virtual ModelIndex parent_index(const ModelIndex&) const override; virtual ModelIndex index(int row, int column, const ModelIndex& parent) const override; virtual bool is_editable(const ModelIndex&) const override; + virtual bool is_searchable() const override; virtual void set_data(const ModelIndex&, const Variant&) override; + virtual Vector matches(const StringView&, unsigned = MatchesFlag::AllMatching, const ModelIndex& = ModelIndex()) override; virtual bool is_column_sortable(int column_index) const override; diff --git a/Libraries/LibGUI/TableView.cpp b/Libraries/LibGUI/TableView.cpp index 65fd141183..3f7cef5073 100644 --- a/Libraries/LibGUI/TableView.cpp +++ b/Libraries/LibGUI/TableView.cpp @@ -127,11 +127,6 @@ void TableView::paint_event(PaintEvent& event) painter.blit(cell_rect.location(), *bitmap, bitmap->rect()); } } else { - Color text_color; - if (is_selected_row) - text_color = is_focused() ? palette().selection_text() : palette().inactive_selection_text(); - else - text_color = cell_index.data(ModelRole::ForegroundColor).to_color(palette().color(foreground_role())); if (!is_selected_row) { auto cell_background_color = cell_index.data(ModelRole::BackgroundColor); if (cell_background_color.is_valid()) @@ -139,7 +134,7 @@ void TableView::paint_event(PaintEvent& event) } auto text_alignment = cell_index.data(ModelRole::TextAlignment).to_text_alignment(Gfx::TextAlignment::CenterLeft); - painter.draw_text(cell_rect, data.to_string(), font_for_index(cell_index), text_alignment, text_color, Gfx::TextElision::Right); + draw_item_text(painter, cell_index, is_selected_row, cell_rect, data.to_string(), font_for_index(cell_index), text_alignment, Gfx::TextElision::Right); } } diff --git a/Libraries/LibGUI/TreeView.cpp b/Libraries/LibGUI/TreeView.cpp index fc79ba0742..7ad0a482cd 100644 --- a/Libraries/LibGUI/TreeView.cpp +++ b/Libraries/LibGUI/TreeView.cpp @@ -318,10 +318,8 @@ void TreeView::paint_event(PaintEvent& event) if (auto bitmap = data.as_icon().bitmap_for_size(16)) painter.blit(cell_rect.location(), *bitmap, bitmap->rect()); } else { - if (!is_selected_row) - text_color = cell_index.data(ModelRole::ForegroundColor).to_color(palette().color(foreground_role())); auto text_alignment = cell_index.data(ModelRole::TextAlignment).to_text_alignment(Gfx::TextAlignment::CenterLeft); - painter.draw_text(cell_rect, data.to_string(), font_for_index(cell_index), text_alignment, text_color, Gfx::TextElision::Right); + draw_item_text(painter, cell_index, is_selected_row, cell_rect, data.to_string(), font_for_index(cell_index), text_alignment, Gfx::TextElision::Right); } } } else { @@ -340,8 +338,7 @@ void TreeView::paint_event(PaintEvent& event) icon_rect.right() + 1 + icon_spacing(), rect.y(), rect.width() - icon_size() - icon_spacing(), rect.height() }; - auto node_text = index.data().to_string(); - painter.draw_text(text_rect, node_text, font_for_index(index), Gfx::TextAlignment::Center, text_color); + draw_item_text(painter, index, is_selected_row, text_rect, index.data().to_string(), font_for_index(index), Gfx::TextAlignment::Center, Gfx::TextElision::None); auto index_at_indent = index; for (int i = indent_level; i > 0; --i) { auto parent_of_index_at_indent = index_at_indent.parent();