Anim: add Action+Slot selector for Masks

This adds an Animation panel to the Mask tab of the n-panel of the
Movie Clip and Image editors. Masks can be animated (for example, the
opacity of a Mask Layer), but there was no way to manage the Action
and Slot that held those F-Curves.

To keep things DRY, this PR also moves the code for drawing Action+Slot
selectors from the `PropertiesAnimationMixin` class to a utility
function, which is now called from both that class and the Mask UI code.

Pull Request: https://projects.blender.org/blender/blender/pulls/133153
This commit is contained in:
Nathan Vegdahl 2025-01-21 15:12:59 +01:00 committed by Nathan Vegdahl
parent 18f145d1ac
commit b2a06888c7
5 changed files with 71 additions and 15 deletions

View file

@ -5,6 +5,33 @@
from bpy.types import Menu from bpy.types import Menu
def draw_action_and_slot_selector_for_id(layout, animated_id):
"""
Draw the action and slot selector for an ID, using the given layout.
The ID must be an animatable ID.
Note that the slot selector is only drawn when the ID has an assigned
Action.
"""
layout.template_action(animated_id, new="action.new", unlink="action.unlink")
adt = animated_id.animation_data
if not adt or not adt.action:
return
# Only show the slot selector when a layered Action is assigned.
if adt.action.is_action_layered:
layout.context_pointer_set("animated_id", animated_id)
layout.template_search(
adt, "action_slot",
adt, "action_suitable_slots",
new="anim.slot_new_for_id",
unlink="anim.slot_unassign_from_id",
)
class ANIM_MT_keyframe_insert_pie(Menu): class ANIM_MT_keyframe_insert_pie(Menu):
bl_label = "Keyframe Insert Pie" bl_label = "Keyframe Insert Pie"

View file

@ -7,6 +7,7 @@
from bpy.types import Menu, UIList from bpy.types import Menu, UIList
from bpy.app.translations import contexts as i18n_contexts from bpy.app.translations import contexts as i18n_contexts
from . import anim
# Use by both image & clip context menus. # Use by both image & clip context menus.
@ -222,6 +223,31 @@ class MASK_PT_point:
) )
class MASK_PT_animation:
# subclasses must define...
# ~ bl_space_type = 'CLIP_EDITOR'
# ~ bl_region_type = 'UI'
bl_label = "Animation"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
space_data = context.space_data
return space_data.mask and space_data.mode == 'MASK'
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
# poll() ensures this is not None.
sc = context.space_data
mask = sc.mask
col = layout.column(align=True)
anim.draw_action_and_slot_selector_for_id(col, mask)
class MASK_PT_display: class MASK_PT_display:
# subclasses must define... # subclasses must define...
# ~ bl_space_type = 'CLIP_EDITOR' # ~ bl_space_type = 'CLIP_EDITOR'

View file

@ -1191,6 +1191,7 @@ from bl_ui.properties_mask_common import (
MASK_PT_layers, MASK_PT_layers,
MASK_PT_spline, MASK_PT_spline,
MASK_PT_point, MASK_PT_point,
MASK_PT_animation,
MASK_PT_display, MASK_PT_display,
MASK_PT_transforms, MASK_PT_transforms,
MASK_PT_tools, MASK_PT_tools,
@ -1215,6 +1216,12 @@ class CLIP_PT_active_mask_point(MASK_PT_point, Panel):
bl_category = "Mask" bl_category = "Mask"
class CLIP_PT_mask_animation(MASK_PT_animation, Panel):
bl_space_type = 'CLIP_EDITOR'
bl_region_type = 'UI'
bl_category = "Mask"
class CLIP_PT_mask(MASK_PT_mask, Panel): class CLIP_PT_mask(MASK_PT_mask, Panel):
bl_space_type = 'CLIP_EDITOR' bl_space_type = 'CLIP_EDITOR'
bl_region_type = 'UI' bl_region_type = 'UI'
@ -1996,6 +2003,7 @@ classes = (
CLIP_PT_mask_display, CLIP_PT_mask_display,
CLIP_PT_active_mask_spline, CLIP_PT_active_mask_spline,
CLIP_PT_active_mask_point, CLIP_PT_active_mask_point,
CLIP_PT_mask_animation,
CLIP_PT_tools_mask_transforms, CLIP_PT_tools_mask_transforms,
CLIP_PT_tools_mask_tools, CLIP_PT_tools_mask_tools,
CLIP_PT_tools_scenesetup, CLIP_PT_tools_scenesetup,

View file

@ -987,6 +987,7 @@ from bl_ui.properties_mask_common import (
MASK_PT_layers, MASK_PT_layers,
MASK_PT_spline, MASK_PT_spline,
MASK_PT_point, MASK_PT_point,
MASK_PT_animation,
MASK_PT_display, MASK_PT_display,
) )
@ -1015,6 +1016,12 @@ class IMAGE_PT_active_mask_point(MASK_PT_point, Panel):
bl_category = "Mask" bl_category = "Mask"
class IMAGE_PT_mask_animation(MASK_PT_animation, Panel):
bl_space_type = 'IMAGE_EDITOR'
bl_region_type = 'UI'
bl_category = "Mask"
class IMAGE_PT_mask_display(MASK_PT_display, Panel): class IMAGE_PT_mask_display(MASK_PT_display, Panel):
bl_space_type = 'IMAGE_EDITOR' bl_space_type = 'IMAGE_EDITOR'
bl_region_type = 'HEADER' bl_region_type = 'HEADER'
@ -1771,6 +1778,7 @@ classes = (
IMAGE_PT_mask_display, IMAGE_PT_mask_display,
IMAGE_PT_active_mask_spline, IMAGE_PT_active_mask_spline,
IMAGE_PT_active_mask_point, IMAGE_PT_active_mask_point,
IMAGE_PT_mask_animation,
IMAGE_PT_snapping, IMAGE_PT_snapping,
IMAGE_PT_proportional_edit, IMAGE_PT_proportional_edit,
IMAGE_PT_image_properties, IMAGE_PT_image_properties,

View file

@ -4,6 +4,7 @@
from bpy.types import Header, Panel from bpy.types import Header, Panel
from rna_prop_ui import PropertyPanel from rna_prop_ui import PropertyPanel
from . import anim
class PROPERTIES_HT_header(Header): class PROPERTIES_HT_header(Header):
@ -122,21 +123,7 @@ class PropertiesAnimationMixin:
layout.label(text="No animatable data-block, please report as bug", icon='ERROR') layout.label(text="No animatable data-block, please report as bug", icon='ERROR')
return return
layout.template_action(animated_id, new="action.new", unlink="action.unlink") anim.draw_action_and_slot_selector_for_id(layout, animated_id)
adt = animated_id.animation_data
if not adt or not adt.action:
return
# Only show the slot selector when a layered Action is assigned.
if adt.action.is_action_layered:
layout.context_pointer_set("animated_id", animated_id)
layout.template_search(
adt, "action_slot",
adt, "action_suitable_slots",
new="anim.slot_new_for_id",
unlink="anim.slot_unassign_from_id",
)
classes = ( classes = (