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.
This commit is contained in:
Julian Eisel
2023-06-12 11:41:00 +02:00
parent b8ce3f69b1
commit acb4f89a40
10 changed files with 155 additions and 7 deletions

View File

@@ -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

View File

@@ -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<bool> is_filtered_visible_;
public:
virtual ~AbstractViewItem() = default;
@@ -174,6 +184,10 @@ class AbstractViewItem {
*/
virtual std::unique_ptr<AbstractViewItemDropTarget> 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

View File

@@ -98,6 +98,8 @@ class AbstractGridView : public AbstractView {
protected:
Vector<std::unique_ptr<AbstractGridViewItem>> items_;
/** Store this to avoid recomputing. */
mutable std::optional<int> item_count_filtered_;
/** <identifier, item> map to lookup items by identifier, used for efficient lookups in
* #update_from_old(). */
Map<StringRef, AbstractGridViewItem *> item_map_;
@@ -109,6 +111,7 @@ class AbstractGridView : public AbstractView {
using ItemIterFn = FunctionRef<void(AbstractGridViewItem &)>;
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<class ItemT, typename... Args> 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;

View File

@@ -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);

View File

@@ -66,6 +66,7 @@ class TreeViewItemContainer {
enum class IterOptions {
None = 0,
SkipCollapsed = 1 << 0,
SkipFiltered = 1 << 1,
/* Keep ENUM_OPERATORS() below updated! */
};

View File

@@ -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);

View File

@@ -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<rcti> AbstractView::get_bounds() const
/** \} */
} // namespace blender::ui
/* ---------------------------------------------------------------------- */
/** \name General API functions
* \{ */
namespace blender::ui {
std::unique_ptr<DropTargetInterface> view_drop_target(uiViewHandle *view_handle)
{
AbstractView &view = reinterpret_cast<AbstractView &>(*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<const ui::AbstractView &>(*view_handle);
return view.begin_filtering(*C);
}
/** \} */

View File

@@ -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
* \{ */

View File

@@ -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++;

View File

@@ -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;