Anim: add concept of 'active slot'

Add the concept of 'active slot' within an Action. This allows
clicking on a slot in the Action editor, to select it and mark it as
'active'.

Note that this does _not_ add support for action slots in
`ANIM_set_active_channel()`, as that function doesn't get enough info
to do that, and refactoring it is not on my wishlist.

RNA property `action.slots.active` can be used to access and set the
active slot in Python. `slot.active` can be used to query the slot's
active state, and is read-only (so that there is one way to set the
active slot).

A panel in the Action editor shows info about the active slot. This
panel is just a minimal UI that shows the name and an icon
representing the idtype of the active slot.

Pull Request: https://projects.blender.org/blender/blender/pulls/124422
This commit is contained in:
Sybren A. Stüvel
2024-07-12 11:59:04 +02:00
parent d0089e6fe1
commit d828e9471e
7 changed files with 268 additions and 11 deletions

View File

@@ -636,6 +636,28 @@ class DOPESHEET_PT_action(DopesheetActionPanelBase, Panel):
self.draw_generic_panel(context, self.layout, action)
class DOPESHEET_PT_action_slot(Panel):
bl_space_type = 'DOPESHEET_EDITOR'
bl_region_type = 'UI'
bl_category = "Action"
bl_label = "Slot"
@classmethod
def poll(cls, context):
action = context.active_action
return bool(action and action.slots.active)
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
action = context.active_action
slot = action.slots.active
layout.prop(slot, "name_display", text="Name", icon_value=slot.idtype_icon)
#######################################
# Grease Pencil Editing
@@ -976,6 +998,7 @@ classes = (
DOPESHEET_MT_view_pie,
DOPESHEET_PT_filters,
DOPESHEET_PT_action,
DOPESHEET_PT_action_slot,
DOPESHEET_PT_gpencil_mode,
DOPESHEET_PT_gpencil_layer_masks,
DOPESHEET_PT_gpencil_layer_transform,

View File

@@ -217,6 +217,27 @@ class Action : public ::bAction {
*/
Slot &slot_ensure_for_id(const ID &animated_id);
/**
* Set the active Slot, ensuring only one Slot is flagged as the Active one.
*
* \param slot_handle if Slot::unassigned, there will not be any active slot.
* Passing an unknown/invalid slot handle will result in no slot being active.
*/
void slot_active_set(slot_handle_t slot_handle);
/**
* Get the active Slot.
*
* This requires a linear scan of the slots, to find the one with the 'Active' flag set. Storing
* this on the Slot itself has the advantage that the 'active' status of a Slot can be determined
* without requiring access to the owning Action.
*
* As this already does a linear scan for the active slot, the slot is returned as a pointer;
* obtaining the pointer from a handle would require another linear scan to get the pointer,
* whereas obtaining the handle from the pointer is a constant operation.
*/
Slot *slot_active_get();
/** Assign this Action to the ID.
*
* \param slot: The slot this ID should be animated by, may be nullptr if it is to be
@@ -519,6 +540,8 @@ class Slot : public ::ActionSlot {
Expanded = (1 << 0),
/** Selected in animation editors. */
Selected = (1 << 1),
/** The active Slot for this Action. Set via a method on the Action. */
Active = (1 << 2),
/* When adding/removing a flag, also update the ENUM_OPERATORS() invocation,
* all the way below the Slot class. */
};
@@ -527,6 +550,7 @@ class Slot : public ::ActionSlot {
void set_expanded(bool expanded);
bool is_selected() const;
void set_selected(bool selected);
bool is_active() const;
/** Return the set of IDs that are animated by this Slot. */
Span<ID *> users(Main &bmain) const;
@@ -580,10 +604,15 @@ class Slot : public ::ActionSlot {
* the responsibility of the caller.
*/
void name_ensure_prefix();
/**
* Set the 'Active' flag. Only allowed to be called by Action.
*/
void set_active(bool active);
};
static_assert(sizeof(Slot) == sizeof(::ActionSlot),
"DNA struct and its C++ wrapper must have the same size");
ENUM_OPERATORS(Slot::Flags, Slot::Flags::Selected);
ENUM_OPERATORS(Slot::Flags, Slot::Flags::Active);
/**
* KeyframeStrips effectively contain a bag of F-Curves for each Slot.

View File

@@ -413,6 +413,23 @@ Slot &Action::slot_ensure_for_id(const ID &animated_id)
return this->slot_add_for_id(animated_id);
}
void Action::slot_active_set(const slot_handle_t slot_handle)
{
for (Slot *slot : slots()) {
slot->set_active(slot->handle == slot_handle);
}
}
Slot *Action::slot_active_get()
{
for (Slot *slot : slots()) {
if (slot->is_active()) {
return slot;
}
}
return nullptr;
}
Slot *Action::find_suitable_slot_for(const ID &animated_id)
{
AnimData *adt = BKE_animdata_from_id(&animated_id);
@@ -711,6 +728,20 @@ void Slot::set_selected(const bool selected)
}
}
bool Slot::is_active() const
{
return this->slot_flags & uint8_t(Flags::Active);
}
void Slot::set_active(const bool active)
{
if (active) {
this->slot_flags |= uint8_t(Flags::Active);
}
else {
this->slot_flags &= ~(uint8_t(Flags::Active));
}
}
Span<ID *> Slot::users(Main &bmain) const
{
if (bmain.is_action_slot_to_id_map_dirty) {

View File

@@ -450,6 +450,62 @@ TEST_F(ActionLayersTest, find_suitable_slot)
EXPECT_EQ(&slot, action->find_suitable_slot_for(cube->id));
}
TEST_F(ActionLayersTest, active_slot)
{
{ /* Empty case, no slots exist yet. */
EXPECT_EQ(nullptr, action->slot_active_get());
action->slot_active_set(Slot::unassigned);
EXPECT_EQ(nullptr, action->slot_active_get());
}
{ /* Single slot case. */
Slot &slot_cube = action->slot_ensure_for_id(cube->id);
EXPECT_EQ(nullptr, action->slot_active_get())
<< "Adding the first slot should not change what is the active slot.";
action->slot_active_set(slot_cube.handle);
EXPECT_EQ(&slot_cube, action->slot_active_get())
<< "It should be possible to activate the only available slot";
EXPECT_TRUE(slot_cube.is_active());
action->slot_active_set(Slot::unassigned);
EXPECT_EQ(nullptr, action->slot_active_get())
<< "It should be possible to de-activate the only available slot";
EXPECT_FALSE(slot_cube.is_active());
}
{
/* Multiple slots case. */
Slot &slot_cube = *action->slot(0);
action->slot_active_set(slot_cube.handle);
Slot &slot_suz = action->slot_ensure_for_id(suzanne->id);
Slot &slot_bob = action->slot_ensure_for_id(bob->id);
EXPECT_EQ(&slot_cube, action->slot_active_get())
<< "Adding a subsequent slot should not change what is the active slot.";
EXPECT_TRUE(slot_cube.is_active());
action->slot_active_set(slot_suz.handle);
EXPECT_EQ(&slot_suz, action->slot_active_get());
EXPECT_FALSE(slot_cube.is_active());
EXPECT_TRUE(slot_suz.is_active());
EXPECT_FALSE(slot_bob.is_active());
action->slot_active_set(slot_bob.handle);
EXPECT_EQ(&slot_bob, action->slot_active_get());
EXPECT_FALSE(slot_cube.is_active());
EXPECT_FALSE(slot_suz.is_active());
EXPECT_TRUE(slot_bob.is_active());
action->slot_active_set(Slot::unassigned);
EXPECT_EQ(nullptr, action->slot_active_get());
EXPECT_FALSE(slot_cube.is_active());
EXPECT_FALSE(slot_suz.is_active());
EXPECT_FALSE(slot_bob.is_active());
}
}
TEST_F(ActionLayersTest, strip)
{
constexpr float inf = std::numeric_limits<float>::infinity();

View File

@@ -348,6 +348,11 @@ void ANIM_set_active_channel(bAnimContext *ac,
nlt->flag |= NLATRACK_ACTIVE;
break;
}
case ANIMTYPE_ACTION_SLOT:
/* ANIMTYPE_ACTION_SLOT is not supported by this function (because the to-be-activated
* bAnimListElement is not passed here, only sub-fields of it), just call
* Action::slot_active_set() directly. */
break;
case ANIMTYPE_FILLACTD: /* Action Expander */
case ANIMTYPE_FILLACT_LAYERED: /* Animation Expander */
case ANIMTYPE_DSMAT: /* Datablock AnimData Expanders */
@@ -401,6 +406,8 @@ void ANIM_set_active_channel(bAnimContext *ac,
bool ANIM_is_active_channel(bAnimListElem *ale)
{
using namespace blender;
switch (ale->type) {
case ANIMTYPE_FILLACTD: /* Action Expander */
case ANIMTYPE_FILLACT_LAYERED: /* Animation Expander */
@@ -445,6 +452,10 @@ bool ANIM_is_active_channel(bAnimListElem *ale)
GreasePencil *grease_pencil = reinterpret_cast<GreasePencil *>(ale->id);
return grease_pencil->is_layer_active(
static_cast<blender::bke::greasepencil::Layer *>(ale->data));
}
case ANIMTYPE_ACTION_SLOT: {
animrig::Slot *slot = reinterpret_cast<animrig::Slot *>(ale->data);
return slot->is_active();
}
/* These channel types do not have active flags. */
case ANIMTYPE_NONE:
@@ -453,7 +464,6 @@ bool ANIM_is_active_channel(bAnimListElem *ale)
case ANIMTYPE_SUMMARY:
case ANIMTYPE_SCENE:
case ANIMTYPE_OBJECT:
case ANIMTYPE_ACTION_SLOT:
case ANIMTYPE_NLACONTROLS:
case ANIMTYPE_FILLDRIVERS:
case ANIMTYPE_SHAPEKEY:
@@ -3802,6 +3812,43 @@ static int click_select_channel_fcurve(bAnimContext *ac,
return (ND_ANIMCHAN | NA_SELECTED);
}
static int click_select_channel_action_slot(bAnimContext *ac,
bAnimListElem *ale,
short /* eEditKeyframes_Select or -1 */ selectmode)
{
using namespace blender;
BLI_assert_msg(GS(ale->fcurve_owner_id->name) == ID_AC,
"fcurve_owner_id of an Action Slot should be an Action");
animrig::Action *action = reinterpret_cast<animrig::Action *>(ale->fcurve_owner_id);
animrig::Slot *slot = static_cast<animrig::Slot *>(ale->data);
if (selectmode == SELECT_INVERT) {
selectmode = slot->is_selected() ? SELECT_SUBTRACT : SELECT_ADD;
}
switch (selectmode) {
case SELECT_REPLACE:
ANIM_anim_channels_select_set(ac, ACHANNEL_SETFLAG_CLEAR);
ATTR_FALLTHROUGH;
case SELECT_ADD:
slot->set_selected(true);
action->slot_active_set(slot->handle);
break;
case SELECT_SUBTRACT:
slot->set_selected(false);
break;
case SELECT_EXTEND_RANGE:
ANIM_anim_channels_select_set(ac, ACHANNEL_SETFLAG_EXTEND_RANGE);
animchannel_select_range(ac, ale);
break;
case SELECT_INVERT:
BLI_assert_unreachable();
break;
}
return (ND_ANIMCHAN | NA_SELECTED);
}
static int click_select_channel_shapekey(bAnimContext *ac,
bAnimListElem *ale,
@@ -4074,6 +4121,9 @@ static int mouse_anim_channels(bContext *C,
case ANIMTYPE_NLACURVE:
notifierFlags |= click_select_channel_fcurve(ac, ale, selectmode, filter);
break;
case ANIMTYPE_ACTION_SLOT:
notifierFlags |= click_select_channel_action_slot(ac, ale, selectmode);
break;
case ANIMTYPE_SHAPEKEY:
notifierFlags |= click_select_channel_shapekey(ac, ale, selectmode);
break;

View File

@@ -1996,6 +1996,14 @@ static int mouse_action_keys(bAnimContext *ac,
ED_gpencil_set_active_channel(gpd, gpl);
}
else if (ale->type == ANIMTYPE_ACTION_SLOT) {
BLI_assert_msg(GS(ale->fcurve_owner_id->name) == ID_AC,
"fcurve_owner_id of an Action Slot should be an Action");
animrig::Action *action = reinterpret_cast<animrig::Action *>(ale->fcurve_owner_id);
animrig::Slot *slot = static_cast<animrig::Slot *>(ale->data);
slot->set_selected(true);
action->slot_active_set(slot->handle);
}
}
}
else if (ac->datatype == ANIMCONT_GPENCIL) {

View File

@@ -88,6 +88,8 @@ const EnumPropertyItem rna_enum_strip_type_items[] = {
# include "WM_api.hh"
# include "UI_interface_icons.hh"
# include "DEG_depsgraph.hh"
# include "ANIM_keyframing.hh"
@@ -103,6 +105,7 @@ static animrig::Action &rna_action(const PointerRNA *ptr)
static animrig::Slot &rna_data_slot(const PointerRNA *ptr)
{
BLI_assert(ptr->type == &RNA_ActionSlot);
return reinterpret_cast<ActionSlot *>(ptr->data)->wrap();
}
@@ -145,6 +148,32 @@ static void rna_iterator_array_begin(CollectionPropertyIterator *iter, MutableSp
rna_iterator_array_begin(iter, (void *)items.data(), sizeof(T *), items.size(), 0, nullptr);
}
static PointerRNA rna_ActionSlots_active_get(PointerRNA *ptr)
{
animrig::Action &action = rna_action(ptr);
animrig::Slot *active_slot = action.slot_active_get();
if (!active_slot) {
return PointerRNA_NULL;
}
return RNA_pointer_create(&action.id, &RNA_ActionSlot, active_slot);
}
static void rna_ActionSlots_active_set(PointerRNA *ptr,
PointerRNA value,
struct ReportList * /*reports*/)
{
animrig::Action &action = rna_action(ptr);
if (value.data) {
animrig::Slot &slot = rna_data_slot(&value);
action.slot_active_set(slot.handle);
}
else {
action.slot_active_set(animrig::Slot::unassigned);
}
}
static ActionSlot *rna_Action_slots_new(bAction *dna_action,
bContext *C,
ReportList *reports,
@@ -249,6 +278,13 @@ static std::optional<std::string> rna_ActionSlot_path(const PointerRNA *ptr)
return fmt::format("slots[\"{}\"]", name_esc);
}
int rna_ActionSlot_idtype_icon_get(PointerRNA *ptr)
{
animrig::Slot &slot = rna_data_slot(ptr);
return UI_icon_from_idcode(slot.idtype);
;
}
/* Name functions that ignore the first two ID characters */
void rna_ActionSlot_name_display_get(PointerRNA *ptr, char *value)
{
@@ -1163,6 +1199,7 @@ static void rna_def_dopesheet(BlenderRNA *brna)
static void rna_def_action_slots(BlenderRNA *brna, PropertyRNA *cprop)
{
StructRNA *srna;
PropertyRNA *prop;
FunctionRNA *func;
PropertyRNA *parm;
@@ -1172,6 +1209,15 @@ static void rna_def_action_slots(BlenderRNA *brna, PropertyRNA *cprop)
RNA_def_struct_sdna(srna, "bAction");
RNA_def_struct_ui_text(srna, "Action Slots", "Collection of action slots");
prop = RNA_def_property(srna, "active", PROP_POINTER, PROP_NONE);
RNA_def_property_struct_type(prop, "ActionSlot");
RNA_def_property_flag(prop, PROP_EDITABLE);
RNA_def_property_pointer_funcs(
prop, "rna_ActionSlots_active_get", "rna_ActionSlots_active_set", nullptr, nullptr);
RNA_def_property_update_notifier(prop, NC_ANIMATION | ND_ANIMCHAN);
RNA_def_property_ui_text(prop, "Active Slot", "Active slot for this action");
/* Animation.slots.new(...) */
func = RNA_def_function(srna, "new", "rna_Action_slots_new");
RNA_def_function_ui_description(func, "Add a slot to the animation");
@@ -1246,11 +1292,15 @@ static void rna_def_action_slot(BlenderRNA *brna)
RNA_def_property_string_funcs(prop, nullptr, nullptr, "rna_ActionSlot_name_set");
RNA_def_property_string_maxlength(prop, sizeof(ActionSlot::name) - 2);
RNA_def_property_update(prop, NC_ANIMATION | ND_ANIMCHAN, "rna_ActionSlot_name_update");
RNA_def_struct_ui_text(
srna,
RNA_def_property_ui_text(
prop,
"Slot Name",
"Used when connecting an Action to a data-block, to find the correct slot handle");
prop = RNA_def_property(srna, "idtype_icon", PROP_INT, PROP_NONE);
RNA_def_property_int_funcs(prop, "rna_ActionSlot_idtype_icon_get", nullptr, nullptr);
RNA_def_property_clear_flag(prop, PROP_EDITABLE);
prop = RNA_def_property(srna, "name_display", PROP_STRING, PROP_NONE);
RNA_def_property_string_funcs(prop,
"rna_ActionSlot_name_display_get",
@@ -1258,19 +1308,29 @@ static void rna_def_action_slot(BlenderRNA *brna)
"rna_ActionSlot_name_display_set");
RNA_def_property_string_maxlength(prop, sizeof(ActionSlot::name) - 2);
RNA_def_property_update(prop, NC_ANIMATION | ND_ANIMCHAN, "rna_ActionSlot_name_update");
RNA_def_struct_ui_text(
srna,
RNA_def_property_ui_text(
prop,
"Slot Display Name",
"Name of the slot for showing in the interface. It is the name, without the first two "
"characters that identify what kind of data-block it animates");
prop = RNA_def_property(srna, "handle", PROP_INT, PROP_NONE);
RNA_def_property_clear_flag(prop, PROP_EDITABLE);
RNA_def_struct_ui_text(srna,
"Slot Handle",
"Number specific to this Slot, unique within the Action"
"This is used, for example, on a KeyframeActionStrip to look up the "
"ActionChannelBag for this Slot");
RNA_def_property_ui_text(prop,
"Slot Handle",
"Number specific to this Slot, unique within the Action"
"This is used, for example, on a KeyframeActionStrip to look up the "
"ActionChannelBag for this Slot");
prop = RNA_def_property(srna, "active", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_boolean_sdna(prop, nullptr, "slot_flags", int(animrig::Slot::Flags::Active));
RNA_def_property_ui_text(
prop,
"Active",
"Whether this is the active slot, can be set by assigning to action.slots.active");
RNA_def_property_flag(prop, PROP_NO_DEG_UPDATE);
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE | PROP_EDITABLE);
RNA_def_property_update_notifier(prop, NC_ANIMATION | ND_ANIMCHAN | NA_SELECTED);
prop = RNA_def_property(srna, "select", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_boolean_sdna(prop, nullptr, "slot_flags", int(animrig::Slot::Flags::Selected));