# SPDX-License-Identifier: GPL-3.0-or-later # Copyright 2016-2020 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors # This file is part of Power Sequencer. import bpy import bgl import gpu import math from mathutils import Vector from .utils.functions import ( find_strips_mouse, trim_strips, find_snap_candidate, find_closest_surrounding_cuts, ) from .utils.draw import ( draw_line, draw_rectangle, draw_triangle_equilateral, draw_arrow_head, get_color_gizmo_primary, get_color_gizmo_secondary, ) from .utils.doc import doc_name, doc_idname, doc_brief, doc_description if not bpy.app.background: SHADER = gpu.shader.from_builtin("2D_UNIFORM_COLOR") class POWER_SEQUENCER_OT_mouse_trim(bpy.types.Operator): """ *brief* Cut or Trim strips quickly with the mouse cursor Click somewhere in the Sequencer to insert a cut, click and drag to trim With this function you can quickly cut and remove a section of strips while keeping or collapsing the remaining gap. Press Ctrl to snap to cuts. A [video demo](https://youtu.be/GiLmDhmMVAM?t=1m35s) is available. """ doc = { "name": doc_name(__qualname__), "demo": "https://i.imgur.com/wVvX4ex.gif", "description": doc_description(__doc__), "shortcuts": [ ( {"type": "T", "value": "PRESS"}, {"select_mode": "CONTEXT", "gap_remove": False}, "Trim using the mouse cursor", ), ( {"type": "T", "value": "PRESS", "shift": True}, {"select_mode": "CURSOR", "gap_remove": True}, "Trim in all channels", ), ( {"type": "T", "value": "PRESS", "shift": True, "alt": True}, {"select_mode": "CURSOR", "gap_remove": True}, "Trim in all channels and remove gaps", ), ( {"type": "T", "value": "PRESS", "ctrl": True}, {"select_mode": "CONTEXT", "gap_remove": False}, "Trim using the mouse cursor", ), ( {"type": "T", "value": "PRESS", "ctrl": True, "alt": True}, {"select_mode": "CONTEXT", "gap_remove": True}, "Trim using the mouse cursor and remove gaps", ), ( {"type": "T", "value": "PRESS", "ctrl": True, "shift": True}, {"select_mode": "CURSOR", "gap_remove": True}, "Trim in all channels", ), ( {"type": "T", "value": "PRESS", "ctrl": True, "shift": True, "alt": True}, {"select_mode": "CURSOR", "gap_remove": True}, "Trim in all channels and remove gaps", ), ], "keymap": "Sequencer", } bl_idname = doc_idname(__qualname__) bl_label = doc["name"] bl_description = doc_brief(doc["description"]) bl_options = {"REGISTER", "UNDO"} select_mode: bpy.props.EnumProperty( items=[ ("CURSOR", "Time cursor", "Select all of the strips the time cursor overlaps"), ("CONTEXT", "Smart", "Uses the selection if possible, else uses the other modes"), ], name="Selection mode", description="Cut only the strip under the mouse or all strips under the time cursor", default="CONTEXT", ) select_linked: bpy.props.BoolProperty( name="Use linked time", description="In mouse or CONTEXT mode, always cut linked strips if this is checked", default=False, ) gap_remove: bpy.props.BoolProperty( name="Remove gaps", description="When trimming the sequences, remove gaps automatically", default=True, ) TABLET_TRIM_DISTANCE_THRESHOLD = 6 # Don't rename these variables, we're using setattr to access them dynamically trim_start, channel_start = 0, 0 trim_end, channel_end = 0, 0 is_trimming = False trim_side = "end" mouse_start_y = -1.0 draw_handler = None use_audio_scrub = False event_shift_released = True event_alt_released = True event_ripple, event_ripple_string = "LEFT_ALT", "Alt" event_select_mode, event_select_mode_string = "LEFT_SHIFT", "Shift" event_change_side = "O" @classmethod def poll(cls, context): return context.sequences def invoke(self, context, event): if context.screen.is_animation_playing: bpy.ops.screen.animation_cancel(restore_frame=False) self.use_audio_scrub = context.scene.use_audio_scrub context.scene.use_audio_scrub = False self.mouse_start_y = event.mouse_region_y self.trim_initialize(context, event) self.update_frame(context, event) self.draw_start(context, event) self.update_header_text(context, event) context.window_manager.modal_handler_add(self) return {"RUNNING_MODAL"} def modal(self, context, event): if event.type == self.event_change_side and event.value == "PRESS": self.trim_side = "start" if self.trim_side == "end" else "end" if event.type == self.event_ripple and event.value == "PRESS": self.gap_remove = False if self.gap_remove else True if event.type == self.event_select_mode and event.value == "PRESS": self.select_mode = "CONTEXT" if self.select_mode == "CURSOR" else "CURSOR" if event.type in {"ESC"}: self.draw_stop() context.scene.use_audio_scrub = self.use_audio_scrub return {"FINISHED"} # Start and end trim if event.type == "LEFTMOUSE" or (event.type in ["RET", "T"] and event.value == "PRESS"): self.trim_apply(context, event) self.draw_stop() context.scene.use_audio_scrub = self.use_audio_scrub return {"FINISHED"} # Update trim if event.type == "MOUSEMOVE": self.draw_stop() self.update_frame(context, event) self.draw_start(context, event) self.update_header_text(context, event) return {"PASS_THROUGH"} return {"RUNNING_MODAL"} def trim_initialize(self, context, event): frame, self.channel_start = get_frame_and_channel(event) self.trim_start = find_snap_candidate(context, frame) if event.ctrl else frame self.trim_end, self.channel_end = self.trim_start, self.channel_start self.is_trimming = True def update_frame(self, context, event): frame, channel = get_frame_and_channel(event) frame_trim = find_snap_candidate(context, frame) if event.ctrl else frame setattr(self, "channel_" + self.trim_side, channel) setattr(self, "trim_" + self.trim_side, frame_trim) context.scene.frame_current = getattr(self, "trim_" + self.trim_side) def draw_start(self, context, event): """Initializes the drawing handler, see draw()""" to_trim, to_delete = self.find_strips_to_trim(context) target_strips = to_trim + to_delete draw_args = (self, context, self.trim_start, self.trim_end, target_strips, self.gap_remove) self.draw_handler = bpy.types.SpaceSequenceEditor.draw_handler_add( draw, draw_args, "WINDOW", "POST_PIXEL" ) def draw_stop(self): if self.draw_handler: bpy.types.SpaceSequenceEditor.draw_handler_remove(self.draw_handler, "WINDOW") def update_header_text(self, context, event): text = ( "Trim from {} to {}".format(self.trim_start, self.trim_end) + ", " + "({}) Gap Remove {}".format( self.event_ripple_string, "ON" if self.gap_remove else "OFF" ) + ", " + "({}) Mode: {}".format(self.event_select_mode_string, self.select_mode.capitalize()) + ", " + "(Ctrl) Snap: {}".format("ON" if event.ctrl else "OFF") + ", " + "({}) Change Side".format(self.event_change_side) ) context.area.header_text_set(text) def trim_apply(self, context, event): start_x = context.region.view2d.region_to_view( x=event.mouse_region_x, y=event.mouse_region_y )[0] distance_to_start = abs(event.mouse_region_x - start_x) is_cutting = ( self.trim_start == self.trim_end or event.is_tablet and distance_to_start <= self.TABLET_TRIM_DISTANCE_THRESHOLD ) if is_cutting: self.cut(context) else: self.trim(context) self.is_trimming = False def cut(self, context): to_cut = self.find_strips_to_cut(context) bpy.ops.sequencer.select_all(action="DESELECT") for s in to_cut: s.select = True if len(to_cut) == 0: bpy.ops.power_sequencer.gap_remove() else: frame_current = context.scene.frame_current context.scene.frame_current = self.trim_start bpy.ops.sequencer.split(frame=context.scene.frame_current, type="SOFT", side="BOTH") context.scene.frame_current = frame_current def find_strips_to_cut(self, context): """ Returns a list of strips to cut, either the strip hovered by the mouse or all strips under the time cursor, depending on the select_mode """ to_cut, overlapping_strips = [], [] if self.select_mode == "CONTEXT": overlapping_strips = find_strips_mouse( context, self.trim_start, self.channel_start, self.select_linked ) to_cut.extend(overlapping_strips) if self.select_mode == "CURSOR" or ( not overlapping_strips and self.select_mode == "CONTEXT" ): to_cut = [ s for s in context.sequences if not s.lock and s.frame_final_start <= self.trim_start <= s.frame_final_end ] return to_cut def trim(self, context): to_trim, to_delete = self.find_strips_to_trim(context) trim_strips(context, self.trim_start, self.trim_end, to_trim, to_delete) if (self.gap_remove and self.select_mode == "CURSOR") or ( self.select_mode == "CONTEXT" and to_trim == [] and to_delete == [] ): context.scene.frame_current = min(self.trim_start, self.trim_end) bpy.ops.power_sequencer.gap_remove() else: context.scene.frame_current = self.trim_end def find_strips_to_trim(self, context): """ Returns two lists of strips to trim and strips to delete """ to_trim, to_delete = [], [] trim_start = min(self.trim_start, self.trim_end) trim_end = max(self.trim_start, self.trim_end) channel_min = min(self.channel_start, self.channel_end) channel_max = max(self.channel_start, self.channel_end) channels = set(range(channel_min, channel_max + 1)) for s in context.sequences: if s.lock: continue if self.select_mode == "CONTEXT" and s.channel not in channels: continue if trim_start <= s.frame_final_start and trim_end >= s.frame_final_end: to_delete.append(s) continue if ( s.frame_final_start <= trim_start <= s.frame_final_end or s.frame_final_start <= trim_end <= s.frame_final_end ): to_trim.append(s) return to_trim, to_delete def draw(self, context, frame_start=-1, frame_end=-1, target_strips=[], draw_arrows=False): """ Draws the line and arrows that represent the trim Params: - start_x and end_x are Vector(), the start_x and end_x of the drawn trim line's vertices in region coordinates """ view_to_region = context.region.view2d.view_to_region # Detect and draw the gap's limits if not trimming any strips if not target_strips: strip_before, strip_after = find_closest_surrounding_cuts(context, frame_end) frame_start = strip_before.frame_final_end frame_end = strip_after.frame_final_start channels = [strip_before.channel, strip_after.channel] else: channels = {s.channel for s in target_strips} start_x, start_y = view_to_region( min(frame_start, frame_end), math.floor(min(channels)), clip=False ) end_x, end_y = view_to_region( max(frame_start, frame_end), math.floor(max(channels) + 1), clip=False ) start_x = max(start_x, context.region.x) start_y = max(start_y, context.region.y) end_x = min(end_x, context.region.x + context.region.width) end_y = min(end_y, context.region.y + context.region.height) # Draw color_line = get_color_gizmo_primary(context) color_fill = color_line.copy() color_fill[-1] = 0.3 rect_origin = Vector((start_x, start_y)) rect_size = Vector((end_x - start_x, abs(start_y - end_y))) bgl.glEnable(bgl.GL_BLEND) bgl.glLineWidth(3) draw_rectangle(SHADER, rect_origin, rect_size, color_fill) # Vertical lines draw_line(SHADER, Vector((start_x, start_y)), Vector((start_x, end_y)), color_line) draw_line(SHADER, Vector((end_x, start_y)), Vector((end_x, end_y)), color_line) offset = 20.0 radius = 12.0 if draw_arrows and end_x - start_x > 2 * offset + radius: center_y = (end_y + start_y) / 2.0 center_1 = Vector((start_x + offset, center_y)) center_2 = Vector((end_x - offset, center_y)) draw_triangle_equilateral(SHADER, center_1, radius, color=color_line) draw_triangle_equilateral(SHADER, center_2, radius, math.pi, color=color_line) bgl.glLineWidth(1) bgl.glDisable(bgl.GL_BLEND) def get_frame_and_channel(event): """ Returns a tuple of (frame, channel) """ frame_float, channel_float = bpy.context.region.view2d.region_to_view( x=event.mouse_region_x, y=event.mouse_region_y ) return round(frame_float), math.floor(channel_float)