# 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 from operator import attrgetter from .utils.doc import doc_name, doc_idname, doc_brief, doc_description class POWER_SEQUENCER_OT_swap_strips(bpy.types.Operator): """ *brief* Swaps the 2 strips between them Places the first strip in the channel and starting frame (frame_final_start) of the second strip, and places the second strip in the channel and starting frame (frame_final_end) of the first strip. If there is no space for the swap, it does nothing. """ doc = { "name": doc_name(__qualname__), "demo": "", "description": doc_description(__doc__), "shortcuts": [], "keymap": "Sequencer", } bl_idname = doc_idname(__qualname__) bl_label = doc["name"] bl_description = doc_brief(doc["description"]) bl_options = {"REGISTER", "UNDO"} direction: bpy.props.EnumProperty( name="Direction", description="The direction to find the closest strip", items=[ ("up", "Up", "The direction up from the selected strip"), ("down", "Down", "The direction down from the selected strip"), ], default="up", ) @classmethod def poll(cls, context): return context.selected_sequences def execute(self, context): if len(context.selected_sequences) != 2: return {"CANCELLED"} strip_1 = context.selected_sequences[0] if len(context.selected_sequences) == 1: strip_2 = self.find_closest_strip_vertical(context, strip_1, self.direction) else: strip_2 = context.selected_sequences[1] if not strip_2 or strip_1.lock or strip_2.lock: return {"CANCELLED"} # Swap a strip and one of its effects if hasattr(strip_1, "input_1") or hasattr(strip_2, "input_1"): if not self.are_linked(strip_1, strip_2): return {"CANCELLED"} self.swap_with_effect(strip_1, strip_2) return {"FINISHED"} s1_start, s1_channel = strip_1.frame_final_start, strip_1.channel s2_start, s2_channel = strip_2.frame_final_start, strip_2.channel self.move_to_end(strip_1, context) self.move_to_end(strip_2, context) s1_start_2 = strip_1.frame_final_start s2_start_2 = strip_2.frame_final_start group_1 = { s: s.channel for s in context.sequences if s.frame_final_start == s1_start_2 and s != strip_1 } group_2 = { s: s.channel for s in context.sequences if s.frame_final_start == s2_start_2 and s != strip_2 } strip_2.select = False bpy.ops.transform.seq_slide( value=(s2_start - strip_1.frame_final_start, s2_channel - strip_1.channel) ) bpy.ops.sequencer.select_all(action="DESELECT") strip_2.select = True bpy.ops.transform.seq_slide( value=(s1_start - strip_2.frame_final_start, s1_channel - strip_2.channel) ) if not self.fits( strip_1, group_1, s2_start, s1_channel, s2_channel, context ) or not self.fits(strip_2, group_2, s1_start, s2_channel, s1_channel, context): self.reconstruct(strip_1, s1_channel, group_1, context) self.reconstruct(strip_2, s2_channel, group_2, context) bpy.ops.sequencer.select_all(action="DESELECT") strip_1.select = True bpy.ops.transform.seq_slide( value=(s1_start - strip_1.frame_final_start, s1_channel - strip_1.channel) ) bpy.ops.sequencer.select_all(action="DESELECT") strip_2.select = True bpy.ops.transform.seq_slide( value=(s2_start - strip_2.frame_final_start, s2_channel - strip_2.channel) ) bpy.ops.sequencer.select_all(action="DESELECT") strip_1.select = True strip_2.select = True return {"CANCELLED"} bpy.ops.sequencer.select_all(action="DESELECT") strip_1.select = True strip_2.select = True return {"FINISHED"} def move_to_frame(self, strip, frame, context): """ Moves a strip based on its frame_final_start without changing its duration. Args: - strip: The strip to be moved. - frame: The frame, the frame_final_start of the strip will be placed at. """ selected_strips = context.selected_sequences bpy.ops.sequencer.select_all(action="DESELECT") strip.select = True bpy.ops.transform.seq_slide(value=(frame - strip.frame_final_start, 0)) bpy.ops.sequencer.select_all(action="DESELECT") for s in selected_strips: s.select = True def move_to_end(self, strip, context): """ Moves a strip to an empty slot at the end of the sequencer, different than its initial slot. Args: - strip: The strip to move. """ end_frame = max(context.sequences, key=attrgetter("frame_final_end")).frame_final_end self.move_to_frame(strip, end_frame, context) def fits(self, strip, group, frame, init_channel, target_channel, context): """ Checks if a swap has been successful or not. Args: - strip: The core strip of the swap. - group: The effect strips of the core strip. - frame: The starting frame of the target location. - init_channel: The initial channel of the strip, before the swap took place. - target_channel: The channel of the target location. Returns: True if the swap was successful, otherwise False. """ if strip.frame_final_start != frame or strip.channel != target_channel: return False offset = strip.channel - init_channel for s in group.keys(): if s.channel != group[s] + offset: return False return context.selected_sequences def reconstruct(self, strip, init_channel, group, context): """ Reconstructs a failed swap, based on a core strip. After its done, the core strip is placed at the end of the sequencer, in an empty slot. Args: - strip: The core strip of the swap. - init_channel: The initial channel of the core strip. - group: A dictionary with the effect strips of the core strip, and their target channels. """ self.move_to_end(strip, context) bpy.ops.sequencer.select_all(action="DESELECT") strip.select = True bpy.ops.transform.seq_slide(value=(0, init_channel - strip.channel)) for s in group.keys(): channel = group[s] for u in group.keys(): if u.channel == channel and u != s: u.channel += 1 s.channel = channel def find_closest_strip_vertical(self, context, strip, direction): """ Finds the closest strip to a given strip in a specific direction. Args: - strip: The base strip. Returns: The closest strip to the given strip, in the proper direction. If no strip is found, returns None. """ strips_in_range = ( s for s in context.sequences if strip.frame_final_start <= s.frame_final_start and s.frame_final_end <= strip.frame_final_end ) if direction == "up": strips_above = [s for s in strips_in_range if s.channel > strip.channel] if not strips_above: return return min(strips_above, key=attrgetter("channel")) elif direction == "down": strips_below = [s for s in strips_in_range if s.channel < strip.channel] if not strips_below: return return max(strips_below, key=attrgetter("channel")) def are_linked(self, strip_1, strip_2): return ( strip_1.frame_final_start == strip_2.frame_final_start and strip_1.frame_final_end == strip_2.frame_final_end ) def swap_with_effect(self, strip_1, strip_2): effect_strip = strip_1 if hasattr(strip_1, "input_1") else strip_2 other_strip = strip_1 if effect_strip != strip_1 else strip_2 effect_strip_channel = effect_strip.channel other_strip_channel = other_strip.channel effect_strip.channel -= 1 other_strip.channel = effect_strip_channel effect_strip.channel = other_strip_channel