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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -66,6 +66,7 @@ class TreeViewItemContainer {
|
||||
enum class IterOptions {
|
||||
None = 0,
|
||||
SkipCollapsed = 1 << 0,
|
||||
SkipFiltered = 1 << 1,
|
||||
|
||||
/* Keep ENUM_OPERATORS() below updated! */
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/** \} */
|
||||
|
||||
@@ -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
|
||||
* \{ */
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user