Assets: Support Renaming Catalogs in the UI

Catalogs can now be renamed by double clicking them in the Asset
Browser. This is mostly done through the tree-view API, the asset
specific code is very little.

There is some polish left to be done here, e.g. the double click
currently also collapses/uncollapses and activates the clicked item. And
the rename button takes the full width of the row. But addressing these
is better done as part of some other behavioral changes that are planned
anyway.
This commit is contained in:
Julian Eisel 2021-10-06 14:18:12 +02:00
parent 335f40ebfa
commit 539575b585
Notes: blender-bot 2023-02-14 01:57:12 +01:00
Referenced by issue #91549, Support renaming catalogs
5 changed files with 278 additions and 41 deletions

View File

@ -2770,6 +2770,8 @@ char *UI_tree_view_item_drop_tooltip(const uiTreeViewItemHandle *item,
const struct bContext *C,
const struct wmDrag *drag,
const struct wmEvent *event);
bool UI_tree_view_item_can_rename(const uiTreeViewItemHandle *item_handle);
void UI_tree_view_item_begin_rename(uiTreeViewItemHandle *item_handle);
uiTreeViewItemHandle *UI_block_tree_view_find_item_at(const struct ARegion *region, int x, int y);

View File

@ -23,10 +23,13 @@
#pragma once
#include <array>
#include <functional>
#include <memory>
#include <string>
#include "DNA_defs.h"
#include "BLI_function_ref.hh"
#include "BLI_vector.hh"
@ -144,8 +147,14 @@ class TreeViewLayoutBuilder {
* \{ */
class AbstractTreeView : public TreeViewItemContainer {
friend AbstractTreeViewItem;
friend TreeViewBuilder;
friend TreeViewLayoutBuilder;
/**
* Only one item can be renamed at a time. So the tree is informed about the renaming state to
* enforce that.
*/
std::unique_ptr<std::array<char, MAX_NAME>> rename_buffer_;
bool is_reconstructed_ = false;
@ -154,6 +163,8 @@ class AbstractTreeView : public TreeViewItemContainer {
void foreach_item(ItemIterFn iter_fn, IterOptions options = IterOptions::None) const;
/** Only one item can be renamed at a time. */
bool is_renaming() const;
/**
* Check if the tree is fully (re-)constructed. That means, both #build_tree() and
* #update_from_old() have finished.
@ -198,6 +209,7 @@ class AbstractTreeView : public TreeViewItemContainer {
*/
class AbstractTreeViewItem : public TreeViewItemContainer {
friend class AbstractTreeView;
friend class TreeViewLayoutBuilder;
public:
using IsActiveFn = std::function<bool()>;
@ -205,12 +217,15 @@ class AbstractTreeViewItem : public TreeViewItemContainer {
private:
bool is_open_ = false;
bool is_active_ = false;
bool is_renaming_ = false;
IsActiveFn is_active_fn_;
protected:
/** This label is used for identifying an item (together with its parent's labels). */
std::string label_{};
/** Every item gets a button of type during the layout building #UI_BTYPE_TREEROW. */
uiButTreeRow *tree_row_but_ = nullptr;
public:
virtual ~AbstractTreeViewItem() = default;
@ -234,6 +249,19 @@ class AbstractTreeViewItem : public TreeViewItemContainer {
virtual std::string drop_tooltip(const bContext &C,
const wmDrag &drag,
const wmEvent &event) const;
/**
* Queries if the tree-view item supports renaming in principle. Renaming may still fail, e.g. if
* another item is already being renamed.
*/
virtual bool can_rename() const;
/**
* Try renaming the item, or the data it represents. Can assume
* #AbstractTreeViewItem::can_rename() returned true. Sub-classes that override this should
* usually call this, unless they have a custom #AbstractTreeViewItem.matches().
*
* \return True if the renaming was successful.
*/
virtual bool rename(StringRefNull new_name);
/**
* Copy persistent state (e.g. is-collapsed flag, selection, etc.) from a matching item of
@ -250,7 +278,11 @@ class AbstractTreeViewItem : public TreeViewItemContainer {
*/
virtual bool matches(const AbstractTreeViewItem &other) const;
void begin_renaming();
void end_renaming();
const AbstractTreeView &get_tree_view() const;
AbstractTreeView &get_tree_view();
int count_parents() const;
void deactivate();
/**
@ -266,6 +298,8 @@ class AbstractTreeViewItem : public TreeViewItemContainer {
bool is_collapsed() const;
void set_collapsed(bool collapsed);
bool is_collapsible() const;
bool is_renaming() const;
void ensure_parents_uncollapsed();
protected:
@ -278,8 +312,14 @@ class AbstractTreeViewItem : public TreeViewItemContainer {
void activate();
private:
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 *);
/** See #AbstractTreeView::change_state_delayed() */
void change_state_delayed();
void add_treerow_button(uiBlock &block);
void add_rename_button(uiBlock &block);
};
/** \} */
@ -304,8 +344,6 @@ class BasicTreeViewItem : public AbstractTreeViewItem {
void on_activate(ActivateFn fn);
protected:
/** Created in the #build() function. */
uiButTreeRow *tree_row_but_ = nullptr;
/**
* Optionally passed to the #BasicTreeViewItem constructor. Called when activating this tree
* view item. This way users don't have to sub-class #BasicTreeViewItem, just to implement

View File

@ -4822,6 +4822,24 @@ static int ui_do_but_TOG(bContext *C, uiBut *but, uiHandleButtonData *data, cons
return WM_UI_HANDLER_CONTINUE;
}
static int ui_do_but_TREEROW(bContext *C,
uiBut *but,
uiHandleButtonData *data,
const wmEvent *event)
{
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);
UI_tree_view_item_begin_rename(tree_row_but->tree_item);
return WM_UI_HANDLER_BREAK;
}
return ui_do_but_TOG(C, but, data, event);
}
static int ui_do_but_EXIT(bContext *C, uiBut *but, uiHandleButtonData *data, const wmEvent *event)
{
if (data->state == BUTTON_STATE_HIGHLIGHT) {
@ -7989,10 +8007,12 @@ static int ui_do_button(bContext *C, uiBlock *block, uiBut *but, const wmEvent *
case UI_BTYPE_CHECKBOX:
case UI_BTYPE_CHECKBOX_N:
case UI_BTYPE_ROW:
case UI_BTYPE_TREEROW:
case UI_BTYPE_DATASETROW:
retval = ui_do_but_TOG(C, but, data, event);
break;
case UI_BTYPE_TREEROW:
retval = ui_do_but_TREEROW(C, but, data, event);
break;
case UI_BTYPE_SCROLL:
retval = ui_do_but_SCROLL(C, block, but, data, event);
break;

View File

@ -20,6 +20,8 @@
#include "DNA_userdef_types.h"
#include "BKE_context.h"
#include "BLT_translation.h"
#include "interface_intern.h"
@ -76,6 +78,11 @@ void AbstractTreeView::foreach_item(ItemIterFn iter_fn, IterOptions options) con
foreach_item_recursive(iter_fn, options);
}
bool AbstractTreeView::is_renaming() const
{
return rename_buffer_ != nullptr;
}
void AbstractTreeView::build_layout_from_tree(const TreeViewLayoutBuilder &builder)
{
uiLayout *prev_layout = builder.current_layout();
@ -103,6 +110,13 @@ void AbstractTreeView::update_from_old(uiBlock &new_block)
BLI_assert(old_view_handle);
AbstractTreeView &old_view = reinterpret_cast<AbstractTreeView &>(*old_view_handle);
/* Update own persistent data. */
/* Keep the rename buffer persistent while renaming! The rename button uses the buffer's
* pointer to identify itself over redraws. */
rename_buffer_ = std::move(old_view.rename_buffer_);
old_view.rename_buffer_ = nullptr;
update_children_from_old_recursive(*this, old_view);
/* Finished (re-)constructing the tree. */
@ -153,6 +167,95 @@ void AbstractTreeView::change_state_delayed()
/* ---------------------------------------------------------------------- */
void AbstractTreeViewItem::tree_row_click_fn(struct bContext * /*C*/,
void *but_arg1,
void * /*arg2*/)
{
uiButTreeRow *tree_row_but = (uiButTreeRow *)but_arg1;
BasicTreeViewItem &tree_item = reinterpret_cast<BasicTreeViewItem &>(*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)
{
tree_row_but_ = (uiButTreeRow *)uiDefBut(
&block, UI_BTYPE_TREEROW, 0, "", 0, 0, UI_UNIT_X, 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());
}
AbstractTreeViewItem *AbstractTreeViewItem::find_tree_item_from_rename_button(
const uiBut &rename_but)
{
/* A minimal sanity check, can't do much more here. */
BLI_assert(rename_but.type == UI_BTYPE_TEXT && rename_but.poin);
LISTBASE_FOREACH (uiBut *, but, &rename_but.block->buttons) {
if (but->type != UI_BTYPE_TREEROW) {
continue;
}
uiButTreeRow *tree_row_but = (uiButTreeRow *)but;
AbstractTreeViewItem *item = reinterpret_cast<AbstractTreeViewItem *>(tree_row_but->tree_item);
const AbstractTreeView &tree_view = item->get_tree_view();
if (item->is_renaming() && (tree_view.rename_buffer_->data() == rename_but.poin)) {
return item;
}
}
return nullptr;
}
void AbstractTreeViewItem::rename_button_fn(bContext *UNUSED(C), void *arg, char *UNUSED(origstr))
{
const uiBut *rename_but = static_cast<uiBut *>(arg);
AbstractTreeViewItem *item = find_tree_item_from_rename_button(*rename_but);
BLI_assert(item);
const AbstractTreeView &tree_view = item->get_tree_view();
item->rename(tree_view.rename_buffer_->data());
item->end_renaming();
}
void AbstractTreeViewItem::add_rename_button(uiBlock &block)
{
AbstractTreeView &tree_view = get_tree_view();
uiBut *rename_but = uiDefBut(&block,
UI_BTYPE_TEXT,
1,
"",
0,
0,
UI_UNIT_X,
UI_UNIT_Y,
tree_view.rename_buffer_->data(),
1.0f,
tree_view.rename_buffer_->max_size(),
0,
0,
"");
/* Gotta be careful with what's passed to the `arg1` here. Any tree data will be freed once the
* 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);
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) {
end_renaming();
}
}
void AbstractTreeViewItem::on_activate()
{
/* Do nothing by default. */
@ -181,10 +284,25 @@ std::string AbstractTreeViewItem::drop_tooltip(const bContext & /*C*/,
return TIP_("Drop into/onto tree item");
}
bool AbstractTreeViewItem::can_rename() const
{
/* No renaming by default. */
return false;
}
bool AbstractTreeViewItem::rename(StringRefNull new_name)
{
/* It is important to update the label after renaming, so #AbstractTreeViewItem::matches()
* recognizes the item. (It only compares labels by default.) */
label_ = new_name;
return true;
}
void AbstractTreeViewItem::update_from_old(const AbstractTreeViewItem &old)
{
is_open_ = old.is_open_;
is_active_ = old.is_active_;
is_renaming_ = old.is_renaming_;
}
bool AbstractTreeViewItem::matches(const AbstractTreeViewItem &other) const
@ -192,11 +310,41 @@ bool AbstractTreeViewItem::matches(const AbstractTreeViewItem &other) const
return label_ == other.label_;
}
void AbstractTreeViewItem::begin_renaming()
{
AbstractTreeView &tree_view = get_tree_view();
if (tree_view.is_renaming() || !can_rename()) {
return;
}
is_renaming_ = true;
tree_view.rename_buffer_ = std::make_unique<decltype(tree_view.rename_buffer_)::element_type>();
std::copy(std::begin(label_), std::end(label_), std::begin(*tree_view.rename_buffer_));
}
void AbstractTreeViewItem::end_renaming()
{
if (!is_renaming()) {
return;
}
is_renaming_ = false;
AbstractTreeView &tree_view = get_tree_view();
tree_view.rename_buffer_ = nullptr;
}
const AbstractTreeView &AbstractTreeViewItem::get_tree_view() const
{
return static_cast<AbstractTreeView &>(*root_);
}
AbstractTreeView &AbstractTreeViewItem::get_tree_view()
{
return static_cast<AbstractTreeView &>(*root_);
}
int AbstractTreeViewItem::count_parents() const
{
int i = 0;
@ -259,6 +407,11 @@ bool AbstractTreeViewItem::is_collapsible() const
return !children_.is_empty();
}
bool AbstractTreeViewItem::is_renaming() const
{
return is_renaming_;
}
void AbstractTreeViewItem::ensure_parents_uncollapsed()
{
for (AbstractTreeViewItem *parent = parent_; parent; parent = parent->parent_) {
@ -298,9 +451,21 @@ void TreeViewLayoutBuilder::build_row(AbstractTreeViewItem &item) const
uiLayout *prev_layout = current_layout();
uiLayout *row = uiLayoutRow(prev_layout, false);
item.build_row(*row);
uiLayoutOverlap(row);
UI_block_layout_set_current(&block(), prev_layout);
uiBlock &block_ = block();
/* Every item gets one! Other buttons can be overlapped on top. */
item.add_treerow_button(block_);
if (item.is_renaming()) {
item.add_rename_button(block_);
}
else {
item.build_row(*row);
}
UI_block_layout_set_current(&block_, prev_layout);
}
uiBlock &TreeViewLayoutBuilder::block() const
@ -320,42 +485,12 @@ BasicTreeViewItem::BasicTreeViewItem(StringRef label, BIFIconID icon_) : icon(ic
label_ = label;
}
void BasicTreeViewItem::tree_row_click_fn(struct bContext * /*C*/, void *but_arg1, void * /*arg2*/)
void BasicTreeViewItem::build_row(uiLayout & /*row*/)
{
uiButTreeRow *tree_row_but = (uiButTreeRow *)but_arg1;
BasicTreeViewItem &tree_item = reinterpret_cast<BasicTreeViewItem &>(*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();
if (BIFIconID icon = get_draw_icon()) {
ui_def_but_icon(&tree_row_but_->but, icon, UI_HAS_ICON);
}
tree_item.activate();
}
void BasicTreeViewItem::build_row(uiLayout &row)
{
uiBlock *block = uiLayoutGetBlock(&row);
tree_row_but_ = (uiButTreeRow *)uiDefIconTextBut(block,
UI_BTYPE_TREEROW,
0,
/* TODO allow icon besides the chevron icon? */
get_draw_icon(),
label_.data(),
0,
0,
UI_UNIT_X,
UI_UNIT_Y,
nullptr,
0,
0,
0,
0,
nullptr);
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());
tree_row_but_->but.str = BLI_strdupn(label_.c_str(), label_.length());
}
void BasicTreeViewItem::on_activate()
@ -437,3 +572,21 @@ bool UI_tree_view_item_drop_handle(uiTreeViewItemHandle *item_, const ListBase *
return false;
}
/**
* Can \a item_handle be renamed right now? Not that this isn't just a mere wrapper around
* #AbstractTreeViewItem::can_rename(). This also checks if there is another item being renamed,
* and returns false if so.
*/
bool UI_tree_view_item_can_rename(const uiTreeViewItemHandle *item_handle)
{
const AbstractTreeViewItem &item = reinterpret_cast<const AbstractTreeViewItem &>(*item_handle);
const AbstractTreeView &tree_view = item.get_tree_view();
return !tree_view.is_renaming() && item.can_rename();
}
void UI_tree_view_item_begin_rename(uiTreeViewItemHandle *item_handle)
{
AbstractTreeViewItem &item = reinterpret_cast<AbstractTreeViewItem &>(*item_handle);
item.begin_renaming();
}

View File

@ -52,6 +52,7 @@ using namespace blender::bke;
namespace blender::ed::asset_browser {
class AssetCatalogTreeView : public ui::AbstractTreeView {
bke::AssetCatalogService *catalog_service_;
/** The asset catalog tree this tree-view represents. */
bke::AssetCatalogTree *catalog_tree_;
FileAssetSelectParams *params_;
@ -99,6 +100,9 @@ class AssetCatalogTreeViewItem : public ui::BasicTreeViewItem {
const wmDrag &drag,
const wmEvent &event) const override;
bool on_drop(const wmDrag &drag) override;
bool can_rename() const override;
bool rename(StringRefNull new_name) override;
};
/** Only reason this isn't just `BasicTreeViewItem` is to add a '+' icon for adding a root level
@ -124,7 +128,8 @@ class AssetCatalogTreeViewUnassignedItem : public ui::BasicTreeViewItem {
AssetCatalogTreeView::AssetCatalogTreeView(::AssetLibrary *library,
FileAssetSelectParams *params,
SpaceFile &space_file)
: catalog_tree_(BKE_asset_library_get_catalog_tree(library)),
: catalog_service_(BKE_asset_library_get_catalog_service(library)),
catalog_tree_(BKE_asset_library_get_catalog_tree(library)),
params_(params),
space_file_(space_file)
{
@ -309,6 +314,25 @@ bool AssetCatalogTreeViewItem::on_drop(const wmDrag &drag)
tree_view, drag, catalog_item_.get_catalog_id(), catalog_item_.get_simple_name());
}
bool AssetCatalogTreeViewItem::can_rename() const
{
return true;
}
bool AssetCatalogTreeViewItem::rename(StringRefNull new_name)
{
/* Important to keep state. */
BasicTreeViewItem::rename(new_name);
const AssetCatalogTreeView &tree_view = static_cast<const AssetCatalogTreeView &>(
get_tree_view());
AssetCatalogPath new_path = catalog_item_.catalog_path().parent();
new_path = new_path / StringRef(new_name);
tree_view.catalog_service_->update_catalog_path(catalog_item_.get_catalog_id(), new_path);
return true;
}
/* ---------------------------------------------------------------------- */
void AssetCatalogTreeViewAllItem::build_row(uiLayout &row)