5905e0b37e40df9bfc4da7000f05a951ff80499d
[blender.git] / release / scripts / startup / bl_ui / space_dopesheet.py
1 # ##### BEGIN GPL LICENSE BLOCK #####
2 #
3 #  This program is free software; you can redistribute it and/or
4 #  modify it under the terms of the GNU General Public License
5 #  as published by the Free Software Foundation; either version 2
6 #  of the License, or (at your option) any later version.
7 #
8 #  This program is distributed in the hope that it will be useful,
9 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
10 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 #  GNU General Public License for more details.
12 #
13 #  You should have received a copy of the GNU General Public License
14 #  along with this program; if not, write to the Free Software Foundation,
15 #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 #
17 # ##### END GPL LICENSE BLOCK #####
18
19 # <pep8 compliant>
20
21 import bpy
22 from bpy.types import (
23     Header,
24     Menu,
25     Panel,
26 )
27
28 #######################################
29 # DopeSheet Filtering - Header Buttons
30
31 # used for DopeSheet, NLA, and Graph Editors
32
33
34 def dopesheet_filter(layout, context, generic_filters_only=False):
35     dopesheet = context.space_data.dopesheet
36     is_nla = context.area.type == 'NLA_EDITOR'
37
38     row = layout.row(align=True)
39     row.prop(dopesheet, "show_only_selected", text="")
40     row.prop(dopesheet, "show_hidden", text="")
41
42     if is_nla:
43         row.prop(dopesheet, "show_missing_nla", text="")
44     else:  # graph and dopesheet editors - F-Curves and drivers only
45         row.prop(dopesheet, "show_only_errors", text="")
46
47     if not generic_filters_only:
48         if bpy.data.collections:
49             row = layout.row(align=True)
50             row.prop(dopesheet, "filter_collection", text="")
51
52     if not is_nla:
53         row = layout.row(align=True)
54         row.prop(dopesheet, "filter_fcurve_name", text="")
55     else:
56         row = layout.row(align=True)
57         row.prop(dopesheet, "filter_text", text="")
58
59 #######################################
60 # Dopesheet Filtering Popovers
61
62 # Generic Layout - Used as base for filtering popovers used in all animation editors
63 # Used for DopeSheet, NLA, and Graph Editors
64
65
66 class DopesheetFilterPopoverBase:
67     bl_region_type = 'HEADER'
68     bl_label = "Filters"
69
70     # Generic = Affects all datatypes
71     # XXX: Perhaps we want these to stay in the header instead, for easy/fast access
72     @classmethod
73     def draw_generic_filters(cls, context, layout):
74         dopesheet = context.space_data.dopesheet
75         is_nla = context.area.type == 'NLA_EDITOR'
76
77         col = layout.column(align=True)
78         col.prop(dopesheet, "show_only_selected", icon='NONE')
79         col.prop(dopesheet, "show_hidden", icon='NONE')
80
81         if is_nla:
82             col.prop(dopesheet, "show_missing_nla", icon='NONE')
83         else:  # graph and dopesheet editors - F-Curves and drivers only
84             col.prop(dopesheet, "show_only_errors", icon='NONE')
85
86     # Name/Membership Filters
87     # XXX: Perhaps these should just stay in the headers (exclusively)?
88     @classmethod
89     def draw_search_filters(cls, context, layout, generic_filters_only=False):
90         dopesheet = context.space_data.dopesheet
91         is_nla = context.area.type == 'NLA_EDITOR'
92
93         col = layout.column(align=True)
94         col.label(text="With Name:")
95         if not is_nla:
96             row = col.row(align=True)
97             row.prop(dopesheet, "filter_fcurve_name", text="")
98         else:
99             row = col.row(align=True)
100             row.prop(dopesheet, "filter_text", text="")
101
102         if (not generic_filters_only) and (bpy.data.collections):
103             col = layout.column(align=True)
104             col.label(text="In Collection:")
105             col.prop(dopesheet, "filter_collection", text="")
106
107     # Standard = Present in all panels
108     @classmethod
109     def draw_standard_filters(cls, context, layout):
110         dopesheet = context.space_data.dopesheet
111
112         # datablock filters
113         layout.label(text="Filter by Type:")
114         flow = layout.grid_flow(row_major=True, columns=2, even_rows=False, align=False)
115
116         flow.prop(dopesheet, "show_scenes", text="Scenes")
117         flow.prop(dopesheet, "show_nodes", text="Node Trees")
118
119         # object types
120         if bpy.data.armatures:
121             flow.prop(dopesheet, "show_armatures", text="Armatures")
122         if bpy.data.cameras:
123             flow.prop(dopesheet, "show_cameras", text="Cameras")
124         if bpy.data.grease_pencils:
125             flow.prop(dopesheet, "show_gpencil", text="Grease Pencil Objects")
126         if bpy.data.lights:
127             flow.prop(dopesheet, "show_lights", text="Lights")
128         if bpy.data.meshes:
129             flow.prop(dopesheet, "show_meshes", text="Meshes")
130         if bpy.data.curves:
131             flow.prop(dopesheet, "show_curves", text="Curves")
132         if bpy.data.lattices:
133             flow.prop(dopesheet, "show_lattices", text="Lattices")
134         if bpy.data.metaballs:
135             flow.prop(dopesheet, "show_metaballs", text="Metaballs")
136
137         # data types
138         flow.prop(dopesheet, "show_worlds", text="Worlds")
139         if bpy.data.particles:
140             flow.prop(dopesheet, "show_particles", text="Particles")
141         if bpy.data.linestyles:
142             flow.prop(dopesheet, "show_linestyles", text="Line Styles")
143         if bpy.data.speakers:
144             flow.prop(dopesheet, "show_speakers", text="Speakers")
145         if bpy.data.materials:
146             flow.prop(dopesheet, "show_materials", text="Materials")
147         if bpy.data.textures:
148             flow.prop(dopesheet, "show_textures", text="Textures")
149         if bpy.data.shape_keys:
150             flow.prop(dopesheet, "show_shapekeys", text="Shape Keys")
151         if bpy.data.cache_files:
152             flow.prop(dopesheet, "show_cache_files", text="Cache Files")
153
154         layout.separator()
155
156         # Object Data Filters
157
158         # TODO: Add per-channel/axis convenience toggles?
159         split = layout.split()
160
161         col = split.column()
162         col.prop(dopesheet, "show_transforms", text="Transforms")
163
164         col = split.column()
165         col.prop(dopesheet, "show_modifiers", text="Modifiers")
166
167         layout.separator()
168
169         # performance-related options (users will mostly have these enabled)
170         col = layout.column(align=True)
171         col.label(text="Options:")
172         col.prop(dopesheet, "use_datablock_sort", icon='NONE')
173
174
175 # Popover for Dopesheet Editor(s) - Dopesheet, Action, Shapekey, GPencil, Mask, etc.
176 class DOPESHEET_PT_filters(DopesheetFilterPopoverBase, Panel):
177     bl_space_type = 'DOPESHEET_EDITOR'
178     bl_region_type = 'HEADER'
179     bl_label = "Filters"
180
181     def draw(self, context):
182         layout = self.layout
183
184         dopesheet = context.space_data.dopesheet
185         ds_mode = context.space_data.mode
186
187         layout.prop(dopesheet, "show_summary", text="Summary")
188
189         DopesheetFilterPopoverBase.draw_generic_filters(context, layout)
190
191         if ds_mode in {'DOPESHEET', 'ACTION', 'GPENCIL'}:
192             layout.separator()
193             generic_filters_only = ds_mode != 'DOPESHEET'
194             DopesheetFilterPopoverBase.draw_search_filters(context, layout,
195                                                            generic_filters_only=generic_filters_only)
196
197         if ds_mode == 'DOPESHEET':
198             layout.separator()
199             DopesheetFilterPopoverBase.draw_standard_filters(context, layout)
200
201
202 #######################################
203 # DopeSheet Editor - General/Standard UI
204
205 class DOPESHEET_HT_header(Header):
206     bl_space_type = 'DOPESHEET_EDITOR'
207
208     def draw(self, context):
209         layout = self.layout
210
211         st = context.space_data
212
213         row = layout.row(align=True)
214         row.template_header()
215
216         if st.mode == 'TIMELINE':
217             from .space_time import (
218                 TIME_MT_editor_menus,
219                 TIME_HT_editor_buttons,
220             )
221             TIME_MT_editor_menus.draw_collapsible(context, layout)
222             TIME_HT_editor_buttons.draw_header(context, layout)
223         else:
224             layout.prop(st, "ui_mode", text="")
225
226             DOPESHEET_MT_editor_menus.draw_collapsible(context, layout)
227             DOPESHEET_HT_editor_buttons.draw_header(context, layout)
228
229
230 # Header for "normal" dopesheet editor modes (e.g. Dope Sheet, Action, Shape Keys, etc.)
231 class DOPESHEET_HT_editor_buttons(Header):
232     bl_idname = "DOPESHEET_HT_editor_buttons"
233     bl_space_type = 'DOPESHEET_EDITOR'
234     bl_label = ""
235
236     def draw(self, context):
237         pass
238
239     def draw_header(context, layout):
240         st = context.space_data
241         tool_settings = context.tool_settings
242
243         if st.mode in {'ACTION', 'SHAPEKEY'}:
244             # TODO: These buttons need some tidying up -
245             # Probably by using a popover, and bypassing the template_id() here
246             row = layout.row(align=True)
247             row.operator("action.layer_prev", text="", icon='TRIA_DOWN')
248             row.operator("action.layer_next", text="", icon='TRIA_UP')
249
250             row = layout.row(align=True)
251             row.operator("action.push_down", text="Push Down", icon='NLA_PUSHDOWN')
252             row.operator("action.stash", text="Stash", icon='FREEZE')
253
254             layout.separator_spacer()
255
256             layout.template_ID(st, "action", new="action.new", unlink="action.unlink")
257
258         layout.separator_spacer()
259
260         if st.mode == 'DOPESHEET':
261             dopesheet_filter(layout, context)
262         elif st.mode == 'ACTION':
263             # 'generic_filters_only' limits the options to only the relevant 'generic' subset of
264             # filters which will work here and are useful (especially for character animation)
265             dopesheet_filter(layout, context, generic_filters_only=True)
266         elif st.mode == 'GPENCIL':
267             row = layout.row(align=True)
268             row.prop(st.dopesheet, "show_gpencil_3d_only", text="Active Only")
269
270             if st.dopesheet.show_gpencil_3d_only:
271                 row = layout.row(align=True)
272                 row.prop(st.dopesheet, "show_only_selected", text="")
273                 row.prop(st.dopesheet, "show_hidden", text="")
274
275             row = layout.row(align=True)
276             row.prop(st.dopesheet, "filter_text", text="")
277
278         layout.popover(
279             panel="DOPESHEET_PT_filters",
280             text="",
281             icon='FILTER',
282         )
283
284         # Grease Pencil mode doesn't need snapping, as it's frame-aligned only
285         if st.mode != 'GPENCIL':
286             layout.prop(st, "auto_snap", text="")
287
288         row = layout.row(align=True)
289         row.prop(tool_settings, "use_proportional_action", text="", icon_only=True)
290         sub = row.row(align=True)
291         sub.active = tool_settings.use_proportional_action
292         sub.prop(tool_settings, "proportional_edit_falloff", text="", icon_only=True)
293
294
295 class DOPESHEET_MT_editor_menus(Menu):
296     bl_idname = "DOPESHEET_MT_editor_menus"
297     bl_label = ""
298
299     def draw(self, context):
300         layout = self.layout
301         st = context.space_data
302
303         layout.menu("DOPESHEET_MT_view")
304         layout.menu("DOPESHEET_MT_select")
305         layout.menu("DOPESHEET_MT_marker")
306
307         if st.mode == 'DOPESHEET' or (st.mode == 'ACTION' and st.action is not None):
308             layout.menu("DOPESHEET_MT_channel")
309         elif st.mode == 'GPENCIL':
310             layout.menu("DOPESHEET_MT_gpencil_channel")
311
312         if st.mode != 'GPENCIL':
313             layout.menu("DOPESHEET_MT_key")
314         else:
315             layout.menu("DOPESHEET_MT_gpencil_frame")
316
317
318 class DOPESHEET_MT_view(Menu):
319     bl_label = "View"
320
321     def draw(self, context):
322         layout = self.layout
323
324         st = context.space_data
325
326         layout.prop(st, "show_region_ui")
327
328         layout.separator()
329
330         layout.prop(st.dopesheet, "use_multi_word_filter", text="Multi-word Match Search")
331
332         layout.separator()
333
334         layout.prop(st, "use_realtime_update")
335         layout.prop(st, "show_frame_indicator")
336         layout.prop(st, "show_sliders")
337         layout.prop(st, "show_group_colors")
338         layout.prop(st, "show_interpolation")
339         layout.prop(st, "show_extremes")
340         layout.prop(st, "show_marker_lines")
341         layout.prop(st, "use_auto_merge_keyframes")
342
343         layout.prop(st, "show_seconds")
344         layout.prop(st, "show_locked_time")
345
346         layout.separator()
347         layout.operator("anim.previewrange_set")
348         layout.operator("anim.previewrange_clear")
349         layout.operator("action.previewrange_set")
350
351         layout.separator()
352         layout.operator("action.view_all")
353         layout.operator("action.view_selected")
354         layout.operator("action.view_frame")
355
356         # Add this to show key-binding (reverse action in dope-sheet).
357         layout.separator()
358         props = layout.operator("wm.context_set_enum", text="Toggle Graph Editor", icon='GRAPH')
359         props.data_path = "area.type"
360         props.value = 'GRAPH_EDITOR'
361
362         layout.separator()
363         layout.menu("INFO_MT_area")
364
365
366 class DOPESHEET_MT_select(Menu):
367     bl_label = "Select"
368
369     def draw(self, context):
370         layout = self.layout
371
372         layout.operator("action.select_all", text="All").action = 'SELECT'
373         layout.operator("action.select_all", text="None").action = 'DESELECT'
374         layout.operator("action.select_all", text="Invert").action = 'INVERT'
375
376         layout.separator()
377         layout.operator("action.select_box").axis_range = False
378         layout.operator("action.select_box", text="Border Axis Range").axis_range = True
379
380         layout.operator("action.select_circle")
381
382         layout.separator()
383         layout.operator("action.select_column", text="Columns on Selected Keys").mode = 'KEYS'
384         layout.operator("action.select_column", text="Column on Current Frame").mode = 'CFRA'
385
386         layout.operator("action.select_column", text="Columns on Selected Markers").mode = 'MARKERS_COLUMN'
387         layout.operator("action.select_column", text="Between Selected Markers").mode = 'MARKERS_BETWEEN'
388
389         layout.separator()
390         props = layout.operator("action.select_leftright", text="Before Current Frame")
391         props.extend = False
392         props.mode = 'LEFT'
393         props = layout.operator("action.select_leftright", text="After Current Frame")
394         props.extend = False
395         props.mode = 'RIGHT'
396
397         # FIXME: grease pencil mode isn't supported for these yet, so skip for that mode only
398         if context.space_data.mode != 'GPENCIL':
399             layout.separator()
400             layout.operator("action.select_more")
401             layout.operator("action.select_less")
402
403             layout.separator()
404             layout.operator("action.select_linked")
405
406
407 class DOPESHEET_MT_marker(Menu):
408     bl_label = "Marker"
409
410     def draw(self, context):
411         layout = self.layout
412
413         from .space_time import marker_menu_generic
414         marker_menu_generic(layout, context)
415
416         st = context.space_data
417
418         if st.mode in {'ACTION', 'SHAPEKEY'} and st.action:
419             layout.separator()
420             layout.prop(st, "show_pose_markers")
421
422             if st.show_pose_markers is False:
423                 layout.operator("action.markers_make_local")
424
425         layout.prop(st, "use_marker_sync")
426
427 #######################################
428 # Keyframe Editing
429
430
431 class DOPESHEET_MT_channel(Menu):
432     bl_label = "Channel"
433
434     def draw(self, _context):
435         layout = self.layout
436
437         layout.operator_context = 'INVOKE_REGION_CHANNELS'
438
439         layout.operator("anim.channels_delete")
440
441         layout.separator()
442         layout.operator("anim.channels_group")
443         layout.operator("anim.channels_ungroup")
444
445         layout.separator()
446         layout.operator_menu_enum("anim.channels_setting_toggle", "type")
447         layout.operator_menu_enum("anim.channels_setting_enable", "type")
448         layout.operator_menu_enum("anim.channels_setting_disable", "type")
449
450         layout.separator()
451         layout.operator("anim.channels_editable_toggle")
452         layout.operator_menu_enum("action.extrapolation_type", "type", text="Extrapolation Mode")
453
454         layout.separator()
455         layout.operator("anim.channels_expand")
456         layout.operator("anim.channels_collapse")
457
458         layout.separator()
459         layout.operator_menu_enum("anim.channels_move", "direction", text="Move...")
460
461         layout.separator()
462         layout.operator("anim.channels_fcurves_enable")
463
464
465 class DOPESHEET_MT_key(Menu):
466     bl_label = "Key"
467
468     def draw(self, _context):
469         layout = self.layout
470
471         layout.menu("DOPESHEET_MT_key_transform", text="Transform")
472
473         layout.operator_menu_enum("action.snap", "type", text="Snap")
474         layout.operator_menu_enum("action.mirror", "type", text="Mirror")
475
476         layout.separator()
477         layout.operator("action.keyframe_insert")
478
479         layout.separator()
480         layout.operator("action.frame_jump")
481
482         layout.separator()
483         layout.operator("action.copy")
484         layout.operator("action.paste")
485         layout.operator("action.paste", text="Paste Flipped").flipped = True
486         layout.operator("action.duplicate_move")
487         layout.operator("action.delete")
488
489         layout.separator()
490         layout.operator_menu_enum("action.keyframe_type", "type", text="Keyframe Type")
491         layout.operator_menu_enum("action.handle_type", "type", text="Handle Type")
492         layout.operator_menu_enum("action.interpolation_type", "type", text="Interpolation Mode")
493
494         layout.separator()
495         layout.operator("action.clean").channels = False
496         layout.operator("action.clean", text="Clean Channels").channels = True
497         layout.operator("action.sample")
498
499
500 class DOPESHEET_MT_key_transform(Menu):
501     bl_label = "Transform"
502
503     def draw(self, _context):
504         layout = self.layout
505
506         layout.operator("transform.transform", text="Move").mode = 'TIME_TRANSLATE'
507         layout.operator("transform.transform", text="Extend").mode = 'TIME_EXTEND'
508         layout.operator("transform.transform", text="Slide").mode = 'TIME_SLIDE'
509         layout.operator("transform.transform", text="Scale").mode = 'TIME_SCALE'
510
511
512 #######################################
513 # Grease Pencil Editing
514
515 class DOPESHEET_MT_gpencil_channel(Menu):
516     bl_label = "Channel"
517
518     def draw(self, _context):
519         layout = self.layout
520
521         layout.operator_context = 'INVOKE_REGION_CHANNELS'
522
523         layout.operator("anim.channels_delete")
524
525         layout.separator()
526         layout.operator("anim.channels_setting_toggle")
527         layout.operator("anim.channels_setting_enable")
528         layout.operator("anim.channels_setting_disable")
529
530         layout.separator()
531         layout.operator("anim.channels_editable_toggle")
532
533         # XXX: to be enabled when these are ready for use!
534         # layout.separator()
535         # layout.operator("anim.channels_expand")
536         # layout.operator("anim.channels_collapse")
537
538         # layout.separator()
539         #layout.operator_menu_enum("anim.channels_move", "direction", text="Move...")
540
541
542 class DOPESHEET_MT_gpencil_frame(Menu):
543     bl_label = "Frame"
544
545     def draw(self, _context):
546         layout = self.layout
547
548         layout.menu("DOPESHEET_MT_key_transform", text="Transform")
549         layout.operator_menu_enum("action.snap", "type", text="Snap")
550         layout.operator_menu_enum("action.mirror", "type", text="Mirror")
551
552         layout.separator()
553         layout.operator("action.duplicate")
554         layout.operator("action.delete")
555
556         layout.separator()
557         layout.operator("action.keyframe_type")
558
559         # layout.separator()
560         # layout.operator("action.copy")
561         # layout.operator("action.paste")
562
563
564 class DOPESHEET_MT_delete(Menu):
565     bl_label = "Delete"
566
567     def draw(self, _context):
568         layout = self.layout
569
570         layout.operator("action.delete")
571
572         layout.separator()
573
574         layout.operator("action.clean").channels = False
575         layout.operator("action.clean", text="Clean Channels").channels = True
576
577
578 class DOPESHEET_MT_context_menu(Menu):
579     bl_label = "Dope Sheet Context Menu"
580
581     def draw(self, _context):
582         layout = self.layout
583
584         layout.operator("action.copy", text="Copy")
585         layout.operator("action.paste", text="Paste")
586         layout.operator("action.paste", text="Paste Flipped").flipped = True
587
588         layout.separator()
589
590         layout.operator_menu_enum("action.handle_type", "type", text="Handle Type")
591         layout.operator_menu_enum("action.interpolation_type", "type", text="Interpolation Mode")
592         layout.operator_menu_enum("action.easing_type", "type", text="Easing Type")
593
594         layout.separator()
595
596         layout.operator("action.keyframe_insert").type = 'SEL'
597         layout.operator("action.duplicate_move")
598         layout.operator("action.delete")
599
600         layout.separator()
601
602         layout.operator_menu_enum("action.mirror", "type", text="Mirror")
603         layout.operator_menu_enum("action.snap", "type", text="Snap")
604
605
606 class DOPESHEET_MT_channel_context_menu(Menu):
607     bl_label = "Dope Sheet Channel Context Menu"
608
609     def draw(self, _context):
610         layout = self.layout
611
612         layout.operator("anim.channels_setting_enable", text="Mute Channels").type = 'MUTE'
613         layout.operator("anim.channels_setting_disable", text="Unmute Channels").type = 'MUTE'
614         layout.separator()
615         layout.operator("anim.channels_setting_enable", text="Protect Channels").type = 'PROTECT'
616         layout.operator("anim.channels_setting_disable", text="Unprotect Channels").type = 'PROTECT'
617
618         layout.separator()
619         layout.operator("anim.channels_group")
620         layout.operator("anim.channels_ungroup")
621
622         layout.separator()
623         layout.operator("anim.channels_editable_toggle")
624         layout.operator_menu_enum("action.extrapolation_type", "type", text="Extrapolation Mode")
625
626         layout.separator()
627         layout.operator("anim.channels_expand")
628         layout.operator("anim.channels_collapse")
629
630         layout.separator()
631         layout.operator_menu_enum("anim.channels_move", "direction", text="Move...")
632
633         layout.separator()
634
635         layout.operator("anim.channels_delete")
636
637
638 class DOPESHEET_MT_snap_pie(Menu):
639     bl_label = "Snap"
640
641     def draw(self, _context):
642         layout = self.layout
643         pie = layout.menu_pie()
644
645         pie.operator("action.snap", text="Current Frame").type = 'CFRA'
646         pie.operator("action.snap", text="Nearest Frame").type = 'NEAREST_FRAME'
647         pie.operator("action.snap", text="Nearest Second").type = 'NEAREST_SECOND'
648         pie.operator("action.snap", text="Nearest Marker").type = 'NEAREST_MARKER'
649
650
651 classes = (
652     DOPESHEET_HT_header,
653     DOPESHEET_HT_editor_buttons,
654     DOPESHEET_MT_editor_menus,
655     DOPESHEET_MT_view,
656     DOPESHEET_MT_select,
657     DOPESHEET_MT_marker,
658     DOPESHEET_MT_channel,
659     DOPESHEET_MT_key,
660     DOPESHEET_MT_key_transform,
661     DOPESHEET_MT_gpencil_channel,
662     DOPESHEET_MT_gpencil_frame,
663     DOPESHEET_MT_delete,
664     DOPESHEET_MT_context_menu,
665     DOPESHEET_MT_channel_context_menu,
666     DOPESHEET_MT_snap_pie,
667     DOPESHEET_PT_filters,
668 )
669
670 if __name__ == "__main__":  # only for live edit.
671     from bpy.utils import register_class
672     for cls in classes:
673         register_class(cls)