Feb 28, 2015

BVP Sequencer Selection Addon

One of the activities performed most often when working with sequence strips in Blender's Video Sequence Editor is the selecting of strips or their endpoints, in order to move them into new positions in relation to each other. In such a workflow, robust selection tools are most welcome.

Unfortunately the selection commands in the VSE don't work quite as one might expect. Of most interest is the grouping of four commands in the Select menu: "Strips to the Left/Right" and "All strips to the Left/Right".

The first pair of commands is fine; they work on one channel only, and take the active strip as the starting point. Most useful for those times when a channel, such as a sound channel or graphic overlay, needs to be muted or locked.

The second pair of commands, however, is not additive, they will deselect whatever is already selected, if it doesn't fall within the side of the playhead that the command is selecting. There also seems to be a bug that causes the "Left" command to select to the right of the cursor (which might be fixed by the time you read this). In addition, it would be nice if the commands alerted the user when locked strips are encountered, to help prevent the user moving things out of alignment, as locked strips can't be moved.

Turi Scandurra and Carlos Padial mostly solved the problem when they created three "current frame aware" selection commands as part of their Extra Sequencer Actions addon. Even here, though, there's room for improvement. All three commands leave muted strips out of selection, and this is a mistake. Muted strips can be moved and usually should be, to stay in sync with the rest of the strips the user is moving. It is locked strips that require special handling. Further, the "left" selection mode doesn't select the strip that ends at the cursor. Finally, the "on" mode, selecting what is under the cursor, is likewise biased toward the right and could be designed to be more useful.

The BVP Sequencer Selection addon offers three similar commands in the hope that they offer the functionality most users would expect.

The addon was created and tested in Blender 2.73. (It might work in earlier versions, if the "blender": (2, 73, 0), value in the bl_info segment at the top of the script is adjusted.) All the commands have keyboard shortcuts assigned as detailed below, in the special addon keymap that is separate from the usual Blender keymap.

Here follows a description of the commands.


A common aspect of the three main commands is how they handle locked strips. If unselected locked strips are found in the set of strips the command is to select, the locked strips remain unselected and an alert in the Info bar states their number (not counting unselected locked strips outside the area to be selected). If selected locked strips are found, they remain selected and their number is also reported in the Info bar (including any found outside the area to be selected). If this paragraph is difficult to understand, don't worry about it, things will become clear as you use the commands. The handling of locked strips is programmed to be as safe and intuitive as possible. All the commands can be undone.

Add Select All Strips Left


Selects all strips with end frames up to the cursor position, except for unselected locked strips as explained above. Anything selected before the command is run will remain selected.
Keyboard shortcut: Ctrl-[

Add Select All Strips Right


Works the same as the Left command, but on all strips with start frames from the cursor position.
Keyboard shortcut: Ctrl-]

Add Select Current Strips


This works two ways. If the cursor is within a strip, but not on the start or end frame, the strip will be selected. If the cursor is at the end or start of the strip, the end or start handle will be selected. If the cursor is placed on a cut between two strips, the end handle of the left strip and the start handle of the right strip will be selected. This works across channels. The feature is most useful when the timing of a cut between strips needs to be adjusted.
Keyboard shortcut: Ctrl-.

A further two commands are offered to make working with locked and mute strips easier.

Select All Locked Strips

If locked strips exist in the sequencer, this command will deselect everything presently selected and select the locked strips. A notice in the Info bar will state the number of selections. If there are no locked strips, present selection will remain and an Info notice will likewise appear.
Keyboard shortcut: Shift-Ctrl-K

Select All Mute Strips

Same as the Locked version above, but for mute strips.
Keyboard shortcut: Shift-Ctrl-M

If you haven't done so already, it is a good idea to assign keyboard shortcuts to the existing selection commands in Blender. Here is a suggestion:
Left Handle - [
Right Handle - ]
Strips to the Left - Shift-Ctrl-[
Strips to the Right - Shift-Ctrl-]

You can download the addon as a tab-indented Python file from Google Drive:

BVP_Sequencer_Selection.py

If you prefer, you can copy and paste the space-indented text of the addon from here:

# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

# ##### BEGIN ACKNOWLEDGEMENT #####
#
#  A thank you note to Turi Scandurra and Carlos Padial, whose
#  work, 'Extra Sequencer Actions', precedes this present effort.
#  Although the code here is not based on theirs, because of the
#  similarities their past endeavour is acknowledged.
#
# ##### END ACKNOWLEDGEMENT #####

bl_info = {
    "name": "BVP Sequencer Selection",
    "author": "BVP, blendervisionpro.blogspot.com",
    "version": (0,1),
    "blender": (2, 73, 0),
    "location": "Sequencer > Select",
    "description": "Additional selection commands in the VSE",
    "category": "Sequencer"}

import bpy                                                # required for access to blender

class AddSelectAllStripsLeft(bpy.types.Operator):                            # type of class, Operator, Panel, etc.
    '''Additively selects all unlocked strips with end frames left of or at current position'''    # tooltip text
    bl_idname = "sequencer.bvp_addselect_allstripsleft"                        # ID name of the class that code will call, lowercase
    bl_label = "Add Select All Strips Left"                                # interface name of class
    bl_description = "Additively selects all unlocked strips with end frames left of or at current position"
    bl_options = {"REGISTER", "UNDO"}                                # enable register in info window, enable undo

    @classmethod
    def poll(cls, context):                                        # poll handles basic error checking
        if context.sequences:                                    # check that there are sequences
            return True
        return False

    def execute(self, context):
        lockNum = 0                                        # we report number of locked strips not selected
        lockSelNum = 0                                        # also report number of selected locked strips
        reportMessage = ""
        for strip in bpy.context.sequences:
            if strip.lock and strip.select:                            # count all selected locked strips, even outside left area
                lockSelNum += 1
            try:
                if strip.frame_final_end <= bpy.context.scene.frame_current:
                    if strip.lock and not strip.select:
                        lockNum += 1
                    else:
                        strip.select=True                    # selected locked strips will be part of this selection
            except:
                pass
        if lockNum == 1:
            reportMessage = "1 locked strip not selected. "
        elif lockNum > 1:
            reportMessage = str(lockNum) + " locked strips not selected. "
        if lockSelNum == 1:
            reportMessage = reportMessage + "1 locked strip remains selected."
        elif lockSelNum > 1:
            reportMessage = reportMessage + str(lockSelNum) + " locked strips remain selected."
        if reportMessage is not "":
            self.report({'INFO'},reportMessage)
        return {'FINISHED'}

class AddSelectAllStripsRight(bpy.types.Operator):                            # type of class, Operator, Panel, etc.
    '''Additively selects all unlocked strips with start frames right of or at current position'''    # tooltip text
    bl_idname = "sequencer.bvp_addselect_allstripsright"                        # ID name of the class that code will call, lowercase
    bl_label = "Add Select All Strips Right"                            # interface name of class
    bl_description = "Additively selects all unlocked strips with start frames right of or at current position"
    bl_options = {"REGISTER", "UNDO"}                                # enable register in info window, enable undo

    @classmethod
    def poll(cls, context):                                        # poll handles basic error checking
        if context.sequences:                                    # check that there are sequences
            return True
        return False

    def execute(self, context):
        lockNum = 0                                        # we report number of locked strips not selected
        lockSelNum = 0                                        # also report number of selected locked strips
        reportMessage = ""
        for strip in bpy.context.sequences:
            if strip.lock and strip.select:                            # count all selected locked strips, even outside right area
                lockSelNum += 1
            try:
                if strip.frame_final_start >= bpy.context.scene.frame_current:
                    if strip.lock and not strip.select:
                        lockNum += 1
                    else:
                        strip.select=True                    # selected locked strips will be part of this selection
            except:
                pass
        if lockNum == 1:
            reportMessage = "1 locked strip not selected. "
        elif lockNum > 1:
            reportMessage = str(lockNum) + " locked strips not selected. "
        if lockSelNum == 1:
            reportMessage = reportMessage + "1 locked strip remains selected."
        elif lockSelNum > 1:
            reportMessage = reportMessage + str(lockSelNum) + " locked strips remain selected."
        if reportMessage is not "":
            self.report({'INFO'},reportMessage)
        return {'FINISHED'}

class AddSelectCurrentStrips(bpy.types.Operator):                            # type of class, Operator, Panel, etc.
    '''Additively selects unlocked strips or left/right handles under the cursor'''            # tooltip text
    bl_idname = "sequencer.bvp_addselect_currentstrips"                        # ID name of the class that code will call, lowercase
    bl_label = "Add Select Current Strips"                                # interface name of class
    bl_description = "Additively selects unlocked strips or left/right handles under the cursor"
    bl_options = {"REGISTER", "UNDO"}                                # enable register in info window, enable undo

    @classmethod
    def poll(cls, context):                                        # poll handles basic error checking
        if context.sequences:                                    # check that there are sequences
            return True
        return False

    def execute(self, context):
        cFrame = bpy.context.scene.frame_current
        selStrips = []
        lockNum = 0                                        # we report number of locked strips not selected
        lockSelNum = 0                                        # also report number of selected locked strips
        reportMessage = ""
        for strip in bpy.context.sequences:
            if strip.lock and strip.select:                            # count all selected locked strips, even outside cursor area
                lockSelNum += 1
            try:
                if strip.frame_final_end >= cFrame:
                    if strip.frame_final_start <= cFrame:
                        if strip.lock and not strip.select:
                            lockNum += 1
                        else:
                            strip.select=True                # selected locked strips will be part of this selection
                            selStrips.append(strip)
            except:
                pass
        if selStrips != []:
            for strip in selStrips:
                try:
                    if strip.frame_final_end == cFrame:
                        strip.select_right_handle = True
                    elif strip.frame_final_start == cFrame:
                        strip.select_left_handle = True
                except:
                    pass
        if lockNum == 1:
            reportMessage = "1 locked strip not selected. "
        elif lockNum > 1:
            reportMessage = str(lockNum) + " locked strips not selected. "
        if lockSelNum == 1:
            reportMessage = reportMessage + "1 locked strip remains selected."
        elif lockSelNum > 1:
            reportMessage = reportMessage + str(lockSelNum) + " locked strips remain selected."
        if reportMessage is not "":
            self.report({'INFO'},reportMessage)
        return {'FINISHED'}

class SelectAllLockedStrips(bpy.types.Operator):                            # type of class, Operator, Panel, etc.
    '''Selects all locked strips'''                                    # tooltip text
    bl_idname = "sequencer.bvp_select_alllockedstrips"                        # ID name of the class that code will call, lowercase
    bl_label = "Select All Locked Strips"                                # interface name of class
    bl_description = "Selects all locked strips"
    bl_options = {"REGISTER", "UNDO"}                                # enable register in info window, enable undo

    @classmethod
    def poll(cls, context):                                        # poll handles basic error checking
        if context.sequences:                                    # check that there are sequences
            return True
        return False

    def execute(self, context):
        lockNum = 0
        lockedStrips = []
        for strip in bpy.context.sequences:                            # do nothing if there are no locked strips, preserving present selection
            if strip.lock:
                lockNum += 1
                lockedStrips.append(strip)
        try:
            if lockedStrips != []:
                bpy.ops.sequencer.select_all(action='DESELECT')
                for strip in lockedStrips:
                    strip.select = True
        except:
            pass
        if lockNum == 0:
            self.report({'INFO'},"No locked strips found.")
        elif lockNum == 1:
            self.report({'INFO'},"1 locked strip selected.")
        elif lockNum > 1:
            self.report({'INFO'},str(lockNum) + " locked strips selected.")
        return {'FINISHED'}

class SelectAllMuteStrips(bpy.types.Operator):                                # type of class, Operator, Panel, etc.
    '''Selects all mute strips'''                                    # tooltip text
    bl_idname = "sequencer.bvp_select_allmutestrips"                        # ID name of the class that code will call, lowercase
    bl_label = "Select All Mute Strips"                                # interface name of class
    bl_description = "Selects all mute strips"
    bl_options = {"REGISTER", "UNDO"}                                # enable register in info window, enable undo

    @classmethod
    def poll(cls, context):                                        # poll handles basic error checking
        if context.sequences:                                    # check that there are sequences
            return True
        return False

    def execute(self, context):
        muteNum = 0
        muteStrips = []
        for strip in bpy.context.sequences:                            # do nothing if there are no locked strips, preserving present selection
            if strip.mute:
                muteNum += 1
                muteStrips.append(strip)
        try:
            if muteStrips != []:
                bpy.ops.sequencer.select_all(action='DESELECT')
                for strip in muteStrips:
                    strip.select = True
        except:
            pass
        if muteNum == 0:
            self.report({'INFO'},"No mute strips found.")
        elif muteNum == 1:
            self.report({'INFO'},"1 mute strip selected.")
        elif muteNum > 1:
            self.report({'INFO'},str(muteNum) + " mute strips selected.")
        return {'FINISHED'}

def menu_draw_select(self, context):
    self.layout.operator(AddSelectAllStripsLeft.bl_idname, "Add Select All Strips Left")        # menu item for this class
    self.layout.operator(AddSelectAllStripsRight.bl_idname, "Add Select All Strips Right")
    self.layout.operator(AddSelectCurrentStrips.bl_idname, "Add Select Current Strips")
    self.layout.operator(SelectAllLockedStrips.bl_idname, "Select All Locked Strips")
    self.layout.operator(SelectAllMuteStrips.bl_idname, "Select All Mute Strips")

addon_keymaps = []                                            # store keymaps here to access after registration

def register():
    bpy.utils.register_module(__name__)                                # module registration registers all classes
    bpy.types.SEQUENCER_MT_select.append(menu_draw_select)                        # add module menu item to menu

    wm = bpy.context.window_manager                                    # handle the keymap
    km = wm.keyconfigs.addon.keymaps.new(name='Sequencer', space_type='SEQUENCE_EDITOR')
    kmi = km.keymap_items.new(AddSelectAllStripsLeft.bl_idname, 'LEFT_BRACKET', 'PRESS', ctrl=True)
    kmi = km.keymap_items.new(AddSelectAllStripsRight.bl_idname, 'RIGHT_BRACKET', 'PRESS', ctrl=True)
    kmi = km.keymap_items.new(AddSelectCurrentStrips.bl_idname, 'PERIOD', 'PRESS', ctrl=True)
    kmi = km.keymap_items.new(SelectAllLockedStrips.bl_idname, 'K', 'PRESS', shift=True, ctrl=True)
    kmi = km.keymap_items.new(SelectAllMuteStrips.bl_idname, 'M', 'PRESS', shift=True, ctrl=True)
    addon_keymaps.append((km, kmi))

def unregister():
    bpy.types.SEQUENCER_MT_select.remove(menu_draw_select)                        # unregister must mirror register
    bpy.utils.unregister_module(__name__)

    for km, kmi in addon_keymaps:                                    # handle the keymap
        km.keymap_items.remove(kmi)
    addon_keymaps.clear()

if __name__ == '__main__':                                        # for use of this script in Blender Text window
    register()
#    bpy.ops.object.myoperator_class()                                # test call


No comments:

Post a Comment

Note: Only a member of this blog may post a comment.