power_sequencer: use unix line endings
[blender-addons.git] / power_sequencer / operators / swap_strips.py
blob3eeb07c1a456105abb15a0deb1b656321031ac9b
2 # Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
4 # This file is part of Power Sequencer.
6 # Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
7 # GNU General Public License as published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
10 # Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
11 # without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License along with Power Sequencer. If
15 # not, see <https://www.gnu.org/licenses/>.
17 import bpy
18 from operator import attrgetter
20 from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
23 class POWER_SEQUENCER_OT_swap_strips(bpy.types.Operator):
24 """
25 *brief* Swaps the 2 strips between them
28 Places the first strip in the channel and starting frame (frame_final_start) of the second
29 strip, and places the second strip in the channel and starting frame (frame_final_end) of
30 the first strip. If there is no space for the swap, it does nothing.
31 """
33 doc = {
34 "name": doc_name(__qualname__),
35 "demo": "",
36 "description": doc_description(__doc__),
37 "shortcuts": [],
38 "keymap": "Sequencer",
40 bl_idname = doc_idname(__qualname__)
41 bl_label = doc["name"]
42 bl_description = doc_brief(doc["description"])
43 bl_options = {"REGISTER", "UNDO"}
45 direction: bpy.props.EnumProperty(
46 name="Direction",
47 description="The direction to find the closest strip",
48 items=[
49 ("up", "Up", "The direction up from the selected strip"),
50 ("down", "Down", "The direction down from the selected strip"),
52 default="up",
55 @classmethod
56 def poll(cls, context):
57 return context.selected_sequences
59 def execute(self, context):
60 strip_1 = context.selected_sequences[0]
61 if len(context.selected_sequences) == 1:
62 strip_2 = self.find_closest_strip_vertical(context, strip_1, self.direction)
63 else:
64 strip_2 = context.selected_sequences[1]
65 if not strip_2 or strip_1.lock or strip_2.lock:
66 return {"CANCELLED"}
68 # Swap a strip and one of its effects
69 if hasattr(strip_1, "input_1") or hasattr(strip_2, "input_1"):
70 if not self.are_linked(strip_1, strip_2):
71 return {"CANCELLED"}
72 self.swap_with_effect(strip_1, strip_2)
73 return {"FINISHED"}
75 s1_start, s1_channel = strip_1.frame_final_start, strip_1.channel
76 s2_start, s2_channel = strip_2.frame_final_start, strip_2.channel
78 self.move_to_end(strip_1, context)
79 self.move_to_end(strip_2, context)
81 s1_start_2 = strip_1.frame_final_start
82 s2_start_2 = strip_2.frame_final_start
84 group_1 = {
85 s: s.channel
86 for s in context.sequences
87 if s.frame_final_start == s1_start_2 and s != strip_1
89 group_2 = {
90 s: s.channel
91 for s in context.sequences
92 if s.frame_final_start == s2_start_2 and s != strip_2
95 strip_2.select = False
96 bpy.ops.transform.seq_slide(
97 value=(s2_start - strip_1.frame_final_start, s2_channel - strip_1.channel)
100 bpy.ops.sequencer.select_all(action="DESELECT")
101 strip_2.select = True
102 bpy.ops.transform.seq_slide(
103 value=(s1_start - strip_2.frame_final_start, s1_channel - strip_2.channel)
106 if not self.fits(
107 strip_1, group_1, s2_start, s1_channel, s2_channel, context
108 ) or not self.fits(strip_2, group_2, s1_start, s2_channel, s1_channel, context):
109 self.reconstruct(strip_1, s1_channel, group_1, context)
110 self.reconstruct(strip_2, s2_channel, group_2, context)
112 bpy.ops.sequencer.select_all(action="DESELECT")
113 strip_1.select = True
114 bpy.ops.transform.seq_slide(
115 value=(s1_start - strip_1.frame_final_start, s1_channel - strip_1.channel)
118 bpy.ops.sequencer.select_all(action="DESELECT")
119 strip_2.select = True
120 bpy.ops.transform.seq_slide(
121 value=(s2_start - strip_2.frame_final_start, s2_channel - strip_2.channel)
124 bpy.ops.sequencer.select_all(action="DESELECT")
125 strip_1.select = True
126 strip_2.select = True
127 return {"CANCELLED"}
129 bpy.ops.sequencer.select_all(action="DESELECT")
130 strip_1.select = True
131 strip_2.select = True
132 return {"FINISHED"}
134 def move_to_frame(self, strip, frame, context):
136 Moves a strip based on its frame_final_start without changing its
137 duration.
138 Args:
139 - strip: The strip to be moved.
140 - frame: The frame, the frame_final_start of the strip will be placed
143 selected_strips = context.selected_sequences
144 bpy.ops.sequencer.select_all(action="DESELECT")
145 strip.select = True
147 bpy.ops.transform.seq_slide(value=(frame - strip.frame_final_start, 0))
149 bpy.ops.sequencer.select_all(action="DESELECT")
150 for s in selected_strips:
151 s.select = True
153 def move_to_end(self, strip, context):
155 Moves a strip to an empty slot at the end of the sequencer, different
156 than its initial slot.
157 Args:
158 - strip: The strip to move.
160 end_frame = max(context.sequences, key=attrgetter("frame_final_end")).frame_final_end
161 self.move_to_frame(strip, end_frame, context)
163 def fits(self, strip, group, frame, init_channel, target_channel, context):
165 Checks if a swap has been successful or not.
166 Args:
167 - strip: The core strip of the swap.
168 - group: The effect strips of the core strip.
169 - frame: The starting frame of the target location.
170 - init_channel: The initial channel of the strip, before the swap took
171 place.
172 - target_channel: The channel of the target location.
173 Returns: True if the swap was successful, otherwise False.
175 if strip.frame_final_start != frame or strip.channel != target_channel:
176 return False
178 offset = strip.channel - init_channel
179 for s in group.keys():
180 if s.channel != group[s] + offset:
181 return False
183 return context.selected_sequences
185 def reconstruct(self, strip, init_channel, group, context):
187 Reconstructs a failed swap, based on a core strip. After its done, the
188 core strip is placed at the end of the sequencer, in an empty slot.
189 Args:
190 - strip: The core strip of the swap.
191 - init_channel: The initial channel of the core strip.
192 - group: A dictionary with the effect strips of the core strip, and
193 their target channels.
195 self.move_to_end(strip, context)
196 bpy.ops.sequencer.select_all(action="DESELECT")
197 strip.select = True
198 bpy.ops.transform.seq_slide(value=(0, init_channel - strip.channel))
200 for s in group.keys():
201 channel = group[s]
202 for u in group.keys():
203 if u.channel == channel and u != s:
204 u.channel += 1
205 s.channel = channel
207 def find_closest_strip_vertical(self, context, strip, direction):
209 Finds the closest strip to a given strip in a specific direction.
210 Args:
211 - strip: The base strip.
212 Returns: The closest strip to the given strip, in the proper direction.
213 If no strip is found, returns None.
215 strips_in_range = (
217 for s in context.sequences
218 if strip.frame_final_start <= s.frame_final_start
219 and s.frame_final_end <= strip.frame_final_end
221 if direction == "up":
222 strips_above = [s for s in strips_in_range if s.channel > strip.channel]
223 if not strips_above:
224 return
225 return min(strips_above, key=attrgetter("channel"))
226 elif direction == "down":
227 strips_below = [s for s in strips_in_range if s.channel < strip.channel]
228 if not strips_below:
229 return
230 return max(strips_below, key=attrgetter("channel"))
232 def are_linked(self, strip_1, strip_2):
233 return (
234 strip_1.frame_final_start == strip_2.frame_final_start
235 and strip_1.frame_final_end == strip_2.frame_final_end
238 def swap_with_effect(self, strip_1, strip_2):
239 effect_strip = strip_1 if hasattr(strip_1, "input_1") else strip_2
240 other_strip = strip_1 if effect_strip != strip_1 else strip_2
242 effect_strip_channel = effect_strip.channel
243 other_strip_channel = other_strip.channel
245 effect_strip.channel -= 1
246 other_strip.channel = effect_strip_channel
247 effect_strip.channel = other_strip_channel