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;