Skip to content

Quicker Text Editing in Blender (Part 2)

Giving it features

Context

I use Blender’s VSE (video sequence editor) to make highlight videos for YouTube, it’s a competent NLE (non-linear editor). However, making videos takes time in and of itself, and I feel there is room for speeding up working with text sequences, which I use for captioning what people are saying- resulting in a lot of text sequences!

That’s a few text sequences

Previously we looked at how we might speed up editing many text sequences in the Blender VSE, via programmatically setting properties- and we found we could!

Note: the following tip from the tips and tricks is very handy in loading/reloading a script while it’s under development:

filename = "/full/path/to/myscript.py"
exec(compile(open(filename).read(), filename, 'exec'))

Setting Specific Values with Operators

We had a sample script which would modify a text sequence’s / several text sequences’ properties: colour, location, duration, size. It would randomly set those, just to prove it worked. Now we can take it to the next stage, by setting properties to specific values.

Thanks to Blender_fun1 pointing me in the right direction, we can quickly and simply make subclasses of the SetText* operators. Using setting text to green as an example:

class SetTextColour(TextSequenceAction):
    """Set colour of text sequence[s]"""
    bl_idname = "sequencer.settextcolor"
    bl_label = "Set Text Colour"

    colour: bpy.props.FloatVectorProperty(
        name="textcolour",
        subtype='COLOR',
        description="Colour for text",
        size=4,
        min=0.0,
        max=1.0,
        default=(0.0, 0.0, 0.0, 1),  # black in RGBA
        )

    def execute(self, context):
        for strip in bpy.context.selected_editable_sequences:
            if strip.type == "TEXT":
                strip.color = self.colour

        return {'FINISHED'}


class SetTextColourGreen(SetTextColour):
    """Set it green"""
    bl_idname = "sequencer.settextcolourgreen"
    bl_label = "Set Text Colour Green"

    def __init__(self):
        self.colour = Colours.GREEN.value

This gives us a specific operator for making text green:

So we can create operators without much code- really just a single-line __init__(). But those are all manual definitions.

Dynamic Operators

I’d like to release this script as an addon, and so I’d like to give users the ability to set their own colour/location/duration/size presets, so hard-coding operators this way isn’t terribly useful. Perhaps if I was creating an addon with standardised set of text colours with names… anyway!

There is a way to create classes dynamically, which the Blender docs have under Dynamic Class Definition. It uses the type() function to create the class- it takes three arguments:

  • the class name
  • the base class[es]
  • attributes

Let’s borrow from their example and create some dynamic colour-setting classes:

DYNAMIC_CLASSES = []


def create_dynamic_classes():
    """Create classes from the colour enum"""
    for colour in Colours:
        idname = f"sequencer.set_text_colour_{colour.name.lower()}"
        label = f"Set Text Colour ({colour.name.title()})"

        operator_class = type(f"SetTextColour{colour.name.title()}",
                              (SetTextColour, ),
                              {"bl_idname": idname,
                               "bl_label": label,
                               "_colour": colour.value},
                              )

        DYNAMIC_CLASSES.append(operator_class)

This correctly creates the appropriate operators:

We can do the same thing for the Positions enum that we already have, and could do similar things for size and duration- depending if we want to create absolute (eg set the text size to 125) or relative operators (eg make the sequence duration longer by 15 frames).

Demo

A quick example of all operators:

Updated Script

"""quicker-text-editing.py -- text addon for Blender VSE"""
bl_info = {
    "name": "Quicker Text Editing for VSE",
    "author": "bertieb",
    "version": (0, 2),
    "blender": (3, 3, 0),
    "location": "Video Sequence Editor > Text Strip",
    "description": "Quicker editing of text strips: position, colour, size, duration",
    "warning": "",
    "doc_url": "",
    "category": "Sequencer",
}

from enum import Enum
import bpy
import random


class Colours(Enum):
    """some predefined colours - array of 4 floats (RGBA)"""
    GREEN = [0.03529411926865578, 0.6117647290229797, 0.03921568766236305, 1.0]
    PURPLE = [0.43800756335258484, 0.0, 0.6117647290229797, 1.0]
    BLUE = [0.12156863510608673, 0.41568630933761597, 0.6117647290229797, 1.0]


class Locations(Enum):
    """predefined locations - array of 2 floats (x,y)"""
    ONE = [0.5, 0.1]
    TWO = [0.5, 0.22]
    THREE = [0.5, 0.34]
    FOUR = [0.5, 0.45]


class Sizes(Enum):
    SMALL = 100
    MEDIUM = 125
    LARGE = 150


class Durations(Enum):
    SHORT = 45
    MEDIUM = 60
    LONG = 90


class TextSequenceAction(bpy.types.Operator):
    """Implements operations for quickly manipulating text sequences in VSE"""
    bl_idname = "sequencer.textsequenceaction"
    bl_label = "Text Sequence Action"

    def execute(self, context):
        return {"FINISHED"}

    @classmethod
    def poll(cls, context):
        """Ensure we're in the VSE with at least one sequence selected"""
        return (context.scene and context.scene.sequence_editor
                and context.selected_editable_sequences is not None)


class SetTextColour(TextSequenceAction):
    """Set colour of text sequence[s]"""
    bl_idname = "sequencer.settextcolor"
    bl_label = "Set Text Colour"

    colour: bpy.props.FloatVectorProperty(
        name="textcolour",
        subtype='COLOR',
        description="Colour for text",
        size=4,
        min=0.0,
        max=1.0,
        default=(0.0, 0.0, 0.0, 1),  # black in RGBA
        )

    _colour: None

    def __init__(self):
        if self._colour:
            self.colour = self._colour

    def execute(self, context):
        for strip in bpy.context.selected_editable_sequences:
            if strip.type == "TEXT":
                strip.color = self.colour

        return {'FINISHED'}


class SetTextLocation(TextSequenceAction):
    """Set location of text sequence[s]"""
    bl_idname = "sequencer.settextlocation"
    bl_label = "Set Text Location"

    location: bpy.props.FloatVectorProperty(
        name="textlocation",
        subtype='COORDINATES',
        description="Location for text",
        size=2,
        min=-inf,
        max=inf,
        default=(0.5, 0.5)  # (x,y)
        )

    _location: None

    def __init__(self):
        if self._location:
            self.location = self._location

    def execute(self, context):
        for strip in bpy.context.selected_editable_sequences:
            if strip.type == "TEXT":
                strip.location = self.location

        return {'FINISHED'}


class SetTextDuration(TextSequenceAction):
    """Set location of text sequence[s]"""
    bl_idname = "sequencer.settextduration"
    bl_label = "Set Text Duration"

    duration: bpy.props.IntProperty(
        name="textduration",
        subtype='TIME_ABSOLUTE',
        description="Duration for text",
        min=1,
        max=1048574,
        default=60  # frames
        )

    _duration: None

    def __init__(self):
        if self._duration:
            self.duration = self._duration

    def execute(self, context):
        for strip in bpy.context.selected_editable_sequences:
            if strip.type == "TEXT":
                strip.frame_final_duration = self.duration

        return {'FINISHED'}


class SetTextSize(TextSequenceAction):
    """Set size of text sequence[s]"""
    bl_idname = "sequencer.settextsize"
    bl_label = "Set Text Size"

    size: bpy.props.FloatProperty(
        name="textsize",
        subtype='UNSIGNED',
        description="Size for text",
        min=0.0,
        max=2000.0,
        default=100.0  # font size
        )

    _size: None

    def __init__(self):
        if self._size:
            self.size = self._size

    def execute(self, context):
        for strip in bpy.context.selected_editable_sequences:
            if strip.type == "TEXT":
                strip.font_size = self.size

        return {'FINISHED'}


REGISTER_CLASSES = [SetTextLocation, SetTextDuration, SetTextSize]
DYNAMIC_CLASSES = []


def create_dynamic_classes():
    """Create classes from the colour enum"""
    for colour in Colours:
        idname = f"sequencer.set_text_colour_{colour.name.lower()}"
        label = f"Set Text Colour ({colour.name.title()})"

        operator_class = type(f"SetTextColour{colour.name.title()}",
                              (SetTextColour, ),
                              {"bl_idname": idname,
                               "bl_label": label,
                               "_colour": colour.value},
                              )

        DYNAMIC_CLASSES.append(operator_class)

    for location in Locations:
        idname = f"sequencer.set_text_location_{location.name.lower()}"
        label = f"Set Text Location ({location.name.title()})"

        operator_class = type(f"SetTextLocation{location.name.title()}",
                              (SetTextLocation, ),
                              {"bl_idname": idname,
                               "bl_label": label,
                               "_location": location.value},
                              )

        DYNAMIC_CLASSES.append(operator_class)

    for duration in Durations:
        idname = f"sequencer.set_text_duration_{duration.name.lower()}"
        label = f"Set Text Duration ({duration.name.title()})"

        operator_class = type(f"SetTextDuration{duration.name.title()}",
                              (SetTextDuration, ),
                              {"bl_idname": idname,
                               "bl_label": label,
                               "_duration": duration.value},
                              )

        DYNAMIC_CLASSES.append(operator_class)

    for size in Sizes:
        idname = f"sequencer.set_text_size_{size.name.lower()}"
        label = f"Set Text Size ({size.name.title()})"

        operator_class = type(f"SetTextSize{size.name.title()}",
                              (SetTextSize, ),
                              {"bl_idname": idname,
                               "bl_label": label,
                               "_size": size.value},
                              )

        DYNAMIC_CLASSES.append(operator_class)


def register():
    create_dynamic_classes()

    for classname in DYNAMIC_CLASSES:
        bpy.utils.register_class(classname)


def unregister():
    for classname in DYNAMIC_CLASSES:
        bpy.utils.unregister_class(classname)


if __name__ == "__main__":
    register()

Next Steps

Genericise! Make it into something anyone can use- let the user define presets for colours/positions/sizes/durations and call those with hotkeys, leader keys, or whatever.

Tell us what's on your mind

Discover more from Rob's Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading