1 # SPDX-FileCopyrightText: 2016-2020 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
3 # SPDX-License-Identifier: GPL-3.0-or-later
8 from mathutils
import Vector
10 from .utils
.functions
import (
14 find_closest_surrounding_cuts
,
17 from .utils
.draw
import (
20 draw_triangle_equilateral
,
22 get_color_gizmo_primary
,
23 get_color_gizmo_secondary
,
25 from .utils
.doc
import doc_name
, doc_idname
, doc_brief
, doc_description
27 if not bpy
.app
.background
:
28 SHADER
= gpu
.shader
.from_builtin("UNIFORM_COLOR")
31 class POWER_SEQUENCER_OT_mouse_trim(bpy
.types
.Operator
):
33 *brief* Cut or Trim strips quickly with the mouse cursor
36 Click somewhere in the Sequencer to insert a cut, click and drag to trim
37 With this function you can quickly cut and remove a section of strips while keeping or
38 collapsing the remaining gap.
39 Press <kbd>Ctrl</kbd> to snap to cuts.
41 A [video demo](https://youtu.be/GiLmDhmMVAM?t=1m35s) is available.
45 "name": doc_name(__qualname__
),
46 "demo": "https://i.imgur.com/wVvX4ex.gif",
47 "description": doc_description(__doc__
),
50 {"type": "T", "value": "PRESS"},
51 {"select_mode": "CONTEXT", "gap_remove": False},
52 "Trim using the mouse cursor",
55 {"type": "T", "value": "PRESS", "shift": True},
56 {"select_mode": "CURSOR", "gap_remove": True},
57 "Trim in all channels",
60 {"type": "T", "value": "PRESS", "shift": True, "alt": True},
61 {"select_mode": "CURSOR", "gap_remove": True},
62 "Trim in all channels and remove gaps",
65 {"type": "T", "value": "PRESS", "ctrl": True},
66 {"select_mode": "CONTEXT", "gap_remove": False},
67 "Trim using the mouse cursor",
70 {"type": "T", "value": "PRESS", "ctrl": True, "alt": True},
71 {"select_mode": "CONTEXT", "gap_remove": True},
72 "Trim using the mouse cursor and remove gaps",
75 {"type": "T", "value": "PRESS", "ctrl": True, "shift": True},
76 {"select_mode": "CURSOR", "gap_remove": True},
77 "Trim in all channels",
80 {"type": "T", "value": "PRESS", "ctrl": True, "shift": True, "alt": True},
81 {"select_mode": "CURSOR", "gap_remove": True},
82 "Trim in all channels and remove gaps",
85 "keymap": "Sequencer",
87 bl_idname
= doc_idname(__qualname__
)
88 bl_label
= doc
["name"]
89 bl_description
= doc_brief(doc
["description"])
90 bl_options
= {"REGISTER", "UNDO"}
92 select_mode
: bpy
.props
.EnumProperty(
94 ("CURSOR", "Time cursor", "Select all of the strips the time cursor overlaps"),
95 ("CONTEXT", "Smart", "Uses the selection if possible, else uses the other modes"),
97 name
="Selection mode",
98 description
="Cut only the strip under the mouse or all strips under the time cursor",
101 select_linked
: bpy
.props
.BoolProperty(
102 name
="Use linked time",
103 description
="In mouse or CONTEXT mode, always cut linked strips if this is checked",
106 gap_remove
: bpy
.props
.BoolProperty(
108 description
="When trimming the sequences, remove gaps automatically",
112 TABLET_TRIM_DISTANCE_THRESHOLD
= 6
113 # Don't rename these variables, we're using setattr to access them dynamically
114 trim_start
, channel_start
= 0, 0
115 trim_end
, channel_end
= 0, 0
123 use_audio_scrub
= False
125 event_shift_released
= True
126 event_alt_released
= True
128 event_ripple
, event_ripple_string
= "LEFT_ALT", "Alt"
129 event_select_mode
, event_select_mode_string
= "LEFT_SHIFT", "Shift"
130 event_change_side
= "O"
133 def poll(cls
, context
):
134 return context
.sequences
136 def invoke(self
, context
, event
):
137 if context
.screen
.is_animation_playing
:
138 bpy
.ops
.screen
.animation_cancel(restore_frame
=False)
140 self
.use_audio_scrub
= context
.scene
.use_audio_scrub
141 context
.scene
.use_audio_scrub
= False
143 self
.mouse_start_y
= event
.mouse_region_y
145 self
.trim_initialize(context
, event
)
146 self
.update_frame(context
, event
)
147 self
.draw_start(context
, event
)
148 self
.update_header_text(context
, event
)
150 context
.window_manager
.modal_handler_add(self
)
151 return {"RUNNING_MODAL"}
153 def modal(self
, context
, event
):
155 if event
.type == self
.event_change_side
and event
.value
== "PRESS":
156 self
.trim_side
= "start" if self
.trim_side
== "end" else "end"
158 if event
.type == self
.event_ripple
and event
.value
== "PRESS":
159 self
.gap_remove
= False if self
.gap_remove
else True
161 if event
.type == self
.event_select_mode
and event
.value
== "PRESS":
162 self
.select_mode
= "CONTEXT" if self
.select_mode
== "CURSOR" else "CURSOR"
164 if event
.type in {"ESC"}:
166 context
.scene
.use_audio_scrub
= self
.use_audio_scrub
170 if event
.type == "LEFTMOUSE" or (event
.type in ["RET", "T"] and event
.value
== "PRESS"):
171 self
.trim_apply(context
, event
)
173 context
.scene
.use_audio_scrub
= self
.use_audio_scrub
177 if event
.type == "MOUSEMOVE":
179 self
.update_frame(context
, event
)
180 self
.draw_start(context
, event
)
181 self
.update_header_text(context
, event
)
182 return {"PASS_THROUGH"}
184 return {"RUNNING_MODAL"}
186 def trim_initialize(self
, context
, event
):
187 frame
, self
.channel_start
= get_frame_and_channel(event
)
188 self
.trim_start
= find_snap_candidate(context
, frame
) if event
.ctrl
else frame
189 self
.trim_end
, self
.channel_end
= self
.trim_start
, self
.channel_start
190 self
.is_trimming
= True
192 def update_frame(self
, context
, event
):
193 frame
, channel
= get_frame_and_channel(event
)
194 frame_trim
= find_snap_candidate(context
, frame
) if event
.ctrl
else frame
195 setattr(self
, "channel_" + self
.trim_side
, channel
)
196 setattr(self
, "trim_" + self
.trim_side
, frame_trim
)
197 context
.scene
.frame_current
= getattr(self
, "trim_" + self
.trim_side
)
199 def draw_start(self
, context
, event
):
200 """Initializes the drawing handler, see draw()"""
201 to_trim
, to_delete
= self
.find_strips_to_trim(context
)
202 target_strips
= to_trim
+ to_delete
204 draw_args
= (self
, context
, self
.trim_start
, self
.trim_end
, target_strips
, self
.gap_remove
)
205 self
.draw_handler
= bpy
.types
.SpaceSequenceEditor
.draw_handler_add(
206 draw
, draw_args
, "WINDOW", "POST_PIXEL"
210 if self
.draw_handler
:
211 bpy
.types
.SpaceSequenceEditor
.draw_handler_remove(self
.draw_handler
, "WINDOW")
213 def update_header_text(self
, context
, event
):
215 "Trim from {} to {}".format(self
.trim_start
, self
.trim_end
)
217 + "({}) Gap Remove {}".format(
218 self
.event_ripple_string
, "ON" if self
.gap_remove
else "OFF"
221 + "({}) Mode: {}".format(self
.event_select_mode_string
, self
.select_mode
.capitalize())
223 + "(Ctrl) Snap: {}".format("ON" if event
.ctrl
else "OFF")
225 + "({}) Change Side".format(self
.event_change_side
)
227 context
.area
.header_text_set(text
)
229 def trim_apply(self
, context
, event
):
230 start_x
= context
.region
.view2d
.region_to_view(
231 x
=event
.mouse_region_x
, y
=event
.mouse_region_y
233 distance_to_start
= abs(event
.mouse_region_x
- start_x
)
236 self
.trim_start
== self
.trim_end
238 and distance_to_start
<= self
.TABLET_TRIM_DISTANCE_THRESHOLD
244 self
.is_trimming
= False
246 def cut(self
, context
):
247 to_cut
= self
.find_strips_to_cut(context
)
248 bpy
.ops
.sequencer
.select_all(action
="DESELECT")
253 bpy
.ops
.power_sequencer
.gap_remove()
255 frame_current
= context
.scene
.frame_current
256 context
.scene
.frame_current
= self
.trim_start
257 bpy
.ops
.sequencer
.split(frame
=context
.scene
.frame_current
, type="SOFT", side
="BOTH")
258 context
.scene
.frame_current
= frame_current
260 def find_strips_to_cut(self
, context
):
262 Returns a list of strips to cut, either the strip hovered by the mouse or all strips under
263 the time cursor, depending on the select_mode
265 to_cut
, overlapping_strips
= [], []
266 if self
.select_mode
== "CONTEXT":
267 overlapping_strips
= find_strips_mouse(
268 context
, self
.trim_start
, self
.channel_start
, self
.select_linked
270 to_cut
.extend(overlapping_strips
)
271 if self
.select_mode
== "CURSOR" or (
272 not overlapping_strips
and self
.select_mode
== "CONTEXT"
276 for s
in context
.sequences
277 if not s
.lock
and s
.frame_final_start
<= self
.trim_start
<= s
.frame_final_end
281 def trim(self
, context
):
282 to_trim
, to_delete
= self
.find_strips_to_trim(context
)
283 trim_strips(context
, self
.trim_start
, self
.trim_end
, to_trim
, to_delete
)
284 if (self
.gap_remove
and self
.select_mode
== "CURSOR") or (
285 self
.select_mode
== "CONTEXT" and to_trim
== [] and to_delete
== []
287 context
.scene
.frame_current
= min(self
.trim_start
, self
.trim_end
)
288 bpy
.ops
.power_sequencer
.gap_remove()
290 context
.scene
.frame_current
= self
.trim_end
292 def find_strips_to_trim(self
, context
):
294 Returns two lists of strips to trim and strips to delete
296 to_trim
, to_delete
= [], []
298 trim_start
= min(self
.trim_start
, self
.trim_end
)
299 trim_end
= max(self
.trim_start
, self
.trim_end
)
301 channel_min
= min(self
.channel_start
, self
.channel_end
)
302 channel_max
= max(self
.channel_start
, self
.channel_end
)
303 channels
= set(range(channel_min
, channel_max
+ 1))
305 for s
in context
.sequences
:
308 if self
.select_mode
== "CONTEXT" and s
.channel
not in channels
:
311 if trim_start
<= s
.frame_final_start
and trim_end
>= s
.frame_final_end
:
315 s
.frame_final_start
<= trim_start
<= s
.frame_final_end
316 or s
.frame_final_start
<= trim_end
<= s
.frame_final_end
320 return to_trim
, to_delete
323 def draw(self
, context
, frame_start
=-1, frame_end
=-1, target_strips
=[], draw_arrows
=False):
325 Draws the line and arrows that represent the trim
328 - start_x and end_x are Vector(), the start_x and end_x of the drawn trim line's vertices in region coordinates
330 view_to_region
= context
.region
.view2d
.view_to_region
332 # Detect and draw the gap's limits if not trimming any strips
333 if not target_strips
:
334 strip_before
, strip_after
= find_closest_surrounding_cuts(context
, frame_end
)
335 frame_start
= strip_before
.frame_final_end
336 frame_end
= strip_after
.frame_final_start
337 channels
= [strip_before
.channel
, strip_after
.channel
]
339 channels
= {s
.channel
for s
in target_strips
}
341 start_x
, start_y
= view_to_region(
342 min(frame_start
, frame_end
), math
.floor(min(channels
)), clip
=False
344 end_x
, end_y
= view_to_region(
345 max(frame_start
, frame_end
), math
.floor(max(channels
) + 1), clip
=False
348 start_x
= max(start_x
, context
.region
.x
)
349 start_y
= max(start_y
, context
.region
.y
)
351 end_x
= min(end_x
, context
.region
.x
+ context
.region
.width
)
352 end_y
= min(end_y
, context
.region
.y
+ context
.region
.height
)
355 color_line
= get_color_gizmo_primary(context
)
356 color_fill
= color_line
.copy()
359 rect_origin
= Vector((start_x
, start_y
))
360 rect_size
= Vector((end_x
- start_x
, abs(start_y
- end_y
)))
362 gpu
.state
.blend_set('ALPHA')
363 gpu
.state
.line_width_set(3.0)
364 draw_rectangle(SHADER
, rect_origin
, rect_size
, color_fill
)
366 draw_line(SHADER
, Vector((start_x
, start_y
)), Vector((start_x
, end_y
)), color_line
)
367 draw_line(SHADER
, Vector((end_x
, start_y
)), Vector((end_x
, end_y
)), color_line
)
371 if draw_arrows
and end_x
- start_x
> 2 * offset
+ radius
:
372 center_y
= (end_y
+ start_y
) / 2.0
373 center_1
= Vector((start_x
+ offset
, center_y
))
374 center_2
= Vector((end_x
- offset
, center_y
))
375 draw_triangle_equilateral(SHADER
, center_1
, radius
, color
=color_line
)
376 draw_triangle_equilateral(SHADER
, center_2
, radius
, math
.pi
, color
=color_line
)
378 gpu
.state
.line_width_set(1)
379 gpu
.state
.blend_set('NONE')
382 def get_frame_and_channel(event
):
384 Returns a tuple of (frame, channel)
386 frame_float
, channel_float
= bpy
.context
.region
.view2d
.region_to_view(
387 x
=event
.mouse_region_x
, y
=event
.mouse_region_y
389 return round(frame_float
), math
.floor(channel_float
)