GPencil Tools: Optimize Undo for Rotate Canvas
[blender-addons.git] / greasepencil_tools / timeline_scrub.py
blob46ac389845e5d7f448f8473c6909e954ecf2405f
1 # SPDX-FileCopyrightText: 2021-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 '''Based on viewport_timeline_scrub standalone addon - Samuel Bernou'''
7 from .prefs import get_addon_prefs
9 import numpy as np
10 from time import time
11 import bpy
12 import gpu
13 import blf
14 from gpu_extras.batch import batch_for_shader
16 from bpy.props import (BoolProperty,
17 StringProperty,
18 IntProperty,
19 FloatVectorProperty,
20 IntProperty,
21 PointerProperty,
22 EnumProperty)
25 def nearest(array, value) -> int:
26 '''
27 Get a numpy array and a target value
28 Return closest val found in array to passed value
29 '''
30 idx = (np.abs(array - value)).argmin()
31 return int(array[idx])
34 def draw_callback_px(self, context):
35 '''Draw callback use by modal to draw in viewport'''
36 if context.area != self.current_area:
37 return
39 # text
40 font_id = 0
42 shader = gpu.shader.from_builtin('UNIFORM_COLOR') # initiate shader
43 gpu.state.blend_set('ALPHA')
44 gpu.state.line_width_set(1.0)
46 # Draw HUD
47 if self.use_hud_time_line:
48 shader.bind()
49 shader.uniform_float("color", self.color_timeline)
50 self.batch_timeline.draw(shader)
52 # Display keyframes
53 if self.use_hud_keyframes and self.batch_keyframes:
54 if self.keyframe_aspect == 'LINE':
55 gpu.state.line_width_set(3.0)
56 shader.bind()
57 shader.uniform_float("color", self.color_timeline)
58 self.batch_keyframes.draw(shader)
59 else:
60 gpu.state.line_width_set(1.0)
61 shader.bind()
62 shader.uniform_float("color", self.color_timeline)
63 self.batch_keyframes.draw(shader)
65 # Show current frame line
66 gpu.state.line_width_set(1.0)
67 if self.use_hud_playhead:
68 playhead = [(self.cursor_x, self.my + self.playhead_size/2),
69 (self.cursor_x, self.my - self.playhead_size/2)]
70 batch = batch_for_shader(shader, 'LINES', {"pos": playhead})
71 shader.bind()
72 shader.uniform_float("color", self.color_playhead)
73 batch.draw(shader)
75 # restore opengl defaults
76 gpu.state.blend_set('NONE')
78 # Display current frame text
79 blf.color(font_id, *self.color_text)
80 if self.use_hud_frame_current:
81 blf.position(font_id, self.mouse[0]+10, self.mouse[1]+10, 0)
82 blf.size(font_id, 30 * (self.dpi / 72.0))
83 blf.draw(font_id, f'{self.new_frame:.0f}')
85 # Display frame offset text
86 if self.use_hud_frame_offset:
87 blf.position(font_id, self.mouse[0]+10,
88 self.mouse[1]+(40*self.ui_scale), 0)
89 blf.size(font_id, 16 * (self.dpi / 72.0))
90 sign = '+' if self.offset > 0 else ''
91 blf.draw(font_id, f'{sign}{self.offset:.0f}')
94 class GPTS_OT_time_scrub(bpy.types.Operator):
95 bl_idname = "animation.time_scrub"
96 bl_label = "Time scrub"
97 bl_description = "Quick time scrubbing with a shortcut"
98 bl_options = {"REGISTER", "INTERNAL", "UNDO"}
100 @classmethod
101 def poll(cls, context):
102 return context.space_data.type in ('VIEW_3D', 'SEQUENCE_EDITOR', 'CLIP_EDITOR')
104 def invoke(self, context, event):
105 prefs = get_addon_prefs().ts
107 self.current_area = context.area
108 self.key = prefs.keycode
109 self.evaluate_gp_obj_key = prefs.evaluate_gp_obj_key
110 self.always_snap = prefs.always_snap
111 self.rolling_mode = prefs.rolling_mode
113 self.dpi = context.preferences.system.dpi
114 self.ui_scale = context.preferences.system.ui_scale
115 # hud prefs
116 self.color_timeline = prefs.color_timeline
117 self.color_playhead = prefs.color_playhead
118 self.color_text = prefs.color_playhead
119 self.use_hud_time_line = prefs.use_hud_time_line
120 self.use_hud_keyframes = prefs.use_hud_keyframes
121 self.keyframe_aspect = prefs.keyframe_aspect
122 self.use_hud_playhead = prefs.use_hud_playhead
123 self.use_hud_frame_current = prefs.use_hud_frame_current
124 self.use_hud_frame_offset = prefs.use_hud_frame_offset
126 self.playhead_size = prefs.playhead_size
127 self.lines_size = prefs.lines_size
129 self.px_step = prefs.pixel_step
130 self.snap_on = False
131 self.mouse = (event.mouse_region_x, event.mouse_region_y)
132 self.init_mouse_x = self.cursor_x = event.mouse_region_x
134 # self.init_mouse_y = event.mouse_region_y # only to display init frame text
135 self.cancel_frame = self.init_frame = self.new_frame = context.scene.frame_current
136 self.lock_range = context.scene.lock_frame_selection_to_range
137 if context.scene.use_preview_range:
138 self.f_start = context.scene.frame_preview_start
139 self.f_end = context.scene.frame_preview_end
140 else:
141 self.f_start = context.scene.frame_start
142 self.f_end = context.scene.frame_end
144 self.offset = 0
145 self.pos = []
147 # Snap control
148 self.snap_ctrl = not prefs.use_ctrl
149 self.snap_shift = not prefs.use_shift
150 self.snap_alt = not prefs.use_alt
151 self.snap_mouse_key = 'LEFTMOUSE' if self.key == 'RIGHTMOUSE' else 'RIGHTMOUSE'
153 ob = context.object
155 if context.space_data.type != 'VIEW_3D':
156 ob = None # do not consider any key
158 if ob: # condition to allow empty scrubing
159 if ob.type != 'GPENCIL' or self.evaluate_gp_obj_key:
160 # Get object keyframe position
161 anim_data = ob.animation_data
162 action = None
164 if anim_data:
165 action = anim_data.action
166 if action:
167 for fcu in action.fcurves:
168 for kf in fcu.keyframe_points:
169 if kf.co.x not in self.pos:
170 self.pos.append(kf.co.x)
172 if ob.type == 'GPENCIL':
173 # Get GP frame position
174 gpl = ob.data.layers
175 layer = gpl.active
176 if layer:
177 for frame in layer.frames:
178 if frame.frame_number not in self.pos:
179 self.pos.append(frame.frame_number)
181 if not ob or not self.pos:
182 # Disable inverted behavior if no frame to snap
183 self.always_snap = False
184 if self.rolling_mode:
185 self.report({'WARNING'}, 'No Keys to flip on')
186 return {'CANCELLED'}
188 if self.rolling_mode:
189 # sorted and casted to int list since it's going to work with indexes
190 self.pos = sorted([int(f) for f in self.pos])
191 # find and make current frame the "starting" frame (force snap)
192 active_pos = [i for i, num in enumerate(self.pos) if num <= self.init_frame]
193 if active_pos:
194 self.init_index = active_pos[-1]
195 self.init_frame = self.new_frame = self.pos[self.init_index]
196 else:
197 self.init_index = 0
198 self.init_frame = self.new_frame = self.pos[0]
200 # del active_pos
201 self.index_limit = len(self.pos) - 1
203 # Also snap on play bounds (sliced off for keyframe display)
204 self.pos += [self.f_start, self.f_end]
206 # Disable Onion skin
207 self.active_space_data = context.space_data
208 self.onion_skin = None
209 self.multi_frame = None
210 if context.space_data.type == 'VIEW_3D': # and 'GPENCIL' in context.mode
211 self.onion_skin = self.active_space_data.overlay.use_gpencil_onion_skin
212 self.active_space_data.overlay.use_gpencil_onion_skin = False
214 if ob and ob.type == 'GPENCIL':
215 if ob.data.use_multiedit:
216 self.multi_frame = ob.data.use_multiedit
217 ob.data.use_multiedit = False
219 self.hud = prefs.use_hud
220 if not self.hud:
221 ## Same as end settings when HUD is On
222 if self.lock_range:
223 self.pos = [i for i in self.pos if self.f_start <= i <= self.f_end]
224 self.pos = np.asarray(self.pos)
225 if self.rolling_mode:
226 context.scene.frame_current = self.new_frame
227 context.window_manager.modal_handler_add(self)
228 return {'RUNNING_MODAL'}
230 # - HUD params
231 width = context.area.width
232 right = int((width - self.init_mouse_x) / self.px_step)
233 left = int(self.init_mouse_x / self.px_step)
235 hud_pos_x = []
236 for i in range(1, left):
237 hud_pos_x.append(self.init_mouse_x - i*self.px_step)
238 for i in range(1, right):
239 hud_pos_x.append(self.init_mouse_x + i*self.px_step)
241 # - list of double coords
243 init_height = 60
244 frame_height = self.lines_size
245 key_height = 14
246 bound_h = key_height + 19
247 bound_bracket_l = self.px_step/2
249 self.my = my = event.mouse_region_y
251 self.hud_lines = []
253 if not self.rolling_mode:
254 # frame marks
255 for x in hud_pos_x:
256 self.hud_lines.append((x, my - (frame_height/2)))
257 self.hud_lines.append((x, my + (frame_height/2)))
259 # init frame mark
260 self.hud_lines += [(self.init_mouse_x, my - (init_height/2)),
261 (self.init_mouse_x, my + (init_height/2))]
263 if not self.rolling_mode:
264 # Add start/end boundary bracket to HUD
265 start_x = self.init_mouse_x + \
266 (self.f_start - self.init_frame) * self.px_step
267 end_x = self.init_mouse_x + \
268 (self.f_end - self.init_frame) * self.px_step
270 # start
271 up = (start_x, my - (bound_h/2))
272 dn = (start_x, my + (bound_h/2))
273 self.hud_lines.append(up)
274 self.hud_lines.append(dn)
276 self.hud_lines.append(up)
277 self.hud_lines.append((up[0] + bound_bracket_l, up[1]))
278 self.hud_lines.append(dn)
279 self.hud_lines.append((dn[0] + bound_bracket_l, dn[1]))
281 # end
282 up = (end_x, my - (bound_h/2))
283 dn = (end_x, my + (bound_h/2))
284 self.hud_lines.append(up)
285 self.hud_lines.append(dn)
287 self.hud_lines.append(up)
288 self.hud_lines.append((up[0] - bound_bracket_l, up[1]))
289 self.hud_lines.append(dn)
290 self.hud_lines.append((dn[0] - bound_bracket_l, dn[1]))
292 # Horizontal line
293 self.hud_lines += [(0, my), (width, my)]
295 # Prepare batchs to draw static parts
296 shader = gpu.shader.from_builtin('UNIFORM_COLOR') # initiate shader
297 self.batch_timeline = batch_for_shader(
298 shader, 'LINES', {"pos": self.hud_lines})
300 if self.rolling_mode:
301 current_id = self.pos.index(self.new_frame)
302 # Add init_frame to "cancel" it in later UI code
303 ui_key_pos = [i - current_id + self.init_frame for i, _f in enumerate(self.pos[:-2])]
304 else:
305 ui_key_pos = self.pos[:-2]
307 self.batch_keyframes = None # init if there are no keyframe to draw
308 if ui_key_pos:
309 if self.keyframe_aspect == 'LINE':
310 key_lines = []
311 # Slice off position of start/end frame added last (in list for snapping)
312 for i in ui_key_pos:
313 key_lines.append(
314 (self.init_mouse_x + ((i-self.init_frame) * self.px_step), my - (key_height/2)))
315 key_lines.append(
316 (self.init_mouse_x + ((i-self.init_frame) * self.px_step), my + (key_height/2)))
318 self.batch_keyframes = batch_for_shader(
319 shader, 'LINES', {"pos": key_lines})
321 else:
322 # diamond and square
323 # keysize5 for square, 4 or 6 for diamond
324 keysize = 6 if self.keyframe_aspect == 'DIAMOND' else 5
325 upper = 0
327 shaped_key = []
328 indices = []
329 idx_offset = 0
330 for i in ui_key_pos:
331 center = self.init_mouse_x + ((i-self.init_frame)*self.px_step)
332 if self.keyframe_aspect == 'DIAMOND':
333 # +1 on x is to correct pixel alignment
334 shaped_key += [(center-keysize, my+upper),
335 (center+1, my+keysize+upper),
336 (center+keysize, my+upper),
337 (center+1, my-keysize+upper)]
339 elif self.keyframe_aspect == 'SQUARE':
340 shaped_key += [(center-keysize+1, my-keysize+upper),
341 (center-keysize+1, my+keysize+upper),
342 (center+keysize, my+keysize+upper),
343 (center+keysize, my-keysize+upper)]
345 indices += [(0+idx_offset, 1+idx_offset, 2+idx_offset),
346 (0+idx_offset, 2+idx_offset, 3+idx_offset)]
347 idx_offset += 4
349 self.batch_keyframes = batch_for_shader(
350 shader, 'TRIS', {"pos": shaped_key}, indices=indices)
352 # Trim snapping list of frame outside of frame range if range lock activated
353 # (after drawing batch so those are still showed)
354 if self.lock_range:
355 self.pos = [i for i in self.pos if self.f_start <= i <= self.f_end]
357 # convert frame list to array for numpy snap utility
358 self.pos = np.asarray(self.pos)
360 if self.rolling_mode:
361 context.scene.frame_current = self.new_frame
363 args = (self, context)
364 self.viewtype = None
365 self.spacetype = 'WINDOW' # is PREVIEW for VSE, needed for handler remove
367 if context.space_data.type == 'VIEW_3D':
368 self.viewtype = bpy.types.SpaceView3D
369 self._handle = bpy.types.SpaceView3D.draw_handler_add(
370 draw_callback_px, args, 'WINDOW', 'POST_PIXEL')
372 elif context.space_data.type == 'SEQUENCE_EDITOR':
373 self.viewtype = bpy.types.SpaceSequenceEditor
374 self.spacetype = 'PREVIEW'
375 self._handle = bpy.types.SpaceSequenceEditor.draw_handler_add(
376 draw_callback_px, args, 'PREVIEW', 'POST_PIXEL')
378 elif context.space_data.type == 'CLIP_EDITOR':
379 self.viewtype = bpy.types.SpaceClipEditor
380 self._handle = bpy.types.SpaceClipEditor.draw_handler_add(
381 draw_callback_px, args, 'WINDOW', 'POST_PIXEL')
383 context.window_manager.modal_handler_add(self)
384 return {'RUNNING_MODAL'}
386 def _exit_modal(self, context):
387 if self.onion_skin is not None:
388 self.active_space_data.overlay.use_gpencil_onion_skin = self.onion_skin
389 if self.multi_frame:
390 context.object.data.use_multiedit = self.multi_frame
391 if self.hud and self.viewtype:
392 self.viewtype.draw_handler_remove(self._handle, self.spacetype)
393 context.area.tag_redraw()
395 def modal(self, context, event):
397 if event.type == 'MOUSEMOVE':
398 # - calculate frame offset from pixel offset
399 # - get mouse.x and add it to initial frame num
400 self.mouse = (event.mouse_region_x, event.mouse_region_y)
402 px_offset = (event.mouse_region_x - self.init_mouse_x)
403 self.offset = int(px_offset / self.px_step)
404 self.new_frame = self.init_frame + self.offset
406 if self.rolling_mode:
407 # Frame Flipping mode (equidistant scrub snap)
408 self.index = self.init_index + self.offset
409 # clamp to possible index range
410 self.index = min(max(self.index, 0), self.index_limit)
411 self.new_frame = self.pos[self.index]
412 context.scene.frame_current = self.new_frame
413 self.cursor_x = self.init_mouse_x + (self.offset * self.px_step)
415 else:
416 mod_snap = False
417 if self.snap_ctrl and event.ctrl:
418 mod_snap = True
419 if self.snap_shift and event.shift:
420 mod_snap = True
421 if self.snap_alt and event.alt:
422 mod_snap = True
424 ## Snapping
425 if self.always_snap:
426 # inverted snapping behavior
427 if not self.snap_on and not mod_snap:
428 self.new_frame = nearest(self.pos, self.new_frame)
429 else:
430 if self.snap_on or mod_snap:
431 self.new_frame = nearest(self.pos, self.new_frame)
433 # frame range restriction
434 if self.lock_range:
435 if self.new_frame < self.f_start:
436 self.new_frame = self.f_start
437 elif self.new_frame > self.f_end:
438 self.new_frame = self.f_end
440 # context.scene.frame_set(self.new_frame)
441 context.scene.frame_current = self.new_frame
443 # - recalculate offset to snap cursor to frame
444 self.offset = self.new_frame - self.init_frame
446 # - calculate cursor pixel position from frame offset
447 self.cursor_x = self.init_mouse_x + (self.offset * self.px_step)
449 if event.type == 'ESC':
450 # frame_set(self.init_frame) ?
451 context.scene.frame_current = self.cancel_frame
452 self._exit_modal(context)
453 return {'CANCELLED'}
455 # Snap if pressing NOT used mouse key (right or mid)
456 if event.type == self.snap_mouse_key:
457 if event.value == "PRESS":
458 self.snap_on = True
459 else:
460 self.snap_on = False
462 if event.type == self.key and event.value == 'RELEASE':
463 self._exit_modal(context)
464 return {'FINISHED'}
466 return {"RUNNING_MODAL"}
469 # --- addon prefs
471 def auto_rebind(self, context):
472 unregister_keymaps()
473 register_keymaps()
476 class GPTS_OT_set_scrub_keymap(bpy.types.Operator):
477 bl_idname = "animation.ts_set_keymap"
478 bl_label = "Change keymap"
479 bl_description = "Quick time scrubbing with a shortcut"
480 bl_options = {"REGISTER", "INTERNAL"}
482 def invoke(self, context, event):
483 self.prefs = get_addon_prefs().ts
484 self.ctrl = False
485 self.shift = False
486 self.alt = False
488 self.init_value = self.prefs.keycode
489 self.prefs.keycode = ''
490 context.window_manager.modal_handler_add(self)
491 return {'RUNNING_MODAL'}
493 def modal(self, context, event):
494 exclude_keys = {'MOUSEMOVE', 'INBETWEEN_MOUSEMOVE',
495 'TIMER_REPORT', 'ESC', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE'}
496 exclude_in = ('SHIFT', 'CTRL', 'ALT')
497 if event.type == 'ESC':
498 self.prefs.keycode = self.init_value
499 return {'CANCELLED'}
501 self.ctrl = event.ctrl
502 self.shift = event.shift
503 self.alt = event.alt
505 if event.type not in exclude_keys and not any(x in event.type for x in exclude_in):
506 print('key:', event.type, 'value:', event.value)
507 if event.value == 'PRESS':
508 self.report({'INFO'}, event.type)
509 # set the chosen key
510 self.prefs.keycode = event.type
511 # Following condition to avoid unnecessary rebind update
512 if self.prefs.use_shift != event.shift:
513 self.prefs.use_shift = event.shift
515 if self.prefs.use_alt != event.alt:
516 self.prefs.use_alt = event.alt
518 # -# Trigger rebind update with last
519 self.prefs.use_ctrl = event.ctrl
521 return {'FINISHED'}
523 return {"RUNNING_MODAL"}
526 class GPTS_timeline_settings(bpy.types.PropertyGroup):
528 keycode: StringProperty(
529 name="Shortcut",
530 description="Shortcut to trigger the scrub in viewport during press",
531 default="MIDDLEMOUSE")
533 always_snap: BoolProperty(
534 name="Always Snap",
535 description="Always snap to keys if any, modifier is used deactivate the snapping\nDisabled if no keyframe found",
536 default=False)
538 rolling_mode: BoolProperty(
539 name="Rolling Mode",
540 description="Alternative Gap-less timeline. No time information to quickly roll/flip over keys\nOverride normal and 'always snap' mode",
541 default=False)
543 use: BoolProperty(
544 name="Enable",
545 description="Enable/Disable timeline scrub",
546 default=True,
547 update=auto_rebind)
549 use_in_timeline_editor: BoolProperty(
550 name="Shortcut in timeline editors",
551 description="Add the same shortcut to scrub in timeline editor windows",
552 default=True,
553 update=auto_rebind)
555 use_shift: BoolProperty(
556 name="Combine With Shift",
557 description="Add shift",
558 default=False,
559 update=auto_rebind)
561 use_alt: BoolProperty(
562 name="Combine With Alt",
563 description="Add alt",
564 default=True,
565 update=auto_rebind)
567 use_ctrl: BoolProperty(
568 name="Combine With Ctrl",
569 description="Add ctrl",
570 default=False,
571 update=auto_rebind)
573 evaluate_gp_obj_key: BoolProperty(
574 name='Use Gpencil Object Keyframes',
575 description="Also snap on greasepencil object keyframe (else only active layer frames)",
576 default=True)
578 pixel_step: IntProperty(
579 name="Frame Interval On Screen",
580 description="Pixel steps on screen that represent a frame intervals",
581 default=10,
582 min=1,
583 max=500,
584 soft_min=2,
585 soft_max=100,
586 step=1,
587 subtype='PIXEL')
589 use_hud: BoolProperty(
590 name='Display Timeline Overlay',
591 description="Display overlays with timeline information when scrubbing time in viewport",
592 default=True)
594 use_hud_time_line: BoolProperty(
595 name='Timeline',
596 description="Display a static marks overlay to represent timeline when scrubbing",
597 default=True)
599 use_hud_keyframes: BoolProperty(
600 name='Keyframes',
601 description="Display shapes overlay to show keyframe position when scrubbing",
602 default=True)
604 use_hud_playhead: BoolProperty(
605 name='Playhead',
606 description="Display the playhead as a vertical line to show position in time",
607 default=True)
609 use_hud_frame_current: BoolProperty(
610 name='Text Frame Current',
611 description="Display the current frame as text above mouse cursor",
612 default=True)
614 use_hud_frame_offset: BoolProperty(
615 name='Text Frame Offset',
616 description="Display frame offset from initial position as text above mouse cursor",
617 default=True)
619 color_timeline: FloatVectorProperty(
620 name="Timeline Color",
621 subtype='COLOR_GAMMA',
622 size=4,
623 default=(0.5, 0.5, 0.5, 0.6),
624 min=0.0, max=1.0,
625 description="Color of the temporary timeline")
627 color_playhead: FloatVectorProperty(
628 name="Cusor Color",
629 subtype='COLOR_GAMMA',
630 size=4,
631 default=(0.01, 0.64, 1.0, 0.8),
632 min=0.0, max=1.0,
633 description="Color of the temporary line cursor and text")
635 # sizes
636 playhead_size: IntProperty(
637 name="Playhead Size",
638 description="Playhead height in pixels",
639 default=100,
640 min=2,
641 max=10000,
642 soft_min=10,
643 soft_max=5000,
644 step=1,
645 subtype='PIXEL')
647 lines_size: IntProperty(
648 name="Frame Lines Size",
649 description="Frame lines height in pixels",
650 default=10,
651 min=1,
652 max=10000,
653 soft_min=5,
654 soft_max=40,
655 step=1,
656 subtype='PIXEL')
658 keyframe_aspect: EnumProperty(
659 name="Keyframe Display",
660 description="Customize aspect of the keyframes",
661 default='LINE',
662 items=(
663 ('LINE', 'Line',
664 'Keyframe displayed as thick lines', 'SNAP_INCREMENT', 0),
665 ('SQUARE', 'Square',
666 'Keyframe displayed as squares', 'HANDLETYPE_VECTOR_VEC', 1),
667 ('DIAMOND', 'Diamond',
668 'Keyframe displayed as diamonds', 'HANDLETYPE_FREE_VEC', 2),
672 def draw_ts_pref(prefs, layout):
673 # - General settings
674 layout.label(text='Timeline Scrub:')
675 layout.prop(prefs, 'use')
676 if not prefs.use:
677 return
678 layout.prop(prefs, 'evaluate_gp_obj_key')
679 layout.prop(prefs, 'pixel_step')
681 # -/ Keymap -
682 box = layout.box()
683 box.label(text='Keymap:')
684 box.operator('animation.ts_set_keymap',
685 text='Click here to change shortcut')
687 if prefs.keycode:
688 row = box.row(align=True)
689 row.prop(prefs, 'use_ctrl', text='Ctrl')
690 row.prop(prefs, 'use_alt', text='Alt')
691 row.prop(prefs, 'use_shift', text='Shift')
692 # -/Cosmetic-
693 icon = None
694 if prefs.keycode == 'LEFTMOUSE':
695 icon = 'MOUSE_LMB'
696 elif prefs.keycode == 'MIDDLEMOUSE':
697 icon = 'MOUSE_MMB'
698 elif prefs.keycode == 'RIGHTMOUSE':
699 icon = 'MOUSE_RMB'
700 if icon:
701 row.label(text=f'{prefs.keycode}', icon=icon)
702 # -Cosmetic-/
703 else:
704 row.label(text=f'Key: {prefs.keycode}')
706 else:
707 box.label(text='[ NOW TYPE KEY OR CLICK TO USE, WITH MODIFIER ]')
709 if prefs.always_snap:
710 snap_text = 'Disable keyframes snap: '
711 else:
712 snap_text = 'Keyframes snap: '
714 snap_text += 'Left Mouse' if prefs.keycode == 'RIGHTMOUSE' else 'Right Mouse'
715 if not prefs.use_ctrl:
716 snap_text += ' or Ctrl'
717 if not prefs.use_shift:
718 snap_text += ' or Shift'
719 if not prefs.use_alt:
720 snap_text += ' or Alt'
722 if prefs.rolling_mode:
723 snap_text = 'Gap-less mode (always snap)'
725 box.label(text=snap_text, icon='SNAP_ON')
726 if prefs.keycode in ('LEFTMOUSE', 'RIGHTMOUSE', 'MIDDLEMOUSE') and not prefs.use_ctrl and not prefs.use_alt and not prefs.use_shift:
727 box.label(
728 text="Recommended to choose at least one modifier to combine with clicks (default: Ctrl+Alt)", icon="ERROR")
730 row = box.row()
731 row.prop(prefs, 'always_snap')
732 row.prop(prefs, 'rolling_mode')
733 box.prop(prefs, 'use_in_timeline_editor',
734 text='Add same shortcut to scrub within timeline editors')
736 # - HUD/OSD
737 box = layout.box()
738 box.prop(prefs, 'use_hud')
740 col = box.column()
741 row = col.row()
742 row.prop(prefs, 'color_timeline')
743 row.prop(prefs, 'color_playhead', text='Cursor And Text Color')
744 col.label(text='Show:')
745 row = col.row()
746 row.prop(prefs, 'use_hud_time_line')
747 row.prop(prefs, 'lines_size')
748 row = col.row()
749 row.prop(prefs, 'use_hud_playhead')
750 row.prop(prefs, 'playhead_size')
751 row = col.row()
752 row.prop(prefs, 'use_hud_keyframes')
753 row.prop(prefs, 'keyframe_aspect', text='')
754 row = col.row()
755 row.prop(prefs, 'use_hud_frame_current')
756 row.prop(prefs, 'use_hud_frame_offset')
757 col.enabled = prefs.use_hud
760 # --- Keymap
762 addon_keymaps = []
765 def register_keymaps():
766 prefs = get_addon_prefs().ts
767 if not prefs.use:
768 return
770 kc = bpy.context.window_manager.keyconfigs.addon
771 if kc is None:
772 return
774 km = kc.keymaps.new(name="Grease Pencil", space_type="EMPTY", region_type='WINDOW')
776 if not prefs.keycode:
777 print(r'/!\ Timeline scrub: no keycode entered for keymap')
778 return
779 kmi = km.keymap_items.new(
780 'animation.time_scrub',
781 type=prefs.keycode, value='PRESS',
782 alt=prefs.use_alt, ctrl=prefs.use_ctrl, shift=prefs.use_shift, any=False)
783 kmi.repeat = False
784 addon_keymaps.append((km, kmi))
786 # - Add keymap in timeline editors
787 if prefs.use_in_timeline_editor:
789 editor_l = [
790 ('Dopesheet', 'DOPESHEET_EDITOR', 'anim.change_frame'),
791 ('Graph Editor', 'GRAPH_EDITOR', 'graph.cursor_set'),
792 ("NLA Editor", "NLA_EDITOR", 'anim.change_frame'),
793 ("Sequencer", "SEQUENCE_EDITOR", 'anim.change_frame')
794 # ("Clip Graph Editor", "CLIP_EDITOR", 'clip.change_frame'),
797 for editor, space, operator in editor_l:
798 km = kc.keymaps.new(name=editor, space_type=space)
799 kmi = km.keymap_items.new(
800 operator, type=prefs.keycode, value='PRESS',
801 alt=prefs.use_alt, ctrl=prefs.use_ctrl, shift=prefs.use_shift)
802 addon_keymaps.append((km, kmi))
805 def unregister_keymaps():
806 for km, kmi in addon_keymaps:
807 km.keymap_items.remove(kmi)
808 addon_keymaps.clear()
810 # --- REGISTER ---
812 classes = (
813 GPTS_OT_time_scrub,
814 GPTS_OT_set_scrub_keymap,
817 def register():
818 for cls in classes:
819 bpy.utils.register_class(cls)
820 register_keymaps()
822 def unregister():
823 unregister_keymaps()
824 for cls in reversed(classes):
825 bpy.utils.unregister_class(cls)