diff options
author | Pullusb <bernou.samuel@gmail.com> | 2021-01-24 20:36:37 +0300 |
---|---|---|
committer | Pullusb <bernou.samuel@gmail.com> | 2021-01-24 20:36:37 +0300 |
commit | 76fba3f84b6fdcc350d232eb740c0cd72ac9ffb7 (patch) | |
tree | 264afd1bda0685b5f8b3ff7c4e4926e3e1acd192 /greasepencil_tools | |
parent | cd176b2617bd3ede969c3aa218ee54a79fc69f27 (diff) |
GPencil Tools: Add viewport timeline scrub feature
Add timeline scrubbing from the standalone add-on : https://github.com/Pullusb/viewport_timeline_scrub
This bind a shortcut to move in timeline directly from viewport, VSE and movie clip editors.
Display keyframes and snap using `Ctrl` key.
Propagate the chosen shortcut to timeline editors.
Shortcut, behavior and overlays can be customized in addon-preferences.
Diffstat (limited to 'greasepencil_tools')
-rw-r--r-- | greasepencil_tools/__init__.py | 5 | ||||
-rw-r--r-- | greasepencil_tools/prefs.py | 13 | ||||
-rw-r--r-- | greasepencil_tools/timeline_scrub.py | 807 |
3 files changed, 823 insertions, 2 deletions
diff --git a/greasepencil_tools/__init__.py b/greasepencil_tools/__init__.py index 02e93c61..abc7a033 100644 --- a/greasepencil_tools/__init__.py +++ b/greasepencil_tools/__init__.py @@ -21,7 +21,7 @@ bl_info = { "name": "Grease Pencil Tools", "description": "Extra tools for Grease Pencil", "author": "Samuel Bernou, Antonio Vazquez, Daniel Martinez Lara, Matias Mendiola", -"version": (1, 1, 6), +"version": (1, 2, 0), "blender": (2, 91, 0), "location": "Sidebar > Grease Pencil > Grease Pencil Tools", "warning": "", @@ -36,12 +36,14 @@ from . import (prefs, box_deform, line_reshape, rotate_canvas, + timeline_scrub, import_brush_pack, ui_panels, ) def register(): prefs.register() + timeline_scrub.register() box_deform.register() line_reshape.register() rotate_canvas.register() @@ -57,6 +59,7 @@ def unregister(): rotate_canvas.unregister() box_deform.unregister() line_reshape.unregister() + timeline_scrub.unregister() prefs.unregister() if __name__ == "__main__": diff --git a/greasepencil_tools/prefs.py b/greasepencil_tools/prefs.py index 1475e95c..69b92780 100644 --- a/greasepencil_tools/prefs.py +++ b/greasepencil_tools/prefs.py @@ -22,6 +22,7 @@ from bpy.props import ( BoolProperty, EnumProperty, StringProperty, + PointerProperty, # IntProperty, ) @@ -33,6 +34,8 @@ def get_addon_prefs(): addon_prefs = bpy.context.preferences.addons[addon_name].preferences return (addon_prefs) +from .timeline_scrub import GPTS_timeline_settings, draw_ts_pref + ## Addons Preferences Update Panel def update_panel(self, context): try: @@ -48,9 +51,11 @@ def auto_rebind(self, context): register_keymaps() class GreasePencilAddonPrefs(bpy.types.AddonPreferences): - bl_idname = os.path.splitext(__name__)[0]#'greasepencil-addon' ... __package__ ? + bl_idname = os.path.splitext(__name__)[0] #'greasepencil-addon' ... __package__ ? # bl_idname = __name__ + ts: PointerProperty(type=GPTS_timeline_settings) + category : StringProperty( name="Category", description="Choose a name for the category of the panel", @@ -127,6 +132,7 @@ class GreasePencilAddonPrefs(bpy.types.AddonPreferences): update=auto_rebind) def draw(self, context): + prefs = get_addon_prefs() layout = self.layout # layout.use_property_split = True row= layout.row(align=True) @@ -176,6 +182,9 @@ class GreasePencilAddonPrefs(bpy.types.AddonPreferences): box.label(text="view3d.rotate_canvas") box.prop(self, 'canvas_use_hud') + ## SCRUB TIMELINE + box = layout.box() + draw_ts_pref(prefs.ts, box) if self.pref_tabs == 'TUTO': @@ -237,6 +246,7 @@ def unregister_keymaps(): ### REGISTER --- def register(): + bpy.utils.register_class(GPTS_timeline_settings) bpy.utils.register_class(GreasePencilAddonPrefs) # Force box deform running to false bpy.context.preferences.addons[os.path.splitext(__name__)[0]].preferences.boxdeform_running = False @@ -245,3 +255,4 @@ def register(): def unregister(): unregister_keymaps() bpy.utils.unregister_class(GreasePencilAddonPrefs) + bpy.utils.unregister_class(GPTS_timeline_settings)
\ No newline at end of file diff --git a/greasepencil_tools/timeline_scrub.py b/greasepencil_tools/timeline_scrub.py new file mode 100644 index 00000000..7a783e0b --- /dev/null +++ b/greasepencil_tools/timeline_scrub.py @@ -0,0 +1,807 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +'''Based on viewport_timeline_scrub standalone addon - Samuel Bernou''' + +from .prefs import get_addon_prefs + +import numpy as np +from time import time +import bpy +import gpu +import bgl +import blf +from gpu_extras.batch import batch_for_shader + +from bpy.props import (BoolProperty, + StringProperty, + IntProperty, + FloatVectorProperty, + IntProperty, + PointerProperty, + EnumProperty) + +""" bl_info = { + "name": "Viewport Scrub Timeline", + "description": "Scrub on timeline from viewport and snap to nearest keyframe", + "author": "Samuel Bernou", + "version": (0, 7, 5), + "blender": (2, 91, 0), + "location": "View3D > shortcut key chosen in addon prefs", + "warning": "", + "doc_url": "https://github.com/Pullusb/scrub_timeline", + "category": "Object"} + """ + +def nearest(array, value): + ''' + Get a numpy array and a target value + Return closest val found in array to passed value + ''' + idx = (np.abs(array - value)).argmin() + return array[idx] + + +def draw_callback_px(self, context): + '''Draw callback use by modal to draw in viewport''' + if context.area != self.current_area: + return + ## lines and shaders + # 50% alpha, 2 pixel width line + + # text + font_id = 0 + + shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR') # initiate shader + bgl.glEnable(bgl.GL_BLEND) + bgl.glLineWidth(1) + + # - # Draw HUD + if self.use_hud_time_line: + shader.bind() + shader.uniform_float("color", self.color_timeline) + self.batch_timeline.draw(shader) + + # - # Display keyframes + if self.use_hud_keyframes: + if self.keyframe_aspect == 'LINE': + bgl.glLineWidth(3) + shader.bind() + shader.uniform_float("color", self.color_timeline) + self.batch_keyframes.draw(shader) + else: + # - # Display keyframe as diamonds + bgl.glLineWidth(1) + shader.bind() + shader.uniform_float("color", self.color_timeline) + # shader.uniform_float("color", list(self.color_timeline[:3]) + [1]) # timeline color full opacity + # shader.uniform_float("color", (0.8, 0.8, 0.8, 0.8)) # grey + # shader.uniform_float("color", (0.9, 0.69, 0.027, 1.0)) # yellow-ish + # shader.uniform_float("color",(1.0, 0.515, 0.033, 1.0)) # orange 'selected keyframe' + self.batch_keyframes.draw(shader) + + # - # Display init frame text (under playhead) + # if self.use_hud_frame_init: # propertie not existing currently + # blf.position(font_id, self.init_mouse_x, + # self.init_mouse_y - (60 *self.ui_scale), 0) + # blf.size(font_id, 16, self.dpi) + # blf.color(font_id, *self.color_timeline) + # blf.draw(font_id, f'{self.init_frame:.0f}') + + # - # Show current frame line + bgl.glLineWidth(1) + if self.use_hud_playhead: + # -# old full height playhead + # playhead = [(self.cursor_x, 0), (self.cursor_x, context.area.height)] + playhead = [(self.cursor_x, self.my + self.playhead_size/2), + (self.cursor_x, self.my - self.playhead_size/2)] + batch = batch_for_shader(shader, 'LINES', {"pos": playhead}) + shader.bind() + shader.uniform_float("color", self.color_playhead) + batch.draw(shader) + + # restore opengl defaults + bgl.glDisable(bgl.GL_BLEND) + + # - # Display current frame text + blf.color(font_id, *self.color_text) + if self.use_hud_frame_current: + blf.position(font_id, self.mouse[0]+10, self.mouse[1]+10, 0) + # Id, Point size of the font, dots per inch value to use for drawing. + blf.size(font_id, 30, self.dpi) # 72 + blf.draw(font_id, f'{self.new_frame:.0f}') + + # - # Display frame offset text + if self.use_hud_frame_offset: + blf.position(font_id, self.mouse[0]+10, + self.mouse[1]+(40*self.ui_scale), 0) + blf.size(font_id, 16, self.dpi) + # blf.color(font_id, *self.color_text) + sign = '+' if self.offset > 0 else '' + blf.draw(font_id, f'{sign}{self.offset:.0f}') + + +class GPTS_OT_time_scrub(bpy.types.Operator): + bl_idname = "animation.time_scrub" + bl_label = "Time scrub" + bl_description = "Quick time scrubbing with a shortcut" + bl_options = {"REGISTER", "INTERNAL", "UNDO"} + + @classmethod + def poll(cls, context): + return context.space_data.type in ('VIEW_3D', 'SEQUENCE_EDITOR', 'CLIP_EDITOR') + + def invoke(self, context, event): + prefs = get_addon_prefs().ts + # Gpencil contexts : ('PAINT_GPENCIL', 'EDIT_GPENCIL') + # if context.space_data.type != 'VIEW_3D': + # self.report({'WARNING'}, "Work only in Viewport") + # return {'CANCELLED'} + + self.current_area = context.area + self.key = prefs.keycode + self.evaluate_gp_obj_key = prefs.evaluate_gp_obj_key + + self.dpi = context.preferences.system.dpi + self.ui_scale = context.preferences.system.ui_scale + # hud prefs + self.color_timeline = prefs.color_timeline + self.color_playhead = prefs.color_playhead + self.color_text = prefs.color_playhead + self.use_hud_time_line = prefs.use_hud_time_line + self.use_hud_keyframes = prefs.use_hud_keyframes + self.keyframe_aspect = prefs.keyframe_aspect + self.use_hud_playhead = prefs.use_hud_playhead + self.use_hud_frame_current = prefs.use_hud_frame_current + self.use_hud_frame_offset = prefs.use_hud_frame_offset + + self.playhead_size = prefs.playhead_size + self.lines_size = prefs.lines_size + + self.px_step = prefs.pixel_step + # global keycode + # self.key = keycode + self.snap_on = False + self.mouse = (event.mouse_region_x, event.mouse_region_y) + self.init_mouse_x = self.cursor_x = event.mouse_region_x # event.mouse_x + # self.init_mouse_y = event.mouse_region_y # only to display init frame text + self.init_frame = self.new_frame = context.scene.frame_current + self.offset = 0 + self.pos = [] + + # Snap touch control + self.snap_ctrl = not prefs.use_ctrl + self.snap_shift = not prefs.use_shift + self.snap_alt = not prefs.use_alt + self.snap_mouse_key = 'LEFTMOUSE' if self.key == 'RIGHTMOUSE' else 'RIGHTMOUSE' + + ob = context.object + + if context.space_data.type != 'VIEW_3D': + ob = None # do not consider any key + + if ob: # condition to allow empty scrubing + if ob.type != 'GPENCIL' or self.evaluate_gp_obj_key: + # Get objet keyframe position + anim_data = ob.animation_data + action = None + + if anim_data: + action = anim_data.action + if action: + for fcu in action.fcurves: + for kf in fcu.keyframe_points: + if kf.co.x not in self.pos: + self.pos.append(kf.co.x) + + if ob.type == 'GPENCIL': + # Get GP frame position + gpl = ob.data.layers + layer = gpl.active + if layer: + for frame in layer.frames: + if frame.frame_number not in self.pos: + self.pos.append(frame.frame_number) + + # - Add start and end to snap on + if context.scene.use_preview_range: + play_bounds = [context.scene.frame_preview_start, + context.scene.frame_preview_end] + else: + play_bounds = [context.scene.frame_start, context.scene.frame_end] + + # Also snap on play bounds (sliced off for keyframe display) + self.pos += play_bounds + self.pos = np.asarray(self.pos) + + # Disable Onion skin + self.active_space_data = context.space_data + self.onion_skin = None + if context.space_data.type == 'VIEW_3D': # and 'GPENCIL' in context.mode + self.onion_skin = self.active_space_data.overlay.use_gpencil_onion_skin + self.active_space_data.overlay.use_gpencil_onion_skin = False + + self.hud = prefs.use_hud + if not self.hud: + context.window_manager.modal_handler_add(self) + return {'RUNNING_MODAL'} + + # - HUD params + + # line_height = 25 # px + width = context.area.width + right = int((width - self.init_mouse_x) / self.px_step) + left = int(self.init_mouse_x / self.px_step) + + hud_pos_x = [] + for i in range(1, left): + hud_pos_x.append(self.init_mouse_x - i*self.px_step) + for i in range(1, right): + hud_pos_x.append(self.init_mouse_x + i*self.px_step) + + # - list of double coords + + init_height = 60 + frame_height = self.lines_size + key_height = 14 + bound_h = key_height + 19 + bound_bracket_l = self.px_step/2 + + self.my = my = event.mouse_region_y # event.mouse_y + + self.hud_lines = [] + # - # frame marks + for x in hud_pos_x: + self.hud_lines.append((x, my - (frame_height/2))) + self.hud_lines.append((x, my + (frame_height/2))) + + # - # init frame mark + self.hud_lines += [(self.init_mouse_x, my - (init_height/2)), + (self.init_mouse_x, my + (init_height/2))] + + # Add start/end boundary bracket to HUD + + start_x = self.init_mouse_x + \ + (play_bounds[0] - self.init_frame) * self.px_step + end_x = self.init_mouse_x + \ + (play_bounds[1] - self.init_frame) * self.px_step + + # - # start + up = (start_x, my - (bound_h/2)) + dn = (start_x, my + (bound_h/2)) + self.hud_lines.append(up) + self.hud_lines.append(dn) + + self.hud_lines.append(up) + self.hud_lines.append((up[0] + bound_bracket_l, up[1])) + self.hud_lines.append(dn) + self.hud_lines.append((dn[0] + bound_bracket_l, dn[1])) + + # - # end + up = (end_x, my - (bound_h/2)) + dn = (end_x, my + (bound_h/2)) + self.hud_lines.append(up) + self.hud_lines.append(dn) + + self.hud_lines.append(up) + self.hud_lines.append((up[0] - bound_bracket_l, up[1])) + self.hud_lines.append(dn) + self.hud_lines.append((dn[0] - bound_bracket_l, dn[1])) + + # - # Horizontal line + self.hud_lines += [(0, my), (width, my)] + + # - #other method with cutted H line + # leftmost = self.init_mouse_x - (left*self.px_step) + # rightmost = self.init_mouse_x + (right*self.px_step) + # self.hud_lines += [(leftmost, my), (rightmost, my)] + + # - # Prepare batchs to draw static parts + shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR') # initiate shader + self.batch_timeline = batch_for_shader( + shader, 'LINES', {"pos": self.hud_lines}) + + # - # keyframe display + if self.keyframe_aspect == 'LINE': + key_lines = [] + # Slice off position of start/end frame added last (in list for snapping) + for i in self.pos[:-2]: + key_lines.append( + (self.init_mouse_x + ((i-self.init_frame) * self.px_step), my - (key_height/2))) + key_lines.append( + (self.init_mouse_x + ((i-self.init_frame)*self.px_step), my + (key_height/2))) + + self.batch_keyframes = batch_for_shader( + shader, 'LINES', {"pos": key_lines}) + + else: + # diamond and square + # keysize = 6 # 5 for square, 4 or 6 for diamond + keysize = 6 if self.keyframe_aspect == 'DIAMOND' else 5 + upper = 0 + + shaped_key = [] + indices = [] + idx_offset = 0 + for i in self.pos[:-2]: + center = self.init_mouse_x + ((i-self.init_frame)*self.px_step) + if self.keyframe_aspect == 'DIAMOND': + # +1 on x is to correct pixel alignement + shaped_key += [(center-keysize, my+upper), + (center+1, my+keysize+upper), + (center+keysize, my+upper), + (center+1, my-keysize+upper)] + + elif self.keyframe_aspect == 'SQUARE': + shaped_key += [(center-keysize+1, my-keysize+upper), + (center-keysize+1, my+keysize+upper), + (center+keysize, my+keysize+upper), + (center+keysize, my-keysize+upper)] + + indices += [(0+idx_offset, 1+idx_offset, 2+idx_offset), + (0+idx_offset, 2+idx_offset, 3+idx_offset)] + idx_offset += 4 + + self.batch_keyframes = batch_for_shader( + shader, 'TRIS', {"pos": shaped_key}, indices=indices) + + args = (self, context) + self.viewtype = None + self.spacetype = 'WINDOW' # is PREVIEW for VSE, needed for handler remove + + if context.space_data.type == 'VIEW_3D': + self.viewtype = bpy.types.SpaceView3D + self._handle = bpy.types.SpaceView3D.draw_handler_add( + draw_callback_px, args, 'WINDOW', 'POST_PIXEL') + + # - # VSE disabling hud : Doesn't get right coordinates in preview window + elif context.space_data.type == 'SEQUENCE_EDITOR': + self.viewtype = bpy.types.SpaceSequenceEditor + self.spacetype = 'PREVIEW' + self._handle = bpy.types.SpaceSequenceEditor.draw_handler_add( + draw_callback_px, args, 'PREVIEW', 'POST_PIXEL') + + elif context.space_data.type == 'CLIP_EDITOR': + self.viewtype = bpy.types.SpaceClipEditor + self._handle = bpy.types.SpaceClipEditor.draw_handler_add( + draw_callback_px, args, 'WINDOW', 'POST_PIXEL') + + context.window_manager.modal_handler_add(self) + return {'RUNNING_MODAL'} + + def _exit_modal(self, context): + if self.onion_skin is not None: + self.active_space_data.overlay.use_gpencil_onion_skin = self.onion_skin + + if self.hud and self.viewtype: + self.viewtype.draw_handler_remove(self._handle, self.spacetype) + context.area.tag_redraw() + + def modal(self, context, event): + + if event.type == 'MOUSEMOVE': + # - calculate frame offset from pixel offset + # - get mouse.x and add it to initial frame num + self.mouse = (event.mouse_region_x, event.mouse_region_y) + # self.mouse = (event.mouse_x, event.mouse_y) + + px_offset = (event.mouse_region_x - self.init_mouse_x) + # int to overtake frame before change, use round to snap to closest (not blender style) + self.offset = int(px_offset / self.px_step) + self.new_frame = self.init_frame + self.offset + + mod_snap = False + if self.snap_ctrl and event.ctrl: + mod_snap = True + if self.snap_shift and event.shift: + mod_snap = True + if self.snap_alt and event.alt: + mod_snap = True + + if self.snap_on or mod_snap: + self.new_frame = nearest(self.pos, self.new_frame) + + # context.scene.frame_set(self.new_frame) + context.scene.frame_current = self.new_frame + + # - # follow exactly mouse + # self.cursor_x = event.mouse_x + + # recalculate offset to snap cursor to frame + self.offset = self.new_frame - self.init_frame + # calculate cursor pixel position from frame offset + self.cursor_x = self.init_mouse_x + (self.offset * self.px_step) + # self._compute_timeline(context, event) + + if event.type == 'ESC': + # context.scene.frame_set(self.init_frame) + context.scene.frame_current = self.init_frame + self._exit_modal(context) + return {'CANCELLED'} + + # Snap if pressing NOT used mouse key (right or mid) + if event.type == self.snap_mouse_key: + if event.value == "PRESS": + self.snap_on = True + else: + self.snap_on = False + + if event.type == self.key and event.value == 'RELEASE': + self._exit_modal(context) + return {'FINISHED'} + + # End modal on right clic release ? (relaunched immediately if main key not released) + # if event.type == 'LEFTMOUSE': + # if event.value == "RELEASE": + # self._exit_modal(context) + # return {'FINISHED'} + + return {"RUNNING_MODAL"} + +# --- addon prefs + + +def auto_rebind(self, context): + unregister_keymaps() + register_keymaps() + + +class GPTS_OT_set_scrub_keymap(bpy.types.Operator): + bl_idname = "animation.ts_set_keymap" + bl_label = "Change keymap" + bl_description = "Quick time scrubbing with a shortcut" + bl_options = {"REGISTER", "INTERNAL"} + + def invoke(self, context, event): + self.prefs = get_addon_prefs().ts + self.ctrl = False + self.shift = False + self.alt = False + + self.init_value = self.prefs.keycode + self.prefs.keycode = '' + context.window_manager.modal_handler_add(self) + return {'RUNNING_MODAL'} + + def modal(self, context, event): + exclude_keys = {'MOUSEMOVE', 'INBETWEEN_MOUSEMOVE', + 'TIMER_REPORT', 'ESC', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE'} + exclude_in = ('SHIFT', 'CTRL', 'ALT') + if event.type == 'ESC': + self.prefs.keycode = self.init_value + # self.report({'WARNING'}, 'Cancelled') + return {'CANCELLED'} + + self.ctrl = event.ctrl + self.shift = event.shift + self.alt = event.alt + + if event.type not in exclude_keys and not any(x in event.type for x in exclude_in): + print('key:', event.type, 'value:', event.value) + if event.value == 'PRESS': + self.report({'INFO'}, event.type) + # set the chosen key + self.prefs.keycode = event.type + # -# following condition aren't needed. Just here to avoid unnecessary rebind update (if possible) + if self.prefs.use_shift != event.shift: # condition + self.prefs.use_shift = event.shift + + if self.prefs.use_alt != event.alt: + self.prefs.use_alt = event.alt + + # -# Trigger rebind update with last + self.prefs.use_ctrl = event.ctrl + + # -# no need to rebind updated by of the modifiers props.. + # auto_rebind() + return {'FINISHED'} + + return {"RUNNING_MODAL"} + + +class GPTS_timeline_settings(bpy.types.PropertyGroup): + + keycode: StringProperty( + name="Shortcut", + description="Shortcut to trigger the scrub in viewport during press", + default="MIDDLEMOUSE") + + use_in_timeline_editor: BoolProperty( + name="Shortcut in timeline editors", + description="Add the same shortcut to scrub in timeline editor windows", + default=True, + update=auto_rebind) + + use_shift: BoolProperty( + name="Combine With Shift", + description="Add shift", + default=False, + update=auto_rebind) + + use_alt: BoolProperty( + name="Combine With Alt", + description="Add alt", + default=True, + update=auto_rebind) + + use_ctrl: BoolProperty( + name="Combine With Ctrl", + description="Add ctrl", + default=False, + update=auto_rebind) + + evaluate_gp_obj_key: BoolProperty( + name='Use Gpencil Object Keyframes', + description="Also snap on greasepencil object keyframe (else only active layer frames)", + default=True) + + # options (set) – Enumerator in ['HIDDEN', 'SKIP_SAVE', 'ANIMATABLE', 'LIBRARY_EDITABLE', 'PROPORTIONAL','TEXTEDIT_UPDATE']. + pixel_step: IntProperty( + name="Frame Interval On Screen", + description="Pixel steps on screen that represent a frame intervals", + default=10, + min=1, + max=500, + soft_min=2, + soft_max=100, + step=1, + subtype='PIXEL') + + use_hud: BoolProperty( + name='Display Timeline Overlay', + description="Display overlays with timeline information when scrubbing time in viewport", + default=True) + + use_hud_time_line: BoolProperty( + name='Timeline', + description="Display a static marks overlay to represent timeline when scrubbing", + default=True) + + use_hud_keyframes: BoolProperty( + name='Keyframes', + description="Display shapes overlay to show keyframe position when scrubbing", + default=True) + + use_hud_playhead: BoolProperty( + name='Playhead', + description="Display the playhead as a vertical line to show position in time", + default=True) + + use_hud_frame_current: BoolProperty( + name='Text Frame Current', + description="Display the current frame as text above mouse cursor", + default=True) + + use_hud_frame_offset: BoolProperty( + name='Text Frame Offset', + description="Display frame offset from initial position as text above mouse cursor", + default=True) + + color_timeline: FloatVectorProperty( + name="Timeline Color", + subtype='COLOR_GAMMA', + size=4, + default=(0.5, 0.5, 0.5, 0.6), + min=0.0, max=1.0, + description="Color of the temporary timeline") + + color_playhead: FloatVectorProperty( + name="Cusor Color", + subtype='COLOR_GAMMA', + size=4, + default=(0.01, 0.64, 1.0, 0.8), # red (0.9, 0.3, 0.3, 0.8) + min=0.0, max=1.0, + description="Color of the temporary line cursor and text") + + # - # sizes + playhead_size: IntProperty( + name="Playhead Size", + description="Playhead height in pixels", + default=100, + min=2, + max=10000, + soft_min=10, + soft_max=5000, + step=1, + subtype='PIXEL') + + lines_size: IntProperty( + name="Frame Lines Size", + description="Frame lines height in pixels", + default=10, + min=1, + max=10000, + soft_min=5, + soft_max=40, + step=1, + subtype='PIXEL') + + keyframe_aspect: EnumProperty( + name="Keyframe Display", + description="Customize aspect of the keyframes", + default='LINE', + items=( + ('LINE', 'Line', + 'Keyframe displayed as thick lines', 'SNAP_INCREMENT', 0), + ('SQUARE', 'Square', + 'Keyframe displayed as squares', 'HANDLETYPE_VECTOR_VEC', 1), + ('DIAMOND', 'Diamond', + 'Keyframe displayed as diamonds', 'HANDLETYPE_FREE_VEC', 2), + )) + + +def draw_ts_pref(prefs, layout): + # layout.use_property_split = True + + # - # General settings + layout.label(text='Timeline Scrub:') + layout.prop(prefs, 'evaluate_gp_obj_key') + # Make a keycode capture system or find a way to display keymap with full_event=True + layout.prop(prefs, 'pixel_step') + + # -/ Keymap - + box = layout.box() + box.label(text='Keymap:') + box.operator('animation.ts_set_keymap', + text='Click here to change shortcut') + + if prefs.keycode: + row = box.row(align=True) + row.prop(prefs, 'use_ctrl', text='Ctrl') + row.prop(prefs, 'use_alt', text='Alt') + row.prop(prefs, 'use_shift', text='Shift') + # -/Cosmetic- + icon = None + if prefs.keycode == 'LEFTMOUSE': + icon = 'MOUSE_LMB' + elif prefs.keycode == 'MIDDLEMOUSE': + icon = 'MOUSE_MMB' + elif prefs.keycode == 'RIGHTMOUSE': + icon = 'MOUSE_RMB' + if icon: + row.label(text=f'{prefs.keycode}', icon=icon) + # -Cosmetic-/ + else: + row.label(text=f'Key: {prefs.keycode}') + + else: + box.label(text='[ NOW TYPE KEY OR CLICK TO USE, WITH MODIFIER ]') + + snap_text = 'Snap to keyframes: ' + snap_text += 'Left Mouse' if prefs.keycode == 'RIGHTMOUSE' else 'Right Mouse' + if not prefs.use_ctrl: + snap_text += ' or Ctrl' + if not prefs.use_shift: + snap_text += ' or Shift' + if not prefs.use_alt: + snap_text += ' or Alt' + box.label(text=snap_text, icon='SNAP_ON') + if prefs.keycode in ('LEFTMOUSE', 'RIGHTMOUSE', 'MIDDLEMOUSE') and not prefs.use_ctrl and not prefs.use_alt and not prefs.use_shift: + box.label( + text="Recommended to choose at least one modifier to combine with clicks (default: Ctrl+Alt)", icon="ERROR") + + box.prop(prefs, 'use_in_timeline_editor', + text='Add same shortcut to scrub within timeline editors') + + # - # HUD/OSD + + box = layout.box() + box.prop(prefs, 'use_hud') + + col = box.column() + row = col.row() + row.prop(prefs, 'color_timeline') + row.prop(prefs, 'color_playhead', text='Cursor And Text Color') + col.label(text='Show:') + row = col.row() + row.prop(prefs, 'use_hud_time_line') + row.prop(prefs, 'lines_size') + row = col.row() + row.prop(prefs, 'use_hud_playhead') + row.prop(prefs, 'playhead_size') + row = col.row() + row.prop(prefs, 'use_hud_keyframes') + row.prop(prefs, 'keyframe_aspect', text='') + row = col.row() + row.prop(prefs, 'use_hud_frame_current') + row.prop(prefs, 'use_hud_frame_offset') + col.enabled = prefs.use_hud + + +# --- Keymap + + +addon_keymaps = [] + + +def register_keymaps(): + prefs = get_addon_prefs().ts + addon = bpy.context.window_manager.keyconfigs.addon + # km = addon.keymaps.new(name = "3D View", space_type = "VIEW_3D") + km = addon.keymaps.new(name="Grease Pencil", + space_type="EMPTY", region_type='WINDOW') + + if not prefs.keycode: + print(r'/!\ Timeline scrub: no keycode entered for keymap') + return + kmi = km.keymap_items.new( + 'animation.time_scrub', + type=prefs.keycode, value='PRESS', + alt=prefs.use_alt, ctrl=prefs.use_ctrl, shift=prefs.use_shift, any=False) + kmi.repeat = False + addon_keymaps.append((km, kmi)) + + # - # Add keymap in timeline editors + if prefs.use_in_timeline_editor: + + editor_l = [ + ('Dopesheet', 'DOPESHEET_EDITOR', 'anim.change_frame'), + ('Graph Editor', 'GRAPH_EDITOR', 'graph.cursor_set'), + ("NLA Editor", "NLA_EDITOR", 'anim.change_frame'), + ("Sequencer", "SEQUENCE_EDITOR", 'anim.change_frame') + # ("Clip Graph Editor", "CLIP_EDITOR", 'clip.change_frame'), + ] + + for editor, space, operator in editor_l: + # region_type='WINDOW') + km = addon.keymaps.new(name=editor, space_type=space) + kmi = km.keymap_items.new( + operator, type=prefs.keycode, value='PRESS', + alt=prefs.use_alt, ctrl=prefs.use_ctrl, shift=prefs.use_shift) + # kmi.repeat = False + addon_keymaps.append((km, kmi)) + + +def unregister_keymaps(): + for km, kmi in addon_keymaps: + km.keymap_items.remove(kmi) + addon_keymaps.clear() + +# --- REGISTER --- + + +classes = ( + # GPTS_timeline_settings, ## registered in prefs.py + GPTS_OT_time_scrub, + GPTS_OT_set_scrub_keymap, +) + + +def register(): + # other_file.register() + for cls in classes: + bpy.utils.register_class(cls) + + # if not bpy.app.background: + register_keymaps() + + #bpy.types.Scene.pgroup_name = bpy.props.PointerProperty(type = GPTS_PGT_settings) + + +def unregister(): + # if not bpy.app.background: + unregister_keymaps() + # other_file.unregister() + + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + #del bpy.types.Scene.pgroup_name + + +if __name__ == "__main__": + register() |