Skip to content
Snippets Groups Projects
timeline_scrub.py 28.2 KiB
Newer Older
  • Learn to ignore specific revisions
  • # SPDX-License-Identifier: GPL-2.0-or-later
    
    
    '''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)
    
    
    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
    
        # text
        font_id = 0
    
        shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')  # initiate shader
        bgl.glEnable(bgl.GL_BLEND)
        bgl.glLineWidth(1)
    
    
    Pullusb's avatar
    Pullusb committed
        # Draw HUD
    
        if self.use_hud_time_line:
            shader.bind()
            shader.uniform_float("color", self.color_timeline)
            self.batch_timeline.draw(shader)
    
    
    Pullusb's avatar
    Pullusb committed
        # 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:
                bgl.glLineWidth(1)
                shader.bind()
                shader.uniform_float("color", self.color_timeline)
                self.batch_keyframes.draw(shader)
    
    
    Pullusb's avatar
    Pullusb committed
        # Show current frame line
    
        bgl.glLineWidth(1)
        if self.use_hud_playhead:
            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)
    
    
    Pullusb's avatar
    Pullusb committed
        # 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)
    
    Pullusb's avatar
    Pullusb committed
            blf.size(font_id, 30, self.dpi)
    
            blf.draw(font_id, f'{self.new_frame:.0f}')
    
    
    Pullusb's avatar
    Pullusb committed
        # 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)
            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
    
            self.current_area = context.area
            self.key = prefs.keycode
            self.evaluate_gp_obj_key = prefs.evaluate_gp_obj_key
    
            self.always_snap = prefs.always_snap
    
            self.rolling_mode = prefs.rolling_mode
    
    
            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
            self.snap_on = False
            self.mouse = (event.mouse_region_x, event.mouse_region_y)
    
    Pullusb's avatar
    Pullusb committed
            self.init_mouse_x = self.cursor_x = event.mouse_region_x
    
            # self.init_mouse_y = event.mouse_region_y # only to display init frame text
    
            self.cancel_frame = self.init_frame = self.new_frame = context.scene.frame_current
    
            self.lock_range = context.scene.lock_frame_selection_to_range
            if context.scene.use_preview_range:
                self.f_start = context.scene.frame_preview_start
                self.f_end = context.scene.frame_preview_end
            else:
                self.f_start = context.scene.frame_start
                self.f_end = context.scene.frame_end
    
            self.offset = 0
            self.pos = []
    
    
    Pullusb's avatar
    Pullusb committed
            # Snap 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)
    
            if not ob or not self.pos:
                # Disable inverted behavior if no frame to snap
                self.always_snap = False
    
                if self.rolling_mode:
                    self.report({'WARNING'}, 'No Keys to flip on')
                    return {'CANCELLED'}
    
            if self.rolling_mode:
                # sorted and casted to int list since it's going to work with indexes
                self.pos = sorted([int(f) for f in self.pos])
                # find and make current frame the "starting" frame (force snap)
                active_pos = [i for i, num in enumerate(self.pos) if num <= self.init_frame]
                if active_pos:
                    self.init_index = active_pos[-1]
                    self.init_frame = self.new_frame = self.pos[self.init_index]
                else:
                    self.init_index = 0
                    self.init_frame = self.new_frame = self.pos[0]
    
                # del active_pos
                self.index_limit = len(self.pos) - 1
    
    
            # Also snap on play bounds (sliced off for keyframe display)
    
            self.pos += [self.f_start, self.f_end]
    
    
            # Disable Onion skin
            self.active_space_data = context.space_data
            self.onion_skin = None
    
            self.multi_frame = 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
    
            if ob and ob.type == 'GPENCIL':
                if ob.data.use_multiedit:
                    self.multi_frame = ob.data.use_multiedit
                    ob.data.use_multiedit = False
    
    
            self.hud = prefs.use_hud
            if not self.hud:
    
                ## Same as end settings when HUD is On
                if self.lock_range:
                    self.pos = [i for i in self.pos if self.f_start <= i <= self.f_end]
                self.pos = np.asarray(self.pos)
                if self.rolling_mode:
                    context.scene.frame_current = self.new_frame
    
                context.window_manager.modal_handler_add(self)
                return {'RUNNING_MODAL'}
    
            # - HUD params
            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
    
    
    Pullusb's avatar
    Pullusb committed
            self.my = my = event.mouse_region_y
    
            if not self.rolling_mode:
                # 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)))
    
    Pullusb's avatar
    Pullusb committed
            # init frame mark
    
            self.hud_lines += [(self.init_mouse_x, my - (init_height/2)),
                               (self.init_mouse_x, my + (init_height/2))]
    
    
            if not self.rolling_mode:
                # Add start/end boundary bracket to HUD
                start_x = self.init_mouse_x + \
                    (self.f_start - self.init_frame) * self.px_step
                end_x = self.init_mouse_x + \
                    (self.f_end - 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]))
    
    Pullusb's avatar
    Pullusb committed
            # Horizontal line
    
            self.hud_lines += [(0, my), (width, my)]
    
    
    Pullusb's avatar
    Pullusb committed
            # 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})
    
            if self.rolling_mode:
                current_id = self.pos.index(self.new_frame)
                # Add init_frame to "cancel" it in later UI code
                ui_key_pos = [i - current_id + self.init_frame for i, _f in enumerate(self.pos[:-2])]
            else:
                ui_key_pos = self.pos[:-2]
    
    Pullusb's avatar
    Pullusb committed
            # 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 ui_key_pos:
    
                    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
    
    Pullusb's avatar
    Pullusb committed
                # keysize5 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 ui_key_pos:
    
                    center = self.init_mouse_x + ((i-self.init_frame)*self.px_step)
                    if self.keyframe_aspect == 'DIAMOND':
    
                        # +1 on x is to correct pixel alignment
    
                        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)
    
    
            # Trim snapping list of frame outside of frame range if range lock activated
            # (after drawing batch so those are still showed)
            if self.lock_range:
                self.pos = [i for i in self.pos if self.f_start <= i <= self.f_end]
    
            # convert frame list to array for numpy snap utility
            self.pos = np.asarray(self.pos)
    
    
            if self.rolling_mode:
                context.scene.frame_current = self.new_frame
    
            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')
    
            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.multi_frame:
                context.object.data.use_multiedit = self.multi_frame
    
            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)
    
                px_offset = (event.mouse_region_x - self.init_mouse_x)
                self.offset = int(px_offset / self.px_step)
                self.new_frame = self.init_frame + self.offset
    
    
                if self.rolling_mode:
                    # Frame Flipping mode (equidistant scrub snap)
                    self.index = self.init_index + self.offset
                    # clamp to possible index range
                    self.index = min(max(self.index, 0), self.index_limit)
                    self.new_frame = self.pos[self.index]
                    context.scene.frame_current = self.new_frame
                    self.cursor_x = self.init_mouse_x + (self.offset * self.px_step)
    
                else:
                    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
    
                    ## Snapping
                    if self.always_snap:
                        # inverted snapping behavior
                        if not self.snap_on and not mod_snap:
                            self.new_frame = nearest(self.pos, self.new_frame)
                    else:
                        if self.snap_on or mod_snap:
                            self.new_frame = nearest(self.pos, self.new_frame)
    
                    # frame range restriction
                    if self.lock_range:
                        if self.new_frame < self.f_start:
                            self.new_frame = self.f_start
                        elif self.new_frame > self.f_end:
                            self.new_frame = self.f_end
    
                    # context.scene.frame_set(self.new_frame)
                    context.scene.frame_current = self.new_frame
    
                    # - 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)
    
    
            if event.type == 'ESC':
    
    Pullusb's avatar
    Pullusb committed
                # frame_set(self.init_frame) ?
    
                context.scene.frame_current = self.cancel_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'}
    
            return {"RUNNING_MODAL"}
    
    
    
    Pullusb's avatar
    Pullusb committed
    # --- 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
                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
    
    Pullusb's avatar
    Pullusb committed
                    # Following condition to avoid unnecessary rebind update
                    if self.prefs.use_shift != event.shift:
    
                        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
    
                    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")
    
    
        always_snap: BoolProperty(
            name="Always Snap",
            description="Always snap to keys if any, modifier is used deactivate the snapping\nDisabled if no keyframe found",
            default=False)
    
    
        rolling_mode: BoolProperty(
            name="Rolling Mode",
    
            description="Alternative Gap-less timeline. No time information to quickly roll/flip over keys\nOverride normal and 'always snap' mode",
    
            default=False)
    
    
        use: BoolProperty(
            name="Enable",
            description="Enable/Disable timeline scrub",
            default=True,
            update=auto_rebind)
    
    
        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)
    
        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,
    
    Pullusb's avatar
    Pullusb committed
            default=(0.01, 0.64, 1.0, 0.8),
    
            min=0.0, max=1.0,
            description="Color of the temporary line cursor and text")
    
    
    Pullusb's avatar
    Pullusb committed
        # 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):
    
    Pullusb's avatar
    Pullusb committed
        # - General settings
    
        layout.label(text='Timeline Scrub:')
    
        layout.prop(prefs, 'use')
        if not prefs.use:
            return
    
        layout.prop(prefs, 'evaluate_gp_obj_key')
        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 ]')
    
    
        if prefs.always_snap:
            snap_text = 'Disable keyframes snap: '
        else:
            snap_text = 'Keyframes snap: '
    
        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'
    
        if prefs.rolling_mode:
            snap_text = 'Gap-less mode (always snap)'
    
    
        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")
    
    
        row = box.row()
        row.prop(prefs, 'always_snap')
        row.prop(prefs, 'rolling_mode')
    
        box.prop(prefs, 'use_in_timeline_editor',
                 text='Add same shortcut to scrub within timeline editors')
    
    
    Pullusb's avatar
    Pullusb committed
        # - 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
    
        if not prefs.use:
            return
    
    
        addon = bpy.context.window_manager.keyconfigs.addon
        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))
    
    
    Pullusb's avatar
    Pullusb committed
        # - 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:
                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)
                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_OT_time_scrub,
        GPTS_OT_set_scrub_keymap,
    )
    
    def register():
        for cls in classes:
            bpy.utils.register_class(cls)
        register_keymaps()
    
    def unregister():
        unregister_keymaps()
        for cls in reversed(classes):
            bpy.utils.unregister_class(cls)