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/>.
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
):
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.
34 "name": doc_name(__qualname__
),
36 "description": doc_description(__doc__
),
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(
47 description
="The direction to find the closest strip",
49 ("up", "Up", "The direction up from the selected strip"),
50 ("down", "Down", "The direction down from the selected strip"),
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
)
64 strip_2
= context
.selected_sequences
[1]
65 if not strip_2
or strip_1
.lock
or strip_2
.lock
:
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
):
72 self
.swap_with_effect(strip_1
, strip_2
)
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
86 for s
in context
.sequences
87 if s
.frame_final_start
== s1_start_2
and s
!= strip_1
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
)
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
129 bpy
.ops
.sequencer
.select_all(action
="DESELECT")
130 strip_1
.select
= True
131 strip_2
.select
= True
134 def move_to_frame(self
, strip
, frame
, context
):
136 Moves a strip based on its frame_final_start without changing its
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")
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
:
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.
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.
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
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
:
178 offset
= strip
.channel
- init_channel
179 for s
in group
.keys():
180 if s
.channel
!= group
[s
] + offset
:
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.
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")
198 bpy
.ops
.transform
.seq_slide(value
=(0, init_channel
- strip
.channel
))
200 for s
in group
.keys():
202 for u
in group
.keys():
203 if u
.channel
== channel
and u
!= s
:
207 def find_closest_strip_vertical(self
, context
, strip
, direction
):
209 Finds the closest strip to a given strip in a specific direction.
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.
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
]
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
]
230 return max(strips_below
, key
=attrgetter("channel"))
232 def are_linked(self
, strip_1
, strip_2
):
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