From acb4f89a403cbc548d4eeb46b9707a2f5e3d6736 Mon Sep 17 00:00:00 2001 From: Julian Eisel Date: Mon, 12 Jun 2023 11:41:00 +0200 Subject: [PATCH] UI: Add filtering support to UI views No user visible changes expected. Needed for the asset shelf (#102879). Adds a UI operator triggered on Ctrl+F that will attempt to start filtering for the hovered UI view, typically enabling a filter text button. View items can implement their own filter method, there's no default one. Maybe we should add default or re-usable string filtering method though. Filtering is applied after constructing the view and filtered out (as in, invisible) items are kept in storage, so that their state (selection, active, etc.) is preserved. The filtered state is cached in the item, so this is only done once per redraw. --- .../keyconfig/keymap_data/blender_default.py | 2 + .../editors/include/UI_abstract_view.hh | 20 +++++++++ .../blender/editors/include/UI_grid_view.hh | 4 ++ source/blender/editors/include/UI_interface.h | 7 +++ .../blender/editors/include/UI_tree_view.hh | 1 + .../editors/interface/interface_ops.cc | 44 +++++++++++++++++++ .../editors/interface/views/abstract_view.cc | 21 ++++++++- .../interface/views/abstract_view_item.cc | 21 +++++++++ .../editors/interface/views/grid_view.cc | 27 +++++++++++- .../editors/interface/views/tree_view.cc | 15 +++++-- 10 files changed, 155 insertions(+), 7 deletions(-) diff --git a/scripts/presets/keyconfig/keymap_data/blender_default.py b/scripts/presets/keyconfig/keymap_data/blender_default.py index d138101eb68..21856379829 100644 --- a/scripts/presets/keyconfig/keymap_data/blender_default.py +++ b/scripts/presets/keyconfig/keymap_data/blender_default.py @@ -931,6 +931,8 @@ def km_user_interface(_params): ("ui.reset_default_button", {"type": 'BACK_SPACE', "value": 'PRESS'}, {"properties": [("all", True)]}), # UI lists (polls check if there's a UI list under the cursor). ("ui.list_start_filter", {"type": 'F', "value": 'PRESS', "ctrl": True}, None), + # UI views (polls check if there's a UI view under the cursor). + ("ui.view_start_filter", {"type": 'F', "value": 'PRESS', "ctrl": True}, None), ]) return keymap diff --git a/source/blender/editors/include/UI_abstract_view.hh b/source/blender/editors/include/UI_abstract_view.hh index 85d52e34487..af96123009f 100644 --- a/source/blender/editors/include/UI_abstract_view.hh +++ b/source/blender/editors/include/UI_abstract_view.hh @@ -84,6 +84,13 @@ class AbstractView { /** Listen to a notifier, returning true if a redraw is needed. */ virtual bool listen(const wmNotifier &) const; + /** + * Enable filtering. Typically used to enable a filter text button. Triggered on Ctrl+F by + * default. + * \return True when filtering was enabled successfully. + */ + virtual bool begin_filtering(const bContext &C) const; + /** * Makes \a item valid for display in this view. Behavior is undefined for items not registered * with this. @@ -136,6 +143,9 @@ class AbstractViewItem { bool is_active_ = false; bool is_renaming_ = false; + /** Cache filtered state here to avoid having to re-query. */ + mutable std::optional is_filtered_visible_; + public: virtual ~AbstractViewItem() = default; @@ -174,6 +184,10 @@ class AbstractViewItem { */ virtual std::unique_ptr create_drop_target(); + /** Return the result of #is_filtered_visible(), but ensure the result is cached so it's only + * queried once per redraw. */ + bool is_filtered_visible_cached() const; + /** Get the view this item is registered for using #AbstractView::register_item(). */ AbstractView &get_view() const; @@ -217,6 +231,12 @@ class AbstractViewItem { */ virtual void update_from_old(const AbstractViewItem &old); + /** + * \note Do not call this directly to avoid constantly rechecking the filter state. Instead use + * #is_filtered_visible_cached() for querying. + */ + virtual bool is_filtered_visible() const; + /** * Add a text button for renaming the item to \a block. This must be used for the built-in * renaming to work. This button is meant to appear temporarily. It is removed when renaming is diff --git a/source/blender/editors/include/UI_grid_view.hh b/source/blender/editors/include/UI_grid_view.hh index 011fcbf241f..3065bcd707b 100644 --- a/source/blender/editors/include/UI_grid_view.hh +++ b/source/blender/editors/include/UI_grid_view.hh @@ -98,6 +98,8 @@ class AbstractGridView : public AbstractView { protected: Vector> items_; + /** Store this to avoid recomputing. */ + mutable std::optional item_count_filtered_; /** map to lookup items by identifier, used for efficient lookups in * #update_from_old(). */ Map item_map_; @@ -109,6 +111,7 @@ class AbstractGridView : public AbstractView { using ItemIterFn = FunctionRef; void foreach_item(ItemIterFn iter_fn) const; + void foreach_filtered_item(ItemIterFn iter_fn) const; /** * Convenience wrapper constructing the item by forwarding given arguments to the constructor of @@ -126,6 +129,7 @@ class AbstractGridView : public AbstractView { template inline ItemT &add_item(Args &&...args); const GridViewStyle &get_style() const; int get_item_count() const; + int get_item_count_filtered() const; protected: virtual void build_items() = 0; diff --git a/source/blender/editors/include/UI_interface.h b/source/blender/editors/include/UI_interface.h index 39fcaaacdee..0efecd2cd61 100644 --- a/source/blender/editors/include/UI_interface.h +++ b/source/blender/editors/include/UI_interface.h @@ -3269,6 +3269,13 @@ void UI_interface_tag_script_reload(void); /* Support click-drag motion which presses the button and closes a popover (like a menu). */ #define USE_UI_POPOVER_ONCE +/** + * Call the #ui::AbstractView::begin_filtering() function of the view to enable filtering. + * Typically used to enable a filter text button. Triggered on Ctrl+F by default. + * \return True when filtering was enabled successfully. + */ +bool UI_view_begin_filtering(const struct bContext *C, const uiViewHandle *view_handle); + bool UI_view_item_is_interactive(const uiViewItemHandle *item_handle); bool UI_view_item_is_active(const uiViewItemHandle *item_handle); bool UI_view_item_matches(const uiViewItemHandle *a_handle, const uiViewItemHandle *b_handle); diff --git a/source/blender/editors/include/UI_tree_view.hh b/source/blender/editors/include/UI_tree_view.hh index 3726d4db56e..c9552b9c680 100644 --- a/source/blender/editors/include/UI_tree_view.hh +++ b/source/blender/editors/include/UI_tree_view.hh @@ -66,6 +66,7 @@ class TreeViewItemContainer { enum class IterOptions { None = 0, SkipCollapsed = 1 << 0, + SkipFiltered = 1 << 1, /* Keep ENUM_OPERATORS() below updated! */ }; diff --git a/source/blender/editors/interface/interface_ops.cc b/source/blender/editors/interface/interface_ops.cc index 1c847551505..e3b391d373b 100644 --- a/source/blender/editors/interface/interface_ops.cc +++ b/source/blender/editors/interface/interface_ops.cc @@ -43,6 +43,7 @@ #include "RNA_access.h" #include "RNA_define.h" +#include "RNA_enum_types.h" #include "RNA_path.h" #include "RNA_prototypes.h" #include "RNA_types.h" @@ -2341,6 +2342,48 @@ static void UI_OT_list_start_filter(wmOperatorType *ot) /** \} */ +/* -------------------------------------------------------------------- */ +/** \name UI View Start Filter Operator + * \{ */ + +static bool ui_view_focused_poll(bContext *C) +{ + const ARegion *region = CTX_wm_region(C); + if (!region) { + return false; + } + const wmWindow *win = CTX_wm_window(C); + const uiViewHandle *view = UI_region_view_find_at(region, win->eventstate->xy, 0); + + return view != nullptr; +} + +static int ui_view_start_filter_invoke(bContext *C, wmOperator * /*op*/, const wmEvent *event) +{ + const ARegion *region = CTX_wm_region(C); + const uiViewHandle *hovered_view = UI_region_view_find_at(region, event->xy, 0); + + if (!UI_view_begin_filtering(C, hovered_view)) { + return OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH; + } + + return OPERATOR_FINISHED; +} + +static void UI_OT_view_start_filter(wmOperatorType *ot) +{ + ot->name = "View Filter"; + ot->idname = "UI_OT_view_start_filter"; + ot->description = "Start entering filter text for the data-set in focus"; + + ot->invoke = ui_view_start_filter_invoke; + ot->poll = ui_view_focused_poll; + + ot->flag = OPTYPE_INTERNAL; +} + +/** \} */ + /* -------------------------------------------------------------------- */ /** \name UI View Drop Operator * \{ */ @@ -2528,6 +2571,7 @@ void ED_operatortypes_ui(void) WM_operatortype_append(UI_OT_list_start_filter); + WM_operatortype_append(UI_OT_view_start_filter); WM_operatortype_append(UI_OT_view_drop); WM_operatortype_append(UI_OT_view_item_rename); diff --git a/source/blender/editors/interface/views/abstract_view.cc b/source/blender/editors/interface/views/abstract_view.cc index ef82d4a0f8e..c4e5c426834 100644 --- a/source/blender/editors/interface/views/abstract_view.cc +++ b/source/blender/editors/interface/views/abstract_view.cc @@ -10,6 +10,8 @@ #include "UI_abstract_view.hh" +using namespace blender; + namespace blender::ui { void AbstractView::register_item(AbstractViewItem &item) @@ -76,6 +78,11 @@ bool AbstractView::listen(const wmNotifier & /*notifier*/) const return false; } +bool AbstractView::begin_filtering(const bContext & /*C*/) const +{ + return false; +} + /** \} */ /* ---------------------------------------------------------------------- */ @@ -119,16 +126,26 @@ std::optional AbstractView::get_bounds() const /** \} */ +} // namespace blender::ui + /* ---------------------------------------------------------------------- */ /** \name General API functions * \{ */ +namespace blender::ui { + std::unique_ptr view_drop_target(uiViewHandle *view_handle) { AbstractView &view = reinterpret_cast(*view_handle); return view.create_drop_target(); } -/** \} */ - } // namespace blender::ui + +bool UI_view_begin_filtering(const bContext *C, const uiViewHandle *view_handle) +{ + const ui::AbstractView &view = reinterpret_cast(*view_handle); + return view.begin_filtering(*C); +} + +/** \} */ diff --git a/source/blender/editors/interface/views/abstract_view_item.cc b/source/blender/editors/interface/views/abstract_view_item.cc index b36d8d86118..95e4c9ecc16 100644 --- a/source/blender/editors/interface/views/abstract_view_item.cc +++ b/source/blender/editors/interface/views/abstract_view_item.cc @@ -166,6 +166,27 @@ void AbstractViewItem::build_context_menu(bContext & /*C*/, uiLayout & /*column* /** \} */ +/* ---------------------------------------------------------------------- */ +/** \name Filtering + * \{ */ + +bool AbstractViewItem::is_filtered_visible() const +{ + return true; +} + +bool AbstractViewItem::is_filtered_visible_cached() const +{ + if (is_filtered_visible_.has_value()) { + return *is_filtered_visible_; + } + + is_filtered_visible_ = is_filtered_visible(); + return *is_filtered_visible_; +} + +/** \} */ + /* ---------------------------------------------------------------------- */ /** \name Drag 'n Drop * \{ */ diff --git a/source/blender/editors/interface/views/grid_view.cc b/source/blender/editors/interface/views/grid_view.cc index 76a4ef612a2..efd86ac6704 100644 --- a/source/blender/editors/interface/views/grid_view.cc +++ b/source/blender/editors/interface/views/grid_view.cc @@ -44,6 +44,15 @@ void AbstractGridView::foreach_item(ItemIterFn iter_fn) const } } +void AbstractGridView::foreach_filtered_item(ItemIterFn iter_fn) const +{ + for (const auto &item_ptr : items_) { + if (item_ptr->is_filtered_visible_cached()) { + iter_fn(*item_ptr); + } + } +} + AbstractGridViewItem *AbstractGridView::find_matching_item( const AbstractGridViewItem &item_to_match, const AbstractGridView &view_to_search_in) const { @@ -86,6 +95,20 @@ int AbstractGridView::get_item_count() const return items_.size(); } +int AbstractGridView::get_item_count_filtered() const +{ + if (item_count_filtered_) { + return *item_count_filtered_; + } + + int i = 0; + foreach_filtered_item([&i](const auto &) { i++; }); + + BLI_assert(i <= get_item_count()); + item_count_filtered_ = i; + return i; +} + GridViewStyle::GridViewStyle(int width, int height) : tile_width(width), tile_height(height) {} /* ---------------------------------------------------------------------- */ @@ -265,7 +288,7 @@ void BuildOnlyVisibleButtonsHelper::fill_layout_before_visible(uiBlock &block) c void BuildOnlyVisibleButtonsHelper::fill_layout_after_visible(uiBlock &block) const { - const int last_item_idx = grid_view_.get_item_count() - 1; + const int last_item_idx = grid_view_.get_item_count_filtered() - 1; const int last_visible_idx = visible_items_range_.last(); if (last_item_idx > last_visible_idx) { @@ -350,7 +373,7 @@ void GridViewLayoutBuilder::build_from_view(const AbstractGridView &grid_view, int item_idx = 0; uiLayout *row = nullptr; - grid_view.foreach_item([&](AbstractGridViewItem &item) { + grid_view.foreach_filtered_item([&](AbstractGridViewItem &item) { /* Skip if item isn't visible. */ if (!build_visible_helper.is_item_visible(item_idx)) { item_idx++; diff --git a/source/blender/editors/interface/views/tree_view.cc b/source/blender/editors/interface/views/tree_view.cc index 05141a3a50a..1f005c875fe 100644 --- a/source/blender/editors/interface/views/tree_view.cc +++ b/source/blender/editors/interface/views/tree_view.cc @@ -52,7 +52,15 @@ AbstractTreeViewItem &TreeViewItemContainer::add_tree_item( void TreeViewItemContainer::foreach_item_recursive(ItemIterFn iter_fn, IterOptions options) const { for (const auto &child : children_) { - iter_fn(*child); + bool skip = false; + if (bool(options & IterOptions::SkipFiltered) && !child->is_filtered_visible_cached()) { + skip = true; + } + + if (!skip) { + iter_fn(*child); + } + if (bool(options & IterOptions::SkipCollapsed) && child->is_collapsed()) { continue; } @@ -428,7 +436,8 @@ void TreeViewLayoutBuilder::build_from_tree(const AbstractTreeView &tree_view) uiLayoutColumn(box, false); tree_view.foreach_item([this](AbstractTreeViewItem &item) { build_row(item); }, - AbstractTreeView::IterOptions::SkipCollapsed); + AbstractTreeView::IterOptions::SkipCollapsed | + AbstractTreeView::IterOptions::SkipFiltered); UI_block_layout_set_current(&block(), &parent_layout); } @@ -502,7 +511,7 @@ void TreeViewBuilder::ensure_min_rows_items(AbstractTreeView &tree_view) int tot_visible_items = 0; tree_view.foreach_item( [&tot_visible_items](AbstractTreeViewItem & /*item*/) { tot_visible_items++; }, - AbstractTreeView::IterOptions::SkipCollapsed); + AbstractTreeView::IterOptions::SkipCollapsed | AbstractTreeView::IterOptions::SkipFiltered); if (tot_visible_items >= tree_view.min_rows_) { return;