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:
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user