Asset Browser: Rework layout & behavior of catalog tree-view

This reworks how tree rows are constructed in the layout and how they
behave in return.

* To open or collapse a row, the triangle/chevron icon has to be clicked
  now. The previous behavior of allowing to do it on the entire row, but
  only if the item was active already, was just too unusual and felt
  weird.
* Reduce margin between chevron icon and the row label.
* Indent child items without chevron some more, otherwise they feel like
  a row on the same level as their parent, just without chevron.
* Fix renaming button taking entire row width. Respect indentation now.
* Fix double-clicking to rename toggling collapsed state on each click.

Some hacks/special-handling was needed so tree-rows always highlight
while the mouse is hovering them, even if the mouse is actually hovering
another button inside the row.
This commit is contained in:
Julian Eisel 2021-10-07 14:59:43 +02:00
parent 13a28d9e6f
commit c0a5b13b5e
5 changed files with 201 additions and 64 deletions

View File

@ -138,6 +138,8 @@ class TreeViewLayoutBuilder {
private:
/* Created through #TreeViewBuilder. */
TreeViewLayoutBuilder(uiBlock &block);
static void polish_layout(const uiBlock &block);
};
/** \} */
@ -282,8 +284,7 @@ class AbstractTreeViewItem : public TreeViewItemContainer {
void begin_renaming();
void end_renaming();
const AbstractTreeView &get_tree_view() const;
AbstractTreeView &get_tree_view();
AbstractTreeView &get_tree_view() const;
int count_parents() const;
void deactivate();
/**
@ -310,6 +311,8 @@ class AbstractTreeViewItem : public TreeViewItemContainer {
void ensure_parents_uncollapsed();
bool matches_including_parents(const AbstractTreeViewItem &other) const;
uiButTreeRow *tree_row_button();
protected:
/**
* Activates this item, deactivates other items, calls the #AbstractTreeViewItem::on_activate()
@ -323,11 +326,16 @@ class AbstractTreeViewItem : public TreeViewItemContainer {
static void rename_button_fn(bContext *, void *, char *);
static AbstractTreeViewItem *find_tree_item_from_rename_button(const uiBut &but);
static void tree_row_click_fn(struct bContext *, void *, void *);
static void collapse_chevron_click_fn(bContext *, void *but_arg1, void *);
static bool is_collapse_chevron_but(const uiBut *but);
/** See #AbstractTreeView::change_state_delayed() */
void change_state_delayed();
void add_treerow_button(uiBlock &block);
void add_rename_button(uiBlock &block);
void add_indent(uiLayout &row) const;
void add_collapse_chevron(uiBlock &block) const;
void add_rename_button(uiLayout &row);
};
/** \} */
@ -359,9 +367,6 @@ class BasicTreeViewItem : public AbstractTreeViewItem {
*/
ActivateFn activate_fn_;
uiBut *button();
BIFIconID get_draw_icon() const;
private:
static void tree_row_click_fn(struct bContext *C, void *arg1, void *arg2);

View File

@ -965,7 +965,13 @@ static bool ui_but_update_from_old_block(const bContext *C,
found_active = true;
}
else {
const int flag_copy = UI_BUT_DRAG_MULTI;
int flag_copy = UI_BUT_DRAG_MULTI;
/* Stupid special case: The active button may be inside (as in, overlapped on top) a tree-row
* button which we also want to keep highlighted then. */
if (but->type == UI_BTYPE_TREEROW) {
flag_copy |= UI_ACTIVE;
}
but->flag = (but->flag & ~flag_copy) | (oldbut->flag & flag_copy);

View File

@ -4830,14 +4830,23 @@ static int ui_do_but_TREEROW(bContext *C,
uiButTreeRow *tree_row_but = (uiButTreeRow *)but;
BLI_assert(tree_row_but->but.type == UI_BTYPE_TREEROW);
if ((event->type == LEFTMOUSE) && (event->val == KM_DBL_CLICK)) {
button_activate_state(C, but, BUTTON_STATE_EXIT);
if (data->state == BUTTON_STATE_HIGHLIGHT) {
if (event->type == LEFTMOUSE) {
if (event->val == KM_CLICK) {
button_activate_state(C, but, BUTTON_STATE_EXIT);
return WM_UI_HANDLER_BREAK;
}
else if (event->val == KM_DBL_CLICK) {
data->cancel = true;
UI_tree_view_item_begin_rename(tree_row_but->tree_item);
return WM_UI_HANDLER_BREAK;
UI_tree_view_item_begin_rename(tree_row_but->tree_item);
ED_region_tag_redraw(CTX_wm_region(C));
return WM_UI_HANDLER_BREAK;
}
}
}
return ui_do_but_TOG(C, but, data, event);
return WM_UI_HANDLER_CONTINUE;
}
static int ui_do_but_EXIT(bContext *C, uiBut *but, uiHandleButtonData *data, const wmEvent *event)
@ -9683,6 +9692,38 @@ static int ui_handle_list_event(bContext *C, const wmEvent *event, ARegion *regi
return retval;
}
static int ui_handle_tree_hover(const wmEvent *event, const ARegion *region)
{
bool has_treerows = false;
LISTBASE_FOREACH (uiBlock *, block, &region->uiblocks) {
/* Avoid unnecessary work: Tree-rows are assumed to be inside tree-views. */
if (BLI_listbase_is_empty(&block->views)) {
continue;
}
LISTBASE_FOREACH (uiBut *, but, &block->buttons) {
if (but->type == UI_BTYPE_TREEROW) {
but->flag &= ~UI_ACTIVE;
has_treerows = true;
}
}
}
if (!has_treerows) {
/* Avoid unnecessary lookup. */
return WM_UI_HANDLER_CONTINUE;
}
/* Always highlight the hovered tree-row, even if the mouse hovers another button inside of it.
*/
uiBut *hovered_row_but = ui_tree_row_find_mouse_over(region, event->x, event->y);
if (hovered_row_but) {
hovered_row_but->flag |= UI_ACTIVE;
}
return WM_UI_HANDLER_CONTINUE;
}
static void ui_handle_button_return_submenu(bContext *C, const wmEvent *event, uiBut *but)
{
uiHandleButtonData *data = but->active;
@ -11286,6 +11327,10 @@ static int ui_region_handler(bContext *C, const wmEvent *event, void *UNUSED(use
ui_blocks_set_tooltips(region, true);
}
/* Always do this, to reliably update tree-row highlighting, even if the mouse hovers a button
* inside the row (it's an overlapping layout). */
ui_handle_tree_hover(event, region);
/* delayed apply callbacks */
ui_apply_but_funcs_after(C);

View File

@ -19,6 +19,7 @@
*/
#include "DNA_userdef_types.h"
#include "DNA_windowmanager_types.h"
#include "BKE_context.h"
@ -28,6 +29,8 @@
#include "UI_interface.h"
#include "WM_types.h"
#include "UI_tree_view.hh"
namespace blender::ui {
@ -88,7 +91,7 @@ void AbstractTreeView::build_layout_from_tree(const TreeViewLayoutBuilder &build
uiLayout *prev_layout = builder.current_layout();
uiLayout *box = uiLayoutBox(prev_layout);
uiLayoutColumn(box, true);
uiLayoutColumn(box, false);
foreach_item([&builder](AbstractTreeViewItem &item) { builder.build_row(item); },
IterOptions::SkipCollapsed);
@ -172,24 +175,80 @@ void AbstractTreeViewItem::tree_row_click_fn(struct bContext * /*C*/,
void * /*arg2*/)
{
uiButTreeRow *tree_row_but = (uiButTreeRow *)but_arg1;
BasicTreeViewItem &tree_item = reinterpret_cast<BasicTreeViewItem &>(*tree_row_but->tree_item);
AbstractTreeViewItem &tree_item = reinterpret_cast<AbstractTreeViewItem &>(
*tree_row_but->tree_item);
/* Let a click on an opened item activate it, a second click will close it then.
* TODO Should this be for asset catalogs only? */
if (tree_item.is_collapsed() || tree_item.is_active()) {
tree_item.toggle_collapsed();
}
tree_item.activate();
}
void AbstractTreeViewItem::add_treerow_button(uiBlock &block)
{
/* For some reason a width > (UI_UNIT_X * 2) make the layout system use all available width. */
tree_row_but_ = (uiButTreeRow *)uiDefBut(
&block, UI_BTYPE_TREEROW, 0, "", 0, 0, UI_UNIT_X, UI_UNIT_Y, nullptr, 0, 0, 0, 0, "");
&block, UI_BTYPE_TREEROW, 0, "", 0, 0, UI_UNIT_X * 10, UI_UNIT_Y, nullptr, 0, 0, 0, 0, "");
tree_row_but_->tree_item = reinterpret_cast<uiTreeViewItemHandle *>(this);
UI_but_func_set(&tree_row_but_->but, tree_row_click_fn, tree_row_but_, nullptr);
UI_but_treerow_indentation_set(&tree_row_but_->but, count_parents());
}
void AbstractTreeViewItem::add_indent(uiLayout &row) const
{
uiBlock *block = uiLayoutGetBlock(&row);
uiLayout *subrow = uiLayoutRow(&row, true);
uiLayoutSetFixedSize(subrow, true);
const float indent_size = count_parents() * UI_DPI_ICON_SIZE;
uiDefBut(block, UI_BTYPE_SEPR, 0, "", 0, 0, indent_size, 0, NULL, 0.0, 0.0, 0, 0, "");
/* Indent items without collapsing icon some more within their parent. Makes it clear that they
* are actually nested and not just a row at the same level without a chevron. */
if (!is_collapsible() && parent_) {
uiDefBut(block, UI_BTYPE_SEPR, 0, "", 0, 0, 0.2f * UI_UNIT_X, 0, NULL, 0.0, 0.0, 0, 0, "");
}
/* Restore. */
UI_block_layout_set_current(block, &row);
}
void AbstractTreeViewItem::collapse_chevron_click_fn(struct bContext *C,
void * /*but_arg1*/,
void * /*arg2*/)
{
/* There's no data we could pass to this callback. It must be either the button itself or a
* consistent address to match buttons over redraws. So instead of passing it somehow, just
* lookup the hovered item via context here. */
const wmWindow *win = CTX_wm_window(C);
const ARegion *region = CTX_wm_region(C);
uiTreeViewItemHandle *hovered_item_handle = UI_block_tree_view_find_item_at(
region, win->eventstate->x, win->eventstate->y);
AbstractTreeViewItem *hovered_item = reinterpret_cast<AbstractTreeViewItem *>(
hovered_item_handle);
BLI_assert(hovered_item != nullptr);
hovered_item->toggle_collapsed();
}
bool AbstractTreeViewItem::is_collapse_chevron_but(const uiBut *but)
{
return but->type == UI_BTYPE_BUT_TOGGLE && ELEM(but->icon, ICON_TRIA_RIGHT, ICON_TRIA_DOWN) &&
(but->func == collapse_chevron_click_fn);
}
void AbstractTreeViewItem::add_collapse_chevron(uiBlock &block) const
{
if (!is_collapsible()) {
return;
}
const BIFIconID icon = is_collapsed() ? ICON_TRIA_RIGHT : ICON_TRIA_DOWN;
uiBut *but = uiDefIconBut(
&block, UI_BTYPE_BUT_TOGGLE, 0, icon, 0, 0, UI_UNIT_X, UI_UNIT_Y, nullptr, 0, 0, 0, 0, "");
/* Note that we're passing the tree-row button here, not the chevron one. */
UI_but_func_set(but, collapse_chevron_click_fn, nullptr, nullptr);
/* Check if the query for the button matches the created button. */
BLI_assert(is_collapse_chevron_but(but));
}
AbstractTreeViewItem *AbstractTreeViewItem::find_tree_item_from_rename_button(
@ -226,16 +285,23 @@ void AbstractTreeViewItem::rename_button_fn(bContext *UNUSED(C), void *arg, char
item->end_renaming();
}
void AbstractTreeViewItem::add_rename_button(uiBlock &block)
void AbstractTreeViewItem::add_rename_button(uiLayout &row)
{
uiBlock *block = uiLayoutGetBlock(&row);
eUIEmbossType previous_emboss = UI_block_emboss_get(block);
uiLayoutRow(&row, false);
/* Enable emboss for the text button. */
UI_block_emboss_set(block, UI_EMBOSS);
AbstractTreeView &tree_view = get_tree_view();
uiBut *rename_but = uiDefBut(&block,
uiBut *rename_but = uiDefBut(block,
UI_BTYPE_TEXT,
1,
"",
0,
0,
UI_UNIT_X,
UI_UNIT_X * 10,
UI_UNIT_Y,
tree_view.rename_buffer_->data(),
1.0f,
@ -248,12 +314,15 @@ void AbstractTreeViewItem::add_rename_button(uiBlock &block)
* callback is executed. */
UI_but_func_rename_set(rename_but, AbstractTreeViewItem::rename_button_fn, rename_but);
const bContext *evil_C = static_cast<bContext *>(block.evil_C);
const bContext *evil_C = static_cast<bContext *>(block->evil_C);
ARegion *region = CTX_wm_region(evil_C);
/* Returns false if the button was removed. */
if (UI_but_active_only(evil_C, region, &block, rename_but) == false) {
if (UI_but_active_only(evil_C, region, block, rename_but) == false) {
end_renaming();
}
UI_block_emboss_set(block, previous_emboss);
UI_block_layout_set_current(block, &row);
}
void AbstractTreeViewItem::on_activate()
@ -335,12 +404,7 @@ void AbstractTreeViewItem::end_renaming()
tree_view.rename_buffer_ = nullptr;
}
const AbstractTreeView &AbstractTreeViewItem::get_tree_view() const
{
return static_cast<AbstractTreeView &>(*root_);
}
AbstractTreeView &AbstractTreeViewItem::get_tree_view()
AbstractTreeView &AbstractTreeViewItem::get_tree_view() const
{
return static_cast<AbstractTreeView &>(*root_);
}
@ -454,6 +518,11 @@ bool AbstractTreeViewItem::matches_including_parents(const AbstractTreeViewItem
return true;
}
uiButTreeRow *AbstractTreeViewItem::tree_row_button()
{
return tree_row_but_;
}
void AbstractTreeViewItem::change_state_delayed()
{
if (is_active_fn_()) {
@ -481,25 +550,56 @@ TreeViewLayoutBuilder::TreeViewLayoutBuilder(uiBlock &block) : block_(block)
{
}
/**
* Moves the button following the last added chevron closer to the list item.
*
* Iterates backwards over buttons until finding the tree-row button, which is assumed to be the
* first button added for the row, and can act as a delimiter that way.
*/
void TreeViewLayoutBuilder::polish_layout(const uiBlock &block)
{
LISTBASE_FOREACH_BACKWARD (uiBut *, but, &block.buttons) {
if (AbstractTreeViewItem::is_collapse_chevron_but(but) && but->next &&
/* Embossed buttons with padding-less text padding look weird, so don't touch them. */
ELEM(but->next->emboss, UI_EMBOSS_NONE, UI_EMBOSS_NONE_OR_STATUS)) {
UI_but_drawflag_enable(static_cast<uiBut *>(but->next), UI_BUT_NO_TEXT_PADDING);
}
if (but->type == UI_BTYPE_TREEROW) {
break;
}
}
}
void TreeViewLayoutBuilder::build_row(AbstractTreeViewItem &item) const
{
uiLayout *prev_layout = current_layout();
uiLayout *row = uiLayoutRow(prev_layout, false);
uiLayoutOverlap(row);
uiBlock &block_ = block();
uiLayout *prev_layout = current_layout();
eUIEmbossType previous_emboss = UI_block_emboss_get(&block_);
uiLayout *overlap = uiLayoutOverlap(prev_layout);
uiLayoutRow(overlap, false);
/* Every item gets one! Other buttons can be overlapped on top. */
item.add_treerow_button(block_);
/* After adding tree-row button (would disable hover highlighting). */
UI_block_emboss_set(&block_, UI_EMBOSS_NONE);
uiLayout *row = uiLayoutRow(overlap, true);
item.add_indent(*row);
item.add_collapse_chevron(block_);
if (item.is_renaming()) {
item.add_rename_button(block_);
item.add_rename_button(*row);
}
else {
item.build_row(*row);
}
polish_layout(block_);
UI_block_emboss_set(&block_, previous_emboss);
UI_block_layout_set_current(&block_, prev_layout);
}
@ -520,12 +620,9 @@ BasicTreeViewItem::BasicTreeViewItem(StringRef label, BIFIconID icon_) : icon(ic
label_ = label;
}
void BasicTreeViewItem::build_row(uiLayout & /*row*/)
void BasicTreeViewItem::build_row(uiLayout &row)
{
if (BIFIconID icon = get_draw_icon()) {
ui_def_but_icon(&tree_row_but_->but, icon, UI_HAS_ICON);
}
tree_row_but_->but.str = BLI_strdupn(label_.c_str(), label_.length());
uiItemL(&row, label_.c_str(), icon);
}
void BasicTreeViewItem::on_activate()
@ -540,24 +637,6 @@ void BasicTreeViewItem::on_activate(ActivateFn fn)
activate_fn_ = fn;
}
BIFIconID BasicTreeViewItem::get_draw_icon() const
{
if (icon) {
return icon;
}
if (is_collapsible()) {
return is_collapsed() ? ICON_TRIA_RIGHT : ICON_TRIA_DOWN;
}
return ICON_NONE;
}
uiBut *BasicTreeViewItem::button()
{
return &tree_row_but_->but;
}
} // namespace blender::ui
using namespace blender::ui;

View File

@ -223,11 +223,12 @@ void AssetCatalogTreeViewItem::build_row(uiLayout &row)
return;
}
uiButTreeRow *tree_row_but = tree_row_button();
PointerRNA *props;
const CatalogID catalog_id = catalog_item_.get_catalog_id();
props = UI_but_extra_operator_icon_add(
button(), "ASSET_OT_catalog_new", WM_OP_INVOKE_DEFAULT, ICON_ADD);
(uiBut *)tree_row_but, "ASSET_OT_catalog_new", WM_OP_INVOKE_DEFAULT, ICON_ADD);
RNA_string_set(props, "parent_path", catalog_item_.catalog_path().c_str());
/* Tree items without a catalog ID represent components of catalog paths that are not
@ -238,7 +239,7 @@ void AssetCatalogTreeViewItem::build_row(uiLayout &row)
BLI_uuid_format(catalog_id_str_buffer, catalog_id);
props = UI_but_extra_operator_icon_add(
button(), "ASSET_OT_catalog_delete", WM_OP_INVOKE_DEFAULT, ICON_X);
(uiBut *)tree_row_but, "ASSET_OT_catalog_delete", WM_OP_INVOKE_DEFAULT, ICON_X);
RNA_string_set(props, "catalog_id", catalog_id_str_buffer);
}
}
@ -301,6 +302,7 @@ bool AssetCatalogTreeViewItem::drop_into_catalog(const AssetCatalogTreeView &tre
/* Trigger re-run of filtering to update visible assets. */
filelist_tag_needs_filtering(tree_view.space_file_.files);
file_select_deselect_all(&tree_view.space_file_, FILE_SEL_SELECTED | FILE_SEL_HIGHLIGHTED);
WM_main_add_notifier(NC_SPACE | ND_SPACE_FILE_LIST, nullptr);
}
return true;
@ -341,7 +343,7 @@ void AssetCatalogTreeViewAllItem::build_row(uiLayout &row)
PointerRNA *props;
props = UI_but_extra_operator_icon_add(
button(), "ASSET_OT_catalog_new", WM_OP_INVOKE_DEFAULT, ICON_ADD);
(uiBut *)tree_row_button(), "ASSET_OT_catalog_new", WM_OP_INVOKE_DEFAULT, ICON_ADD);
/* No parent path to use the root level. */
RNA_string_set(props, "parent_path", nullptr);
}