This patch adds snapping options for the playhead to all animation editors. The options can be modified through a new dropdown in the editor header. All editors will show all those options, and they are shared, so toggling the option in on editor will change it for all other editors too. Some options are not working/relevant in some editors for example Strips in the Dope Sheet. However for consistency the option is still shown. This is a separate menu from the transform snapping menu because you can toggle the snapping for transform and playhead separately. Putting it in the existing snapping transform menu would imply that it can be turned off with the magnet which is not the case. Playhead snapping is explicitly disabled for the drivers editor because there is no playhead to drag around. Snapping to Frame/Second intervals takes the scene start as a starting point. That means you can snap to the n-th second of the animation even though it might not start at frame 1. The preview range is NOT taken into account by design since the use case is working on a sub-section of the animation in which case the snap target should not change. Snapping is toggled by pressing CTRL as indicated by the status bar. Snapping to Frames/Seconds is absolute, meaning no matter how far away your cursor it will snap to the closest snap point. All others only snap to things if they are close to the cursor in pixel values. When mixing those two behaviors, it prefers relative snapping. If no point is close enough to snap relative, it will fall back to absolute snapping. Based on the prototype #135913 Part of #135794 Pull Request: https://projects.blender.org/blender/blender/pulls/137278
1022 lines
33 KiB
Python
1022 lines
33 KiB
Python
# SPDX-FileCopyrightText: 2009-2023 Blender Authors
|
|
#
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
import bpy
|
|
from bpy.types import (
|
|
Header,
|
|
Menu,
|
|
Panel,
|
|
)
|
|
|
|
from bl_ui.properties_data_grease_pencil import (
|
|
GreasePencil_LayerMaskPanel,
|
|
GreasePencil_LayerTransformPanel,
|
|
GreasePencil_LayerRelationsPanel,
|
|
GreasePencil_LayerAdjustmentsPanel,
|
|
GreasePencil_LayerDisplayPanel,
|
|
)
|
|
from bl_ui.space_toolsystem_common import PlayheadSnappingPanel
|
|
|
|
|
|
from rna_prop_ui import PropertyPanel
|
|
|
|
#######################################
|
|
# DopeSheet Filtering - Header Buttons
|
|
|
|
# used for DopeSheet, NLA, and Graph Editors
|
|
|
|
|
|
def dopesheet_filter(layout, context):
|
|
dopesheet = context.space_data.dopesheet
|
|
is_nla = context.area.type == 'NLA_EDITOR'
|
|
is_action_editor = not is_nla and context.space_data.mode == 'ACTION'
|
|
|
|
row = layout.row(align=True)
|
|
if is_action_editor:
|
|
row.prop(dopesheet, "show_only_slot_of_active_object", text="")
|
|
row.prop(dopesheet, "show_only_selected", text="")
|
|
row.prop(dopesheet, "show_hidden", text="")
|
|
|
|
if is_nla:
|
|
row.prop(dopesheet, "show_missing_nla", text="")
|
|
else: # graph and dopesheet editors - F-Curves and drivers only
|
|
row.prop(dopesheet, "show_only_errors", text="")
|
|
|
|
#######################################
|
|
# Dope-sheet Filtering Popovers
|
|
|
|
# Generic Layout - Used as base for filtering popovers used in all animation editors
|
|
# Used for DopeSheet, NLA, and Graph Editors
|
|
|
|
|
|
class DopesheetFilterPopoverBase:
|
|
bl_region_type = 'HEADER'
|
|
bl_label = "Filters"
|
|
|
|
# Generic = Affects all data-types.
|
|
# XXX: Perhaps we want these to stay in the header instead, for easy/fast access
|
|
@classmethod
|
|
def draw_generic_filters(cls, context, layout):
|
|
dopesheet = context.space_data.dopesheet
|
|
is_nla = context.area.type == 'NLA_EDITOR'
|
|
|
|
col = layout.column(align=True)
|
|
col.prop(dopesheet, "show_only_selected", icon='NONE')
|
|
col.prop(dopesheet, "show_hidden", icon='NONE')
|
|
|
|
if is_nla:
|
|
col.prop(dopesheet, "show_missing_nla", icon='NONE')
|
|
else: # Graph and dope-sheet editors - F-Curves and drivers only.
|
|
col.prop(dopesheet, "show_only_errors", icon='NONE')
|
|
|
|
# Name/Membership Filters
|
|
# XXX: Perhaps these should just stay in the headers (exclusively)?
|
|
@classmethod
|
|
def draw_search_filters(cls, context, layout, generic_filters_only=False):
|
|
dopesheet = context.space_data.dopesheet
|
|
is_nla = context.area.type == 'NLA_EDITOR'
|
|
|
|
col = layout.column(align=True)
|
|
if not is_nla:
|
|
row = col.row(align=True)
|
|
row.prop(dopesheet, "filter_fcurve_name", text="")
|
|
else:
|
|
row = col.row(align=True)
|
|
row.prop(dopesheet, "filter_text", text="")
|
|
|
|
if (not generic_filters_only) and bpy.data.collections:
|
|
col = layout.column(align=True)
|
|
col.prop(dopesheet, "filter_collection", text="")
|
|
|
|
# Standard = Present in all panels
|
|
@classmethod
|
|
def draw_standard_filters(cls, context, layout):
|
|
dopesheet = context.space_data.dopesheet
|
|
|
|
# datablock filters
|
|
layout.label(text="Filter by Type:")
|
|
flow = layout.grid_flow(row_major=True, columns=2, even_rows=False, align=False)
|
|
|
|
flow.prop(dopesheet, "show_scenes", text="Scenes")
|
|
flow.prop(dopesheet, "show_nodes", text="Node Trees")
|
|
|
|
# object types
|
|
if bpy.data.armatures:
|
|
flow.prop(dopesheet, "show_armatures", text="Armatures")
|
|
if bpy.data.cameras:
|
|
flow.prop(dopesheet, "show_cameras", text="Cameras")
|
|
if bpy.data.grease_pencils_v3:
|
|
flow.prop(dopesheet, "show_gpencil", text="Grease Pencil Objects")
|
|
if bpy.data.lights:
|
|
flow.prop(dopesheet, "show_lights", text="Lights")
|
|
if bpy.data.meshes:
|
|
flow.prop(dopesheet, "show_meshes", text="Meshes")
|
|
if bpy.data.curves:
|
|
flow.prop(dopesheet, "show_curves", text="Curves")
|
|
if bpy.data.lattices:
|
|
flow.prop(dopesheet, "show_lattices", text="Lattices")
|
|
if bpy.data.metaballs:
|
|
flow.prop(dopesheet, "show_metaballs", text="Metaballs")
|
|
if hasattr(bpy.data, "hair_curves") and bpy.data.hair_curves:
|
|
flow.prop(dopesheet, "show_hair_curves", text="Hair Curves")
|
|
if hasattr(bpy.data, "pointclouds") and bpy.data.pointclouds:
|
|
flow.prop(dopesheet, "show_pointclouds", text="Point Clouds")
|
|
if bpy.data.volumes:
|
|
flow.prop(dopesheet, "show_volumes", text="Volumes")
|
|
|
|
# data types
|
|
flow.prop(dopesheet, "show_worlds", text="Worlds")
|
|
if bpy.data.particles:
|
|
flow.prop(dopesheet, "show_particles", text="Particles")
|
|
if bpy.data.linestyles:
|
|
flow.prop(dopesheet, "show_linestyles", text="Line Styles")
|
|
if bpy.data.speakers:
|
|
flow.prop(dopesheet, "show_speakers", text="Speakers")
|
|
if bpy.data.materials:
|
|
flow.prop(dopesheet, "show_materials", text="Materials")
|
|
if bpy.data.textures:
|
|
flow.prop(dopesheet, "show_textures", text="Textures")
|
|
if bpy.data.shape_keys:
|
|
flow.prop(dopesheet, "show_shapekeys", text="Shape Keys")
|
|
if bpy.data.cache_files:
|
|
flow.prop(dopesheet, "show_cache_files", text="Cache Files")
|
|
if bpy.data.movieclips:
|
|
flow.prop(dopesheet, "show_movieclips", text="Movie Clips")
|
|
|
|
layout.separator()
|
|
|
|
# Object Data Filters
|
|
|
|
# TODO: Add per-channel/axis convenience toggles?
|
|
split = layout.split()
|
|
|
|
col = split.column()
|
|
col.prop(dopesheet, "show_transforms", text="Transforms")
|
|
|
|
col = split.column()
|
|
col.prop(dopesheet, "show_modifiers", text="Modifiers")
|
|
|
|
layout.separator()
|
|
|
|
# performance-related options (users will mostly have these enabled)
|
|
col = layout.column(align=True)
|
|
col.label(text="Options:")
|
|
col.prop(dopesheet, "use_datablock_sort", icon='NONE')
|
|
|
|
|
|
# Popover for Dope-sheet Editor(s) - Dope-sheet, Action, Shape-key, GPencil, Mask, etc.
|
|
class DOPESHEET_PT_filters(DopesheetFilterPopoverBase, Panel):
|
|
bl_space_type = 'DOPESHEET_EDITOR'
|
|
bl_region_type = 'HEADER'
|
|
bl_label = "Filters"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
dopesheet = context.space_data.dopesheet
|
|
ds_mode = context.space_data.mode
|
|
|
|
layout.prop(dopesheet, "show_summary", text="Summary")
|
|
|
|
DopesheetFilterPopoverBase.draw_generic_filters(context, layout)
|
|
|
|
if ds_mode in {'DOPESHEET', 'ACTION', 'GPENCIL'}:
|
|
layout.separator()
|
|
generic_filters_only = ds_mode != 'DOPESHEET'
|
|
DopesheetFilterPopoverBase.draw_search_filters(context, layout, generic_filters_only=generic_filters_only)
|
|
|
|
if ds_mode == 'DOPESHEET':
|
|
layout.separator()
|
|
DopesheetFilterPopoverBase.draw_standard_filters(context, layout)
|
|
|
|
|
|
#######################################
|
|
# DopeSheet Editor - General/Standard UI
|
|
|
|
class DOPESHEET_HT_header(Header):
|
|
bl_space_type = 'DOPESHEET_EDITOR'
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
st = context.space_data
|
|
|
|
layout.template_header()
|
|
|
|
if st.mode == 'TIMELINE':
|
|
from bl_ui.space_time import (
|
|
TIME_MT_editor_menus,
|
|
TIME_HT_editor_buttons,
|
|
)
|
|
TIME_MT_editor_menus.draw_collapsible(context, layout)
|
|
TIME_HT_editor_buttons.draw_header(context, layout)
|
|
else:
|
|
layout.prop(st, "ui_mode", text="")
|
|
|
|
DOPESHEET_MT_editor_menus.draw_collapsible(context, layout)
|
|
DOPESHEET_HT_editor_buttons.draw_header(context, layout)
|
|
|
|
|
|
class DOPESHEET_PT_playhead_snapping(PlayheadSnappingPanel, Panel):
|
|
bl_space_type = 'DOPESHEET_EDITOR'
|
|
|
|
|
|
# Header for "normal" dopesheet editor modes (e.g. Dope Sheet, Action, Shape Keys, etc.)
|
|
class DOPESHEET_HT_editor_buttons:
|
|
|
|
@classmethod
|
|
def draw_header(cls, context, layout):
|
|
st = context.space_data
|
|
tool_settings = context.tool_settings
|
|
|
|
if st.mode in {'ACTION', 'SHAPEKEY'} and context.object:
|
|
layout.separator_spacer()
|
|
cls._draw_action_selector(context, layout)
|
|
|
|
# Layer management
|
|
if st.mode == 'GPENCIL':
|
|
ob = context.active_object
|
|
|
|
enable_but = ob is not None and ob.type == 'GREASEPENCIL'
|
|
|
|
row = layout.row(align=True)
|
|
row.enabled = enable_but
|
|
row.operator("grease_pencil.layer_add", icon='ADD', text="")
|
|
row.operator("grease_pencil.layer_remove", icon='REMOVE', text="")
|
|
row.menu("GREASE_PENCIL_MT_grease_pencil_add_layer_extra", icon='DOWNARROW_HLT', text="")
|
|
|
|
row = layout.row(align=True)
|
|
row.enabled = enable_but
|
|
row.operator("anim.channels_move", icon='TRIA_UP', text="").direction = 'UP'
|
|
row.operator("anim.channels_move", icon='TRIA_DOWN', text="").direction = 'DOWN'
|
|
|
|
row = layout.row(align=True)
|
|
row.enabled = enable_but
|
|
row.operator("grease_pencil.layer_isolate", icon='RESTRICT_VIEW_ON', text="").affect_visibility = True
|
|
row.operator("grease_pencil.layer_isolate", icon='LOCKED', text="").affect_visibility = False
|
|
|
|
layout.separator_spacer()
|
|
|
|
if st.mode == 'DOPESHEET':
|
|
dopesheet_filter(layout, context)
|
|
elif st.mode == 'ACTION':
|
|
dopesheet_filter(layout, context)
|
|
elif st.mode == 'GPENCIL':
|
|
row = layout.row(align=True)
|
|
row.prop(st.dopesheet, "show_only_selected", text="")
|
|
row.prop(st.dopesheet, "show_hidden", text="")
|
|
|
|
layout.popover(
|
|
panel="DOPESHEET_PT_filters",
|
|
text="",
|
|
icon='FILTER',
|
|
)
|
|
|
|
# Grease Pencil mode doesn't need snapping, as it's frame-aligned only
|
|
if st.mode != 'GPENCIL':
|
|
row = layout.row(align=True)
|
|
row.prop(tool_settings, "use_snap_anim", text="")
|
|
sub = row.row(align=True)
|
|
sub.popover(
|
|
panel="DOPESHEET_PT_snapping",
|
|
text="",
|
|
)
|
|
|
|
layout.popover(panel="DOPESHEET_PT_playhead_snapping")
|
|
|
|
row = layout.row(align=True)
|
|
row.prop(tool_settings, "use_proportional_action", text="", icon_only=True)
|
|
sub = row.row(align=True)
|
|
sub.active = tool_settings.use_proportional_action
|
|
sub.prop_with_popover(
|
|
tool_settings,
|
|
"proportional_edit_falloff",
|
|
text="",
|
|
icon_only=True,
|
|
panel="DOPESHEET_PT_proportional_edit",
|
|
)
|
|
|
|
@classmethod
|
|
def _draw_action_selector(cls, context, layout):
|
|
animated_id = cls._get_animated_id(context)
|
|
if not animated_id:
|
|
return
|
|
|
|
row = layout.row()
|
|
if animated_id.animation_data and animated_id.animation_data.use_tweak_mode:
|
|
row.enabled = False
|
|
|
|
row.template_action(animated_id, new="action.new", unlink="action.unlink")
|
|
|
|
adt = animated_id and animated_id.animation_data
|
|
if not adt or not adt.action or not adt.action.is_action_layered:
|
|
return
|
|
|
|
# Store the animated ID in the context, so that the new/unlink operators
|
|
# have access to it.
|
|
row.context_pointer_set("animated_id", animated_id)
|
|
row.template_search(
|
|
adt, "action_slot",
|
|
adt, "action_suitable_slots",
|
|
new="anim.slot_new_for_id",
|
|
unlink="anim.slot_unassign_from_id",
|
|
)
|
|
|
|
@staticmethod
|
|
def _get_animated_id(context):
|
|
st = context.space_data
|
|
match st.mode:
|
|
case 'ACTION':
|
|
return context.object
|
|
case 'SHAPEKEY':
|
|
return getattr(context.object.data, "shape_keys", None)
|
|
case _:
|
|
print("Dope Sheet mode '{:s}' not expected to have an Action selector".format(st.mode))
|
|
return context.object
|
|
|
|
|
|
class DOPESHEET_PT_snapping(Panel):
|
|
bl_space_type = 'DOPESHEET_EDITOR'
|
|
bl_region_type = 'HEADER'
|
|
bl_label = "Snapping"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
col = layout.column()
|
|
col.label(text="Snap To")
|
|
tool_settings = context.tool_settings
|
|
col.prop(tool_settings, "snap_anim_element", expand=True)
|
|
if tool_settings.snap_anim_element != 'MARKER':
|
|
col.prop(tool_settings, "use_snap_time_absolute")
|
|
|
|
|
|
class DOPESHEET_PT_proportional_edit(Panel):
|
|
bl_space_type = 'DOPESHEET_EDITOR'
|
|
bl_region_type = 'HEADER'
|
|
bl_label = "Proportional Editing"
|
|
bl_ui_units_x = 8
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
tool_settings = context.tool_settings
|
|
col = layout.column()
|
|
col.active = tool_settings.use_proportional_action
|
|
|
|
col.prop(tool_settings, "proportional_edit_falloff", expand=True)
|
|
col.prop(tool_settings, "proportional_size")
|
|
|
|
|
|
class DOPESHEET_MT_editor_menus(Menu):
|
|
bl_idname = "DOPESHEET_MT_editor_menus"
|
|
bl_label = ""
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
st = context.space_data
|
|
|
|
layout.menu("DOPESHEET_MT_view")
|
|
layout.menu("DOPESHEET_MT_select")
|
|
if st.show_markers:
|
|
layout.menu("DOPESHEET_MT_marker")
|
|
|
|
if st.mode == 'DOPESHEET' or (st.mode == 'ACTION' and st.action is not None):
|
|
layout.menu("DOPESHEET_MT_channel")
|
|
elif st.mode == 'GPENCIL':
|
|
layout.menu("DOPESHEET_MT_gpencil_channel")
|
|
|
|
layout.menu("DOPESHEET_MT_key")
|
|
|
|
if st.mode in {'ACTION', 'SHAPEKEY'} and st.action is not None:
|
|
layout.menu("DOPESHEET_MT_action")
|
|
|
|
|
|
class DOPESHEET_MT_view(Menu):
|
|
bl_label = "View"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
st = context.space_data
|
|
|
|
layout.prop(st, "show_region_ui")
|
|
layout.prop(st, "show_region_hud")
|
|
layout.prop(st, "show_region_channels")
|
|
layout.separator()
|
|
|
|
layout.operator("action.view_selected")
|
|
layout.operator("action.view_all")
|
|
if context.scene.use_preview_range:
|
|
layout.operator("anim.scene_range_frame", text="Frame Preview Range")
|
|
else:
|
|
layout.operator("anim.scene_range_frame", text="Frame Scene Range")
|
|
layout.operator("action.view_frame")
|
|
layout.separator()
|
|
|
|
layout.prop(st.dopesheet, "use_multi_word_filter", text="Multi-Word Match Search")
|
|
layout.separator()
|
|
|
|
layout.prop(st, "use_realtime_update")
|
|
|
|
# Sliders are always shown in the Shape Key Editor regardless of this setting.
|
|
col = layout.column()
|
|
col.active = context.space_data.mode != 'SHAPEKEY'
|
|
col.prop(st, "show_sliders")
|
|
|
|
layout.prop(st, "show_interpolation")
|
|
layout.prop(st, "show_extremes")
|
|
layout.prop(st, "use_auto_merge_keyframes")
|
|
layout.separator()
|
|
|
|
layout.prop(st, "show_markers")
|
|
layout.prop(st, "show_seconds")
|
|
layout.prop(st, "show_locked_time")
|
|
layout.separator()
|
|
|
|
layout.operator("anim.previewrange_set")
|
|
layout.operator("anim.previewrange_clear")
|
|
layout.operator("action.previewrange_set")
|
|
layout.separator()
|
|
|
|
# Add this to show key-binding (reverse action in dope-sheet).
|
|
props = layout.operator("wm.context_set_enum", text="Toggle Graph Editor", icon='GRAPH')
|
|
props.data_path = "area.type"
|
|
props.value = 'GRAPH_EDITOR'
|
|
layout.separator()
|
|
|
|
layout.menu("INFO_MT_area")
|
|
|
|
|
|
class DOPESHEET_MT_view_pie(Menu):
|
|
bl_label = "View"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
pie = layout.menu_pie()
|
|
pie.operator("action.view_all")
|
|
pie.operator("action.view_selected", icon='ZOOM_SELECTED')
|
|
pie.operator("action.view_frame")
|
|
if context.scene.use_preview_range:
|
|
pie.operator("anim.scene_range_frame", text="Frame Preview Range")
|
|
else:
|
|
pie.operator("anim.scene_range_frame", text="Frame Scene Range")
|
|
|
|
|
|
class DOPESHEET_MT_select(Menu):
|
|
bl_label = "Select"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
layout.operator("action.select_all", text="All").action = 'SELECT'
|
|
layout.operator("action.select_all", text="None").action = 'DESELECT'
|
|
layout.operator("action.select_all", text="Invert").action = 'INVERT'
|
|
|
|
layout.separator()
|
|
layout.operator("action.select_box").axis_range = False
|
|
layout.operator("action.select_box", text="Box Select (Axis Range)").axis_range = True
|
|
layout.operator("action.select_circle")
|
|
layout.operator_menu_enum("action.select_lasso", "mode")
|
|
|
|
# FIXME: grease pencil mode isn't supported for these yet, so skip for that mode only
|
|
if context.space_data.mode != 'GPENCIL':
|
|
layout.separator()
|
|
layout.operator("action.select_more", text="More")
|
|
layout.operator("action.select_less", text="Less")
|
|
|
|
layout.separator()
|
|
layout.operator("action.select_linked")
|
|
|
|
layout.separator()
|
|
layout.operator("action.select_column", text="Columns on Selected Keys").mode = 'KEYS'
|
|
layout.operator("action.select_column", text="Column on Current Frame").mode = 'CFRA'
|
|
|
|
layout.operator("action.select_column", text="Columns on Selected Markers").mode = 'MARKERS_COLUMN'
|
|
layout.operator("action.select_column", text="Between Selected Markers").mode = 'MARKERS_BETWEEN'
|
|
|
|
layout.separator()
|
|
props = layout.operator("action.select_leftright", text="Before Current Frame")
|
|
props.extend = False
|
|
props.mode = 'LEFT'
|
|
props = layout.operator("action.select_leftright", text="After Current Frame")
|
|
props.extend = False
|
|
props.mode = 'RIGHT'
|
|
|
|
|
|
class DOPESHEET_MT_marker(Menu):
|
|
bl_label = "Marker"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
from bl_ui.space_time import marker_menu_generic
|
|
marker_menu_generic(layout, context)
|
|
|
|
st = context.space_data
|
|
|
|
if st.mode in {'ACTION', 'SHAPEKEY'} and st.action:
|
|
layout.separator()
|
|
layout.prop(st, "show_pose_markers")
|
|
|
|
if st.show_pose_markers is False:
|
|
layout.operator("action.markers_make_local")
|
|
|
|
layout.prop(st, "use_marker_sync")
|
|
|
|
#######################################
|
|
# Keyframe Editing
|
|
|
|
|
|
class DOPESHEET_MT_channel(Menu):
|
|
bl_label = "Channel"
|
|
|
|
def draw(self, _context):
|
|
layout = self.layout
|
|
|
|
layout.operator_context = 'INVOKE_REGION_CHANNELS'
|
|
|
|
layout.operator("anim.channels_delete")
|
|
layout.operator("action.clean", text="Clean Channels").channels = True
|
|
|
|
layout.separator()
|
|
layout.operator("anim.channels_group")
|
|
layout.operator("anim.channels_ungroup")
|
|
|
|
layout.separator()
|
|
layout.operator_menu_enum("anim.channels_setting_toggle", "type")
|
|
layout.operator_menu_enum("anim.channels_setting_enable", "type")
|
|
layout.operator_menu_enum("anim.channels_setting_disable", "type")
|
|
|
|
layout.separator()
|
|
layout.operator("anim.channels_editable_toggle")
|
|
layout.operator_menu_enum("action.extrapolation_type", "type", text="Extrapolation Mode")
|
|
|
|
layout.separator()
|
|
layout.operator("anim.channels_expand")
|
|
layout.operator("anim.channels_collapse")
|
|
|
|
layout.separator()
|
|
layout.operator_menu_enum("anim.channels_move", "direction", text="Move...")
|
|
|
|
layout.separator()
|
|
layout.operator("anim.channels_fcurves_enable")
|
|
|
|
layout.separator()
|
|
layout.operator("anim.channels_bake")
|
|
|
|
layout.separator()
|
|
layout.operator("anim.channels_view_selected")
|
|
|
|
|
|
class DOPESHEET_MT_action(Menu):
|
|
bl_label = "Action"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.operator("anim.merge_animation")
|
|
layout.operator("anim.separate_slots")
|
|
|
|
layout.separator()
|
|
layout.operator("anim.slot_channels_move_to_new_action")
|
|
|
|
layout.separator()
|
|
layout.operator("action.push_down", text="Push Down Action", icon='NLA_PUSHDOWN')
|
|
layout.operator("action.stash", text="Stash Action", icon='FREEZE')
|
|
|
|
|
|
class DOPESHEET_MT_key(Menu):
|
|
bl_label = "Key"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
ob = context.active_object
|
|
|
|
layout.menu("DOPESHEET_MT_key_transform", text="Transform")
|
|
|
|
layout.operator_menu_enum("action.snap", "type", text="Snap")
|
|
layout.operator_menu_enum("action.mirror", "type", text="Mirror")
|
|
|
|
layout.separator()
|
|
layout.operator("action.keyframe_insert")
|
|
|
|
layout.separator()
|
|
layout.operator("action.frame_jump")
|
|
|
|
layout.separator()
|
|
layout.operator("action.copy")
|
|
layout.operator("action.paste")
|
|
layout.operator("action.paste", text="Paste Flipped").flipped = True
|
|
layout.operator("action.duplicate_move")
|
|
layout.operator("action.delete")
|
|
if ob and ob.type == 'GREASEPENCIL':
|
|
layout.operator("grease_pencil.delete_breakdown")
|
|
|
|
layout.separator()
|
|
layout.operator_menu_enum("action.keyframe_type", "type", text="Keyframe Type")
|
|
layout.operator_menu_enum("action.handle_type", "type", text="Handle Type")
|
|
layout.operator_menu_enum("action.interpolation_type", "type", text="Interpolation Mode")
|
|
layout.operator_menu_enum("action.easing_type", "type", text="Easing Mode")
|
|
|
|
layout.separator()
|
|
layout.operator("action.clean").channels = False
|
|
layout.operator("action.bake_keys")
|
|
|
|
layout.separator()
|
|
layout.operator("graph.euler_filter", text="Discontinuity (Euler) Filter")
|
|
|
|
|
|
class DOPESHEET_MT_key_transform(Menu):
|
|
bl_label = "Transform"
|
|
|
|
def draw(self, _context):
|
|
layout = self.layout
|
|
|
|
layout.operator("transform.transform", text="Move").mode = 'TIME_TRANSLATE'
|
|
layout.operator("transform.transform", text="Extend").mode = 'TIME_EXTEND'
|
|
layout.operator("transform.transform", text="Slide").mode = 'TIME_SLIDE'
|
|
layout.operator("transform.transform", text="Scale").mode = 'TIME_SCALE'
|
|
|
|
|
|
class DopesheetActionPanelBase:
|
|
bl_region_type = 'UI'
|
|
bl_label = "Action"
|
|
|
|
@classmethod
|
|
def draw_generic_panel(cls, _context, layout, action):
|
|
layout.label(text=action.name, icon='ACTION', translate=False)
|
|
|
|
layout.prop(action, "use_frame_range")
|
|
|
|
col = layout.column()
|
|
col.active = action.use_frame_range
|
|
|
|
row = col.row(align=True)
|
|
row.prop(action, "frame_start", text="Start")
|
|
row.prop(action, "frame_end", text="End")
|
|
|
|
col.prop(action, "use_cyclic")
|
|
|
|
|
|
class DOPESHEET_PT_custom_props_action(PropertyPanel, Panel):
|
|
bl_space_type = 'DOPESHEET_EDITOR'
|
|
bl_category = "Action"
|
|
bl_region_type = 'UI'
|
|
bl_context = "data"
|
|
_context_path = "active_action"
|
|
_property_type = bpy.types.Action
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return bool(context.active_action)
|
|
|
|
|
|
class DOPESHEET_PT_action(DopesheetActionPanelBase, Panel):
|
|
bl_space_type = 'DOPESHEET_EDITOR'
|
|
bl_category = "Action"
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return bool(context.active_action)
|
|
|
|
def draw(self, context):
|
|
action = context.active_action
|
|
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")
|
|
|
|
# Draw the ID type of the slot.
|
|
try:
|
|
enum_items = slot.bl_rna.properties['target_id_type'].enum_items
|
|
idtype_label = enum_items[slot.target_id_type].name
|
|
except (KeyError, IndexError, AttributeError) as ex:
|
|
idtype_label = str(ex)
|
|
|
|
split = layout.split(factor=0.4)
|
|
split.alignment = 'RIGHT'
|
|
split.label(text="Type")
|
|
split.alignment = 'LEFT'
|
|
|
|
split.label(text=idtype_label, icon_value=slot.target_id_type_icon)
|
|
|
|
|
|
#######################################
|
|
# Grease Pencil Editing
|
|
|
|
class DOPESHEET_MT_gpencil_channel(Menu):
|
|
bl_label = "Channel"
|
|
|
|
def draw(self, _context):
|
|
layout = self.layout
|
|
|
|
layout.operator_context = 'INVOKE_REGION_CHANNELS'
|
|
|
|
layout.operator("anim.channels_delete")
|
|
|
|
layout.separator()
|
|
layout.operator("anim.channels_setting_toggle")
|
|
layout.operator("anim.channels_setting_enable")
|
|
layout.operator("anim.channels_setting_disable")
|
|
|
|
layout.separator()
|
|
layout.operator("anim.channels_editable_toggle")
|
|
|
|
# XXX: to be enabled when these are ready for use!
|
|
# layout.separator()
|
|
# layout.operator("anim.channels_expand")
|
|
# layout.operator("anim.channels_collapse")
|
|
|
|
layout.separator()
|
|
layout.operator_menu_enum("anim.channels_move", "direction", text="Move...")
|
|
|
|
layout.separator()
|
|
layout.operator("anim.channels_view_selected")
|
|
|
|
|
|
class DOPESHEET_MT_delete(Menu):
|
|
bl_label = "Delete"
|
|
|
|
def draw(self, _context):
|
|
layout = self.layout
|
|
|
|
layout.operator("action.delete")
|
|
|
|
layout.separator()
|
|
|
|
layout.operator("action.clean").channels = False
|
|
layout.operator("action.clean", text="Clean Channels").channels = True
|
|
|
|
|
|
class DOPESHEET_MT_context_menu(Menu):
|
|
bl_label = "Dope Sheet"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
st = context.space_data
|
|
|
|
layout.operator_context = 'INVOKE_DEFAULT'
|
|
|
|
layout.operator("action.copy", text="Copy", icon='COPYDOWN')
|
|
layout.operator("action.paste", text="Paste", icon='PASTEDOWN')
|
|
layout.operator("action.paste", text="Paste Flipped", icon='PASTEFLIPDOWN').flipped = True
|
|
|
|
layout.separator()
|
|
|
|
layout.operator_menu_enum("action.keyframe_type", "type", text="Keyframe Type")
|
|
|
|
if st.mode != 'GPENCIL':
|
|
layout.operator_menu_enum("action.handle_type", "type", text="Handle Type")
|
|
layout.operator_menu_enum("action.interpolation_type", "type", text="Interpolation Mode")
|
|
layout.operator_menu_enum("action.easing_type", "type", text="Easing Mode")
|
|
|
|
layout.separator()
|
|
|
|
layout.operator("action.keyframe_insert").type = 'SEL'
|
|
layout.operator("action.duplicate_move")
|
|
|
|
if st.mode == 'GPENCIL':
|
|
layout.separator()
|
|
layout.operator("grease_pencil.delete_breakdown")
|
|
|
|
layout.operator_context = 'EXEC_REGION_WIN'
|
|
layout.operator("action.delete")
|
|
|
|
layout.separator()
|
|
|
|
layout.operator_menu_enum("action.mirror", "type", text="Mirror")
|
|
layout.operator_menu_enum("action.snap", "type", text="Snap")
|
|
|
|
|
|
class DOPESHEET_MT_channel_context_menu(Menu):
|
|
bl_label = "Channel"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
# This menu is used from the graph editor too.
|
|
is_graph_editor = context.area.type == 'GRAPH_EDITOR'
|
|
|
|
layout.operator_context = 'INVOKE_REGION_CHANNELS'
|
|
|
|
layout.separator()
|
|
layout.operator("anim.channels_view_selected")
|
|
|
|
layout.operator("anim.channels_setting_enable", text="Mute Channels").type = 'MUTE'
|
|
layout.operator("anim.channels_setting_disable", text="Unmute Channels").type = 'MUTE'
|
|
layout.separator()
|
|
layout.operator("anim.channels_setting_enable", text="Protect Channels").type = 'PROTECT'
|
|
layout.operator("anim.channels_setting_disable", text="Unprotect Channels").type = 'PROTECT'
|
|
|
|
layout.separator()
|
|
layout.operator("anim.channels_group")
|
|
layout.operator("anim.channels_ungroup")
|
|
|
|
layout.separator()
|
|
layout.operator("anim.channels_editable_toggle")
|
|
|
|
if is_graph_editor:
|
|
operator = "graph.extrapolation_type"
|
|
else:
|
|
operator = "action.extrapolation_type"
|
|
layout.operator_menu_enum(operator, "type", text="Extrapolation Mode")
|
|
|
|
if is_graph_editor:
|
|
layout.operator_menu_enum("graph.fmodifier_add", "type", text="Add F-Curve Modifier").only_active = False
|
|
layout.separator()
|
|
layout.operator("graph.hide", text="Hide Selected Curves").unselected = False
|
|
layout.operator("graph.hide", text="Hide Unselected Curves").unselected = True
|
|
layout.operator("graph.reveal")
|
|
|
|
layout.separator()
|
|
layout.operator("anim.channels_expand")
|
|
layout.operator("anim.channels_collapse")
|
|
|
|
layout.separator()
|
|
layout.operator_menu_enum("anim.channels_move", "direction", text="Move...")
|
|
|
|
layout.separator()
|
|
|
|
layout.operator("anim.channels_delete")
|
|
|
|
if is_graph_editor and context.space_data.mode == 'DRIVERS':
|
|
layout.operator("graph.driver_delete_invalid")
|
|
|
|
|
|
class DOPESHEET_MT_snap_pie(Menu):
|
|
bl_label = "Snap"
|
|
|
|
def draw(self, _context):
|
|
layout = self.layout
|
|
pie = layout.menu_pie()
|
|
|
|
pie.operator("action.snap", text="Selection to Current Frame").type = 'CFRA'
|
|
pie.operator("action.snap", text="Selection to Nearest Frame").type = 'NEAREST_FRAME'
|
|
pie.operator("action.snap", text="Selection to Nearest Second").type = 'NEAREST_SECOND'
|
|
pie.operator("action.snap", text="Selection to Nearest Marker").type = 'NEAREST_MARKER'
|
|
|
|
|
|
class LayersDopeSheetPanel:
|
|
bl_space_type = 'DOPESHEET_EDITOR'
|
|
bl_region_type = 'UI'
|
|
bl_category = "View"
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
st = context.space_data
|
|
ob = context.object
|
|
if st.mode != 'GPENCIL' or ob is None or ob.type != 'GREASEPENCIL':
|
|
return False
|
|
|
|
gpd = ob.data
|
|
gpl = gpd.layers.active
|
|
if gpl:
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
class GreasePencilLayersDopeSheetPanel:
|
|
bl_space_type = 'DOPESHEET_EDITOR'
|
|
bl_region_type = 'UI'
|
|
bl_category = "View"
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
st = context.space_data
|
|
ob = context.object
|
|
if st.mode != 'GPENCIL' or ob is None or ob.type != 'GREASEPENCIL':
|
|
return False
|
|
|
|
grease_pencil = ob.data
|
|
active_layer = grease_pencil.layers.active
|
|
if active_layer:
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
class DOPESHEET_PT_grease_pencil_mode(GreasePencilLayersDopeSheetPanel, Panel):
|
|
bl_label = "Layer"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.use_property_split = True
|
|
layout.use_property_decorate = False
|
|
|
|
ob = context.object
|
|
grease_pencil = ob.data
|
|
active_layer = grease_pencil.layers.active
|
|
|
|
if active_layer:
|
|
row = layout.row(align=True)
|
|
row.prop(active_layer, "blend_mode", text="Blend")
|
|
|
|
row = layout.row(align=True)
|
|
row.prop(active_layer, "opacity", text="Opacity", slider=True)
|
|
|
|
row = layout.row(align=True)
|
|
row.prop(active_layer, "use_lights", text="Lights")
|
|
|
|
|
|
class DOPESHEET_PT_grease_pencil_layer_masks(GreasePencilLayersDopeSheetPanel, GreasePencil_LayerMaskPanel, Panel):
|
|
bl_label = "Masks"
|
|
bl_parent_id = "DOPESHEET_PT_grease_pencil_mode"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
|
|
|
|
class DOPESHEET_PT_grease_pencil_layer_transform(
|
|
GreasePencilLayersDopeSheetPanel,
|
|
GreasePencil_LayerTransformPanel,
|
|
Panel,
|
|
):
|
|
bl_label = "Transform"
|
|
bl_parent_id = "DOPESHEET_PT_grease_pencil_mode"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
|
|
|
|
class DOPESHEET_PT_grease_pencil_layer_relations(
|
|
GreasePencilLayersDopeSheetPanel,
|
|
GreasePencil_LayerRelationsPanel,
|
|
Panel,
|
|
):
|
|
bl_label = "Relations"
|
|
bl_parent_id = "DOPESHEET_PT_grease_pencil_mode"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
|
|
|
|
class DOPESHEET_PT_grease_pencil_layer_adjustments(
|
|
GreasePencilLayersDopeSheetPanel,
|
|
GreasePencil_LayerAdjustmentsPanel,
|
|
Panel,
|
|
):
|
|
bl_label = "Adjustments"
|
|
bl_parent_id = "DOPESHEET_PT_grease_pencil_mode"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
|
|
|
|
class DOPESHEET_PT_grease_pencil_layer_display(
|
|
GreasePencilLayersDopeSheetPanel,
|
|
GreasePencil_LayerDisplayPanel,
|
|
Panel,
|
|
):
|
|
bl_label = "Display"
|
|
bl_parent_id = "DOPESHEET_PT_grease_pencil_mode"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
|
|
|
|
classes = (
|
|
DOPESHEET_HT_header,
|
|
DOPESHEET_PT_proportional_edit,
|
|
DOPESHEET_MT_editor_menus,
|
|
DOPESHEET_MT_view,
|
|
DOPESHEET_MT_select,
|
|
DOPESHEET_MT_marker,
|
|
DOPESHEET_MT_channel,
|
|
DOPESHEET_MT_action,
|
|
DOPESHEET_MT_key,
|
|
DOPESHEET_MT_key_transform,
|
|
DOPESHEET_MT_gpencil_channel,
|
|
DOPESHEET_MT_delete,
|
|
DOPESHEET_MT_context_menu,
|
|
DOPESHEET_MT_channel_context_menu,
|
|
DOPESHEET_MT_snap_pie,
|
|
DOPESHEET_MT_view_pie,
|
|
DOPESHEET_PT_filters,
|
|
DOPESHEET_PT_action,
|
|
DOPESHEET_PT_action_slot,
|
|
DOPESHEET_PT_custom_props_action,
|
|
DOPESHEET_PT_snapping,
|
|
DOPESHEET_PT_grease_pencil_mode,
|
|
DOPESHEET_PT_grease_pencil_layer_masks,
|
|
DOPESHEET_PT_grease_pencil_layer_transform,
|
|
DOPESHEET_PT_grease_pencil_layer_adjustments,
|
|
DOPESHEET_PT_grease_pencil_layer_relations,
|
|
DOPESHEET_PT_grease_pencil_layer_display,
|
|
DOPESHEET_PT_playhead_snapping,
|
|
)
|
|
|
|
if __name__ == "__main__": # only for live edit.
|
|
from bpy.utils import register_class
|
|
for cls in classes:
|
|
register_class(cls)
|