Export_3ds: Improved distance cue node search
[blender-addons.git] / power_sequencer / operators / mouse_trim_modal.py
blob5978a874df139de3edd648bb2d9e9677c3e3df95
1 # SPDX-FileCopyrightText: 2016-2020 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
3 # SPDX-License-Identifier: GPL-3.0-or-later
5 import bpy
6 import gpu
7 import math
8 from mathutils import Vector
10 from .utils.functions import (
11 find_strips_mouse,
12 trim_strips,
13 find_snap_candidate,
14 find_closest_surrounding_cuts,
17 from .utils.draw import (
18 draw_line,
19 draw_rectangle,
20 draw_triangle_equilateral,
21 draw_arrow_head,
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):
32 """
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.
42 """
44 doc = {
45 "name": doc_name(__qualname__),
46 "demo": "https://i.imgur.com/wVvX4ex.gif",
47 "description": doc_description(__doc__),
48 "shortcuts": [
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(
93 items=[
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",
99 default="CONTEXT",
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",
104 default=False,
106 gap_remove: bpy.props.BoolProperty(
107 name="Remove gaps",
108 description="When trimming the sequences, remove gaps automatically",
109 default=True,
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
116 is_trimming = False
117 trim_side = "end"
119 mouse_start_y = -1.0
121 draw_handler = None
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"
132 @classmethod
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"}:
165 self.draw_stop()
166 context.scene.use_audio_scrub = self.use_audio_scrub
167 return {"FINISHED"}
169 # Start and end trim
170 if event.type == "LEFTMOUSE" or (event.type in ["RET", "T"] and event.value == "PRESS"):
171 self.trim_apply(context, event)
172 self.draw_stop()
173 context.scene.use_audio_scrub = self.use_audio_scrub
174 return {"FINISHED"}
176 # Update trim
177 if event.type == "MOUSEMOVE":
178 self.draw_stop()
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"
209 def draw_stop(self):
210 if self.draw_handler:
211 bpy.types.SpaceSequenceEditor.draw_handler_remove(self.draw_handler, "WINDOW")
213 def update_header_text(self, context, event):
214 text = (
215 "Trim from {} to {}".format(self.trim_start, self.trim_end)
216 + ", "
217 + "({}) Gap Remove {}".format(
218 self.event_ripple_string, "ON" if self.gap_remove else "OFF"
220 + ", "
221 + "({}) Mode: {}".format(self.event_select_mode_string, self.select_mode.capitalize())
222 + ", "
223 + "(Ctrl) Snap: {}".format("ON" if event.ctrl else "OFF")
224 + ", "
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
232 )[0]
233 distance_to_start = abs(event.mouse_region_x - start_x)
235 is_cutting = (
236 self.trim_start == self.trim_end
237 or event.is_tablet
238 and distance_to_start <= self.TABLET_TRIM_DISTANCE_THRESHOLD
240 if is_cutting:
241 self.cut(context)
242 else:
243 self.trim(context)
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")
249 for s in to_cut:
250 s.select = True
252 if len(to_cut) == 0:
253 bpy.ops.power_sequencer.gap_remove()
254 else:
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"
274 to_cut = [
276 for s in context.sequences
277 if not s.lock and s.frame_final_start <= self.trim_start <= s.frame_final_end
279 return to_cut
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()
289 else:
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:
306 if s.lock:
307 continue
308 if self.select_mode == "CONTEXT" and s.channel not in channels:
309 continue
311 if trim_start <= s.frame_final_start and trim_end >= s.frame_final_end:
312 to_delete.append(s)
313 continue
314 if (
315 s.frame_final_start <= trim_start <= s.frame_final_end
316 or s.frame_final_start <= trim_end <= s.frame_final_end
318 to_trim.append(s)
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
327 Params:
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]
338 else:
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)
354 # Draw
355 color_line = get_color_gizmo_primary(context)
356 color_fill = color_line.copy()
357 color_fill[-1] = 0.3
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)
365 # Vertical lines
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)
369 offset = 20.0
370 radius = 12.0
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)