diff --git a/source/blender/animrig/ANIM_bone_collections.hh b/source/blender/animrig/ANIM_bone_collections.hh index 386a77cb0cd..6616d638c29 100644 --- a/source/blender/animrig/ANIM_bone_collections.hh +++ b/source/blender/animrig/ANIM_bone_collections.hh @@ -256,6 +256,11 @@ void ANIM_armature_refresh_solo_active(bArmature *armature); bool ANIM_armature_bonecoll_is_visible_effectively(const bArmature *armature, const BoneCollection *bcoll); +/** + * Expand or collapse a bone collection in the tree view. + */ +void ANIM_armature_bonecoll_is_expanded_set(BoneCollection *bcoll, bool is_expanded); + /** * Assign the bone to the bone collection. * @@ -381,6 +386,16 @@ bool armature_bonecoll_is_descendant_of(const bArmature *armature, bool bonecoll_has_children(const BoneCollection *bcoll); +/** + * For each bone collection in the destination armature, copy its #BONE_COLLECTION_EXPANDED flag + * from the corresponding bone collection in the source armature. + * + * This is used in the handling of undo steps, to ensure that undo'ing does _not_ + * modify this flag. + */ +void bonecolls_copy_expanded_flag(Span bcolls_dest, + Span bcolls_source); + /** * Move a bone collection from one parent to another. * diff --git a/source/blender/animrig/intern/bone_collections.cc b/source/blender/animrig/intern/bone_collections.cc index d45a4daf70b..26ecbffd303 100644 --- a/source/blender/animrig/intern/bone_collections.cc +++ b/source/blender/animrig/intern/bone_collections.cc @@ -711,9 +711,11 @@ void ANIM_armature_bonecoll_remove(bArmature *armature, BoneCollection *bcoll) armature_bonecoll_find_index(armature, bcoll)); } -BoneCollection *ANIM_armature_bonecoll_get_by_name(bArmature *armature, const char *name) +template +static MaybeConstBoneCollection *bonecolls_get_by_name( + blender::Span bonecolls, const char *name) { - for (BoneCollection *bcoll : armature->collections_span()) { + for (MaybeConstBoneCollection *bcoll : bonecolls) { if (STREQ(bcoll->name, name)) { return bcoll; } @@ -721,6 +723,11 @@ BoneCollection *ANIM_armature_bonecoll_get_by_name(bArmature *armature, const ch return nullptr; } +BoneCollection *ANIM_armature_bonecoll_get_by_name(bArmature *armature, const char *name) +{ + return bonecolls_get_by_name(armature->collections_span(), name); +} + int ANIM_armature_bonecoll_get_index_by_name(bArmature *armature, const char *name) { for (int index = 0; index < armature->collection_array_num; index++) { @@ -846,6 +853,16 @@ bool ANIM_armature_bonecoll_is_visible_effectively(const bArmature *armature, return bcoll->is_visible_with_ancestors(); } +void ANIM_armature_bonecoll_is_expanded_set(BoneCollection *bcoll, bool is_expanded) +{ + if (is_expanded) { + bcoll->flags |= BONE_COLLECTION_EXPANDED; + } + else { + bcoll->flags &= ~BONE_COLLECTION_EXPANDED; + } +} + /* Store the bone's membership on the collection. */ static void add_membership(BoneCollection *bcoll, Bone *bone) { @@ -1237,6 +1254,45 @@ bool bonecoll_has_children(const BoneCollection *bcoll) return bcoll->child_count > 0; } +void bonecolls_copy_expanded_flag(Span bcolls_dest, + Span bcolls_source) +{ + /* Try to preserve the bone collection expanded/collapsed states. These are UI + * changes that shouldn't impact undo steps. Care has to be taken to match the + * old and the new bone collections, though, as they may have been reordered + * or renamed. + * + * Reordering is handled by looking up collections by name. + * Renames are handled by skipping those that cannot be found by name. */ + + auto find_old = [bcolls_source](const char *name, const int index) -> const BoneCollection * { + /* Only check index when it's valid in the old armature. */ + if (index < bcolls_source.size()) { + const BoneCollection *bcoll = bcolls_source[index]; + if (STREQ(bcoll->name, name)) { + /* Index and name matches, let's use */ + return bcoll; + } + } + + /* Try to find by name as a last resort. This function only works with + * non-const pointers, hence the const_cast. */ + const BoneCollection *bcoll = bonecolls_get_by_name(bcolls_source, name); + return bcoll; + }; + + for (int i = 0; i < bcolls_dest.size(); i++) { + BoneCollection *bcoll_new = bcolls_dest[i]; + + const BoneCollection *bcoll_old = find_old(bcoll_new->name, i); + if (!bcoll_old) { + continue; + } + + ANIM_armature_bonecoll_is_expanded_set(bcoll_new, bcoll_old->is_expanded()); + } +} + int armature_bonecoll_move_to_parent(bArmature *armature, const int from_bcoll_index, int to_child_num, diff --git a/source/blender/blenkernel/intern/armature.cc b/source/blender/blenkernel/intern/armature.cc index e4432feed58..3cfb9d1b431 100644 --- a/source/blender/blenkernel/intern/armature.cc +++ b/source/blender/blenkernel/intern/armature.cc @@ -25,6 +25,7 @@ #include "BLI_math_vector.h" #include "BLI_span.hh" #include "BLI_string.h" +#include "BLI_string_ref.hh" #include "BLI_utildefines.h" #include "BLT_translation.h" @@ -63,6 +64,8 @@ #include "CLG_log.h" +using namespace blender; + /* -------------------------------------------------------------------- */ /** \name Prototypes * \{ */ @@ -461,6 +464,14 @@ static void armature_blend_read_data(BlendDataReader *reader, ID *id) ANIM_armature_runtime_refresh(arm); } +static void armature_undo_preserve(BlendLibReader * /*reader*/, ID *id_new, ID *id_old) +{ + bArmature *arm_new = (bArmature *)id_new; + bArmature *arm_old = (bArmature *)id_old; + + animrig::bonecolls_copy_expanded_flag(arm_new->collections_span(), arm_old->collections_span()); +} + IDTypeInfo IDType_ID_AR = { /*id_code*/ ID_AR, /*id_filter*/ FILTER_ID_AR, @@ -485,7 +496,7 @@ IDTypeInfo IDType_ID_AR = { /*blend_read_data*/ armature_blend_read_data, /*blend_read_after_liblink*/ nullptr, - /*blend_read_undo_preserve*/ nullptr, + /*blend_read_undo_preserve*/ armature_undo_preserve, /*lib_override_apply_post*/ nullptr, }; @@ -3190,5 +3201,9 @@ bool BoneCollection::is_solo() const { return this->flags & BONE_COLLECTION_SOLO; } +bool BoneCollection::is_expanded() const +{ + return this->flags & BONE_COLLECTION_EXPANDED; +} /** \} */ diff --git a/source/blender/editors/armature/bone_collections.cc b/source/blender/editors/armature/bone_collections.cc index 914625905eb..c6202e842c8 100644 --- a/source/blender/editors/armature/bone_collections.cc +++ b/source/blender/editors/armature/bone_collections.cc @@ -135,6 +135,7 @@ static int bone_collection_add_exec(bContext *C, wmOperator * /*op*/) } ANIM_armature_bonecoll_active_set(armature, bcoll); + /* TODO: ensure the ancestors of the new bone collection are all expanded. */ WM_event_add_notifier(C, NC_OBJECT | ND_POSE, ob); return OPERATOR_FINISHED; diff --git a/source/blender/editors/armature/editarmature_undo.cc b/source/blender/editors/armature/editarmature_undo.cc index ab8517d1875..c6cd3627140 100644 --- a/source/blender/editors/armature/editarmature_undo.cc +++ b/source/blender/editors/armature/editarmature_undo.cc @@ -97,6 +97,11 @@ static void undoarm_to_editarm(UndoArmature *uarm, bArmature *arm) ED_armature_ebone_listbase_temp_clear(arm->edbo); + /* Before freeing the old bone collections, copy their 'expanded' flag. This + * flag is not supposed to be restored with any undo steps. */ + bonecolls_copy_expanded_flag(blender::Span(uarm->collection_array, uarm->collection_array_num), + arm->collections_span()); + /* Copy bone collections. */ ANIM_bonecoll_array_free(&arm->collection_array, &arm->collection_array_num, true); auto bcoll_map = ANIM_bonecoll_array_copy_no_membership(&arm->collection_array, diff --git a/source/blender/editors/include/UI_tree_view.hh b/source/blender/editors/include/UI_tree_view.hh index 27c91181217..cfa99b22d8a 100644 --- a/source/blender/editors/include/UI_tree_view.hh +++ b/source/blender/editors/include/UI_tree_view.hh @@ -198,8 +198,22 @@ class AbstractTreeViewItem : public AbstractViewItem, public TreeViewItemContain std::optional get_win_rect(const ARegion ®ion) const; void begin_renaming(); - void toggle_collapsed(); - void set_collapsed(bool collapsed); + + /** + * Toggle the expanded/collapsed state. + * + * \note this does not call #on_collapse_change(). + * \returns true when the collapsed state was changed, false otherwise. + */ + bool toggle_collapsed(); + /** + * Expand or collapse this tree view item. + * + * \note this does not call #on_collapse_change(). + * \returns true when the collapsed state was changed, false otherwise. + */ + virtual bool set_collapsed(bool collapsed); + /** * Requires the tree to have completed reconstruction, see #is_reconstructed(). Otherwise we * can't be sure about the item state. @@ -207,6 +221,19 @@ class AbstractTreeViewItem : public AbstractViewItem, public TreeViewItemContain bool is_collapsed() const; bool is_collapsible() const; + /** + * Called when the view changes an item's state from expanded to collapsed, or vice versa. Will + * only be called if the state change is triggered through the view, not through external + * changes. E.g. a click on an item calls it, a change in the value returned by + * #should_be_collapsed() to reflect an external state change does not. + */ + virtual void on_collapse_change(bContext &C, bool is_collapsed); + /** + * If the result is not empty, it controls whether the item should be collapsed or not, usually + * depending on the data that the view represents. + */ + virtual std::optional should_be_collapsed() const; + protected: /** See AbstractViewItem::get_rename_string(). */ /* virtual */ StringRef get_rename_string() const override; @@ -219,6 +246,13 @@ class AbstractTreeViewItem : public AbstractViewItem, public TreeViewItemContain */ virtual bool supports_collapsing() const; + /** + * Toggle the collapsed/expanded state, and call on_collapse_change() if it changed. + */ + void toggle_collapsed_from_view(bContext &C); + + void change_state_delayed() override; + /** See #AbstractViewItem::matches(). */ /* virtual */ bool matches(const AbstractViewItem &other) const override; diff --git a/source/blender/editors/interface/interface_template_bone_collection_tree.cc b/source/blender/editors/interface/interface_template_bone_collection_tree.cc index f16c14f4ed0..38f28d97175 100644 --- a/source/blender/editors/interface/interface_template_bone_collection_tree.cc +++ b/source/blender/editors/interface/interface_template_bone_collection_tree.cc @@ -38,6 +38,8 @@ class BoneCollectionTreeView : public AbstractTreeView { explicit BoneCollectionTreeView(bArmature &armature); void build_tree() override; + bool listen(const wmNotifier ¬ifier) const override; + private: void build_tree_node_recursive(TreeViewItemContainer &parent, const int bcoll_index); @@ -281,6 +283,37 @@ class BoneCollectionItem : public AbstractTreeViewItem { ED_undo_push(&const_cast(C), "Change Armature's Active Bone Collection"); } + std::optional should_be_collapsed() const override + { + const bool is_collapsed = !bone_collection_.is_expanded(); + return is_collapsed; + } + + bool set_collapsed(const bool collapsed) override + { + if (!AbstractTreeViewItem::set_collapsed(collapsed)) { + return false; + } + + /* Ensure that the flag in DNA is set. */ + ANIM_armature_bonecoll_is_expanded_set(&bone_collection_, !collapsed); + return true; + } + + void on_collapse_change(bContext &C, const bool is_collapsed) override + { + const bool is_expanded = !is_collapsed; + + /* Let RNA handle the property change. This makes sure all the notifiers and DEG + * update calls are properly called. */ + PointerRNA bcoll_ptr = RNA_pointer_create( + &armature_.id, &RNA_BoneCollection, &bone_collection_); + PropertyRNA *prop = RNA_struct_find_property(&bcoll_ptr, "is_expanded"); + + RNA_property_boolean_set(&bcoll_ptr, prop, is_expanded); + RNA_property_update(&C, &bcoll_ptr, prop); + } + bool supports_renaming() const override { return ANIM_armature_bonecoll_is_editable(&armature_, &bone_collection_); @@ -351,8 +384,6 @@ void BoneCollectionTreeView::build_tree_node_recursive(TreeViewItemContainer &pa const bool has_any_selected_bones = bcolls_with_selected_bones_.contains(bcoll); BoneCollectionItem &bcoll_tree_item = parent.add_tree_item( armature_, bcoll_index, has_any_selected_bones); - bcoll_tree_item.set_collapsed(false); - for (int child_index = bcoll->child_index; child_index < bcoll->child_index + bcoll->child_count; child_index++) { @@ -360,6 +391,11 @@ void BoneCollectionTreeView::build_tree_node_recursive(TreeViewItemContainer &pa } } +bool BoneCollectionTreeView::listen(const wmNotifier ¬ifier) const +{ + return notifier.data == ND_BONE_COLLECTION; +} + void BoneCollectionTreeView::build_bcolls_with_selected_bones() { bcolls_with_selected_bones_.clear(); diff --git a/source/blender/editors/interface/views/tree_view.cc b/source/blender/editors/interface/views/tree_view.cc index 953676128f4..017a6958acf 100644 --- a/source/blender/editors/interface/views/tree_view.cc +++ b/source/blender/editors/interface/views/tree_view.cc @@ -354,7 +354,7 @@ void AbstractTreeViewItem::collapse_chevron_click_fn(bContext *C, AbstractTreeViewItem *hovered_item = from_item_handle(hovered_item_handle); BLI_assert(hovered_item != nullptr); - hovered_item->toggle_collapsed(); + hovered_item->toggle_collapsed_from_view(*C); /* When collapsing an item with an active child, make this collapsed item active instead so the * active item stays visible. */ if (hovered_item->has_active_child()) { @@ -504,24 +504,63 @@ bool AbstractTreeViewItem::is_collapsed() const return is_collapsible() && !is_open_; } -void AbstractTreeViewItem::toggle_collapsed() +bool AbstractTreeViewItem::toggle_collapsed() { - is_open_ = !is_open_; + return set_collapsed(is_open_); } -void AbstractTreeViewItem::set_collapsed(const bool collapsed) +bool AbstractTreeViewItem::set_collapsed(const bool collapsed) { + if (!is_collapsible()) { + return false; + } + if (collapsed == is_collapsed()) { + return false; + } + is_open_ = !collapsed; + return true; } bool AbstractTreeViewItem::is_collapsible() const { + BLI_assert_msg(get_tree_view().is_reconstructed(), + "State can't be queried until reconstruction is completed"); if (children_.is_empty()) { return false; } return this->supports_collapsing(); } +void AbstractTreeViewItem::on_collapse_change(bContext & /*C*/, const bool /*is_collapsed*/) +{ + /* Do nothing by default. */ +} + +std::optional AbstractTreeViewItem::should_be_collapsed() const +{ + return std::nullopt; +} + +void AbstractTreeViewItem::toggle_collapsed_from_view(bContext &C) +{ + if (toggle_collapsed()) { + on_collapse_change(C, is_collapsed()); + } +} + +void AbstractTreeViewItem::change_state_delayed() +{ + AbstractViewItem::change_state_delayed(); + + const std::optional should_be_collapsed = this->should_be_collapsed(); + if (should_be_collapsed.has_value()) { + /* This reflects an external state change and therefore shouldn't call #on_collapse_change(). + */ + set_collapsed(*should_be_collapsed); + } +} + void AbstractTreeViewItem::ensure_parents_uncollapsed() { for (AbstractTreeViewItem *parent = parent_; parent; parent = parent->parent_) { diff --git a/source/blender/makesdna/DNA_armature_types.h b/source/blender/makesdna/DNA_armature_types.h index 981b3ff3357..f8cd3f8d036 100644 --- a/source/blender/makesdna/DNA_armature_types.h +++ b/source/blender/makesdna/DNA_armature_types.h @@ -306,6 +306,12 @@ typedef struct BoneCollection { * Return whether this collection is marked as 'solo'. */ bool is_solo() const; + /** + * Whether or not this bone collection is expanded in the tree view. + * + * This corresponds to the #BONE_COLLECTION_EXPANDED flag. + */ + bool is_expanded() const; #endif } BoneCollection; @@ -534,8 +540,10 @@ typedef enum eBoneCollection_Flag { * \see eArmature_Flag::ARM_BCOLL_SOLO_ACTIVE */ BONE_COLLECTION_SOLO = (1 << 4), + + BONE_COLLECTION_EXPANDED = (1 << 5), /* Expanded in the tree view. */ } eBoneCollection_Flag; -ENUM_OPERATORS(eBoneCollection_Flag, BONE_COLLECTION_SOLO) +ENUM_OPERATORS(eBoneCollection_Flag, BONE_COLLECTION_EXPANDED) #ifdef __cplusplus diff --git a/source/blender/makesrna/intern/rna_armature.cc b/source/blender/makesrna/intern/rna_armature.cc index a26b18fdfa0..50e91ca364d 100644 --- a/source/blender/makesrna/intern/rna_armature.cc +++ b/source/blender/makesrna/intern/rna_armature.cc @@ -399,6 +399,12 @@ static void rna_BoneCollection_is_solo_set(PointerRNA *ptr, const bool is_solo) ANIM_armature_bonecoll_solo_set(arm, bcoll, is_solo); } +static void rna_BoneCollection_is_expanded_set(PointerRNA *ptr, const bool is_expanded) +{ + BoneCollection *bcoll = (BoneCollection *)ptr->data; + ANIM_armature_bonecoll_is_expanded_set(bcoll, is_expanded); +} + static std::optional rna_BoneCollection_path(const PointerRNA *ptr) { const BoneCollection *bcoll = (const BoneCollection *)ptr->data; @@ -2306,6 +2312,15 @@ static void rna_def_bonecollection(BlenderRNA *brna) RNA_def_property_string_funcs(prop, nullptr, nullptr, "rna_BoneCollection_name_set"); RNA_def_property_update(prop, NC_OBJECT | ND_BONE_COLLECTION, nullptr); + prop = RNA_def_property(srna, "is_expanded", PROP_BOOLEAN, PROP_NONE); + RNA_def_property_boolean_sdna(prop, nullptr, "flags", BONE_COLLECTION_EXPANDED); + RNA_def_property_ui_text( + prop, "Expanded", "This bone collection is expanded in the bone collections tree view"); + RNA_def_property_flag(prop, PROP_LIB_EXCEPTION); + RNA_def_property_override_flag(prop, PROPOVERRIDE_OVERRIDABLE_LIBRARY); + RNA_def_property_boolean_funcs(prop, nullptr, "rna_BoneCollection_is_expanded_set"); + RNA_def_property_update(prop, NC_OBJECT | ND_BONE_COLLECTION, nullptr); + prop = RNA_def_property(srna, "is_visible", PROP_BOOLEAN, PROP_NONE); RNA_def_property_boolean_sdna(prop, nullptr, "flags", BONE_COLLECTION_VISIBLE); RNA_def_property_ui_text(