Passing an argument to an operator

Background / context: I am writing a simple addon for quicker manipulation of text sequences, to more easily do things like set colour, location, size, and duration.

Given this background and the API documentation, I have found the object (TextSequence) properties that I need to modify, and created a script with operators that modify the property (for testing purposes: randomly). The script in its present, testing form is appended to the post.

(Side note: the script currently uses global enums for the preset colours/positions, I think the better practice is to move those into PropertyGroups)

I am familiar with python, and I am fine with calling functions/methods with arguments; what Iā€™m not sure of is how to do the same thing in the Blender context!

Example:

Say a text sequence is selected, and Iā€™d like to change the text colour to green using the SetTextColour operator Iā€™ve implemented, by pressing a hotkey (eg ctrl+shift+g) or a leader key sequence (eg <leader> t c g). How can I pass one of my preset colours to the operator? ie how can I do SetTextColour(Colours.GREEN) ?

Obviously there are workarounds- I can create operators for each variant: SetTextColourGreen, SetTextColourRed; or I could implement operators which cycle through the presets (eg SetTextColourNextPreset / SetTextColourPreviousPreset). This feels a bit clumsy!

I looked through the quickstart / overview but didnā€™t find what I was looking for. Searches took me to a QA on BSE, but even there the custom argument is set in the execute() method.

I feel like Iā€™m missing something obvious, so appreciate any guidance :slight_smile: If what Iā€™ve written isnā€™t clear I am happy to clarify. Thanks in advance!


Current test script

(as noted above, this randomly modifies the properties- this is not the final behaviour!!)

"""quicker-text-editing.py -- text addon for Blender VSE"""
bl_info = {
    "name": "Quicker Text Editing for VSE",
    "author": "bertieb",
    "version": (0, 1),
    "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 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"

    def execute(self, context):
        for strip in bpy.context.selected_editable_sequences:
            if strip.type == "TEXT":
                strip.color = random.choice(list(Colours)).value

        return {'FINISHED'}


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

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

        return {'FINISHED'}


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

    def execute(self, context):
        for strip in bpy.context.selected_editable_sequences:
            if strip.type == "TEXT":
                strip.frame_final_duration += random.randint(-10, 10)

        return {'FINISHED'}


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

    def execute(self, context):
        for strip in bpy.context.selected_editable_sequences:
            if strip.type == "TEXT":
                strip.font_size += random.choice([-15, -10, -5, 5, 10, 15])

        return {'FINISHED'}


REGISTER_CLASSES = [SetTextColour, SetTextLocation, SetTextDuration,
                    SetTextSize]


def register():
    for classname in REGISTER_CLASSES:
        bpy.utils.register_class(classname)


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


if __name__ == "__main__":
    register()

1 Like

checkout this answer from bafinger: Custom operator with arguments!

1 Like

Thanks for that- I have that open in one of my many tabs >_<

Iā€™ll have another look!

1 Like

so if you use batfingers answer in your code, it might look like this:

"""quicker-text-editing.py -- text addon for Blender VSE"""
bl_info = {
    "name": "Quicker Text Editing for VSE",
    "author": "bertieb",
    "version": (0, 1),
    "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 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"
    
    color : bpy.props.FloatVectorProperty(
                                     name = "myColor",
                                     subtype = "COLOR",
                                     size = 4,
                                     min = 0.0,
                                     max = 1.0,
                                     default = (1.0,0.0,0.0,1.0)
                                     )

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

        return {'FINISHED'}
    
class SetGreenTextColour(SetTextColour):
    """Set colour of text sequence[s]"""
    bl_idname = "sequencer.setgreentextcolor"
    bl_label = "Set Green Text Colour"
    
 
    def __init__(self):
        self.color = (0,1,0,1)
        


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

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

        return {'FINISHED'}


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

    def execute(self, context):
        for strip in bpy.context.selected_editable_sequences:
            if strip.type == "TEXT":
                strip.frame_final_duration += random.randint(-10, 10)

        return {'FINISHED'}


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

    def execute(self, context):
        for strip in bpy.context.selected_editable_sequences:
            if strip.type == "TEXT":
                strip.font_size += random.choice([-15, -10, -5, 5, 10, 15])

        return {'FINISHED'}


REGISTER_CLASSES = [SetTextColour, SetGreenTextColour, SetTextLocation, SetTextDuration,
                    SetTextSize]


def register():
    for classname in REGISTER_CLASSES:
        bpy.utils.register_class(classname)


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


if __name__ == "__main__":
    register()

result:

set green textcolor for strip

1 Like

Thanks for the example code!

Unless Iā€™m missing something, that seems to take the pattern of one of the workaround I mentioned- creating an operator for each colour? Itā€™s a good example though, it shows that not much is needed (just the colour definition in __init__) to make it work :slight_smile:

Thatā€™s fine for my own purposes, but I had hoped to release the addon where a user can define their own presets. I guess thereā€™s still ways to do that- instead of SetGreenTextColour, SetRedTextColour (etc) I could do it numerically- SetTextColourPreset1, SetTextColourPreset2 (<continuing for as many as are defined>)

I just had hoped there was a ā€˜neaterā€™ way of doing it ! >_<

Forgive the self reply, but I found amongst my tabs the reference I was looking for on this:

Dynamic Class Definition

In some cases the specifier for data may not be in Blender, for example a external render engines shader definitions, and it may be useful to define them as types and remove them on the fly.

for i in range(10):
   idname = "object.operator_%d" % i

   def func(self, context):
       print("Hello World", self.bl_idname)
       return {'FINISHED'}

   opclass = type("DynOp%d" % i,
                  (bpy.types.Operator, ),
                  {"bl_idname": idname, "bl_label": "Test", "execute": func},
                  )
   bpy.utils.register_class(opclass)

if you wanna make the user have own presets, i would add a uilist or panel so the user can add/remove presets. And in the ā€œoneā€ operator you have you just have to read out your ā€œcurrentā€ preset. You could save the ā€œcurrentā€ presets and the custom presets of the users e.g. in custom properties. To make a single operator for each presetā€¦i think thatā€™s a bit too much, isnā€™t it? But yeah, of course you could do that. And you could even let the user add custom keystrokes for each color ā†’ so you ā€œjustā€ have to give your ONE operator the pressed key ā†’ your operator reads the color which should be chosen by the keys pressed (which are stored in a custom property list) and changes the strip accordingly. Thatā€™s the way i would goā€¦but of course ā€¦there are always many roads to the same goal :wink:

1 Like

That would be ideal, if I can figure that bit out! But yeah, user-facing stuff comes next- I want to make the current stuff work first! :stuck_out_tongue_winking_eye:

Iā€™ve got the dynamic classes working, from there I can decide which route I want to go down.

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)

lol yup! A single operator which reads a keystroke and sets the [colour/position/size/duration] feels neater, but I could just as easily create however many operators/classes dynamically, register them and let the user set hotkeys in Blender preferences like any other built-in operatorā€¦

1 Like

niiiiiceā€¦now please with a rounded box background on a frosted glass pane! :smiley: (as Apple does it with itā€™s icons/text)

1 Like

:smile:

Iā€™ll add it as a stretch goal to the Kickstarter :stuck_out_tongue_winking_eye:

I still need to figure out undo/redo- it seems to undo all operations if I use them in a sequence, but Iā€™m not sure if thatā€™s down to how I wrote the operators or just how blender squashes groups of related edits.

To pass an argument to a Blender operator, it must have a property definition (annotation).

import bpy
class BLAH_OT_operator(bpy.types.Operator):
    bl_idname = "blah.operator"
    bl_label = "Operator"
    string: bpy.props.StringProperty(default="Default string")
    color: bpy.props.FloatVectorProperty(size=4, default=(0, 1, 0, 1))

    def execute(self, context):
        print(self.string, tuple(self.color))
        return {'FINISHED'}

if __name__ == "__main__":
    bpy.utils.register_class(BLAH_OT_operator)
    bpy.ops.blah.operator(string="Hello World", color=(0, 0, 0, 0))

For colors, youā€™d pass Enum.value, since the enum itself is not a sequence.

1 Like

Good tip on Enum.ITEM.value, that one caught me out for sure! And thanks for the notes on property definitions/annotations.

I guess my question was more about how do I pass an argument from Blender, as opposed to in code. The linked answer Blender_fun1 mentioned showā€™s how to do it both from code and in a panel layout; but Iā€™d like to avoid the panel approach if possible! My goal is to speed up working with text sequences, so a keyboard-only approach would be great.

More concretely, this binding does not work:

Nor does this:

This one does:

Itā€™s simple enough to generate the operator classes dynamically (the third example above uses a dynamically-generated class in fact), so I can use that approach.

The only downsides are that it pollutes the namespace a bit; and thereā€™s probably things I havenā€™t considered yet, like registering/unregistering the classes when the user-defined colours/locations/sizes/durations change instead of when the script/addon is loaded.

Sorry, Iā€™m still confused.
Iā€™ve looked at the link and it explains passing arguments to operator calls and layout operators.

Are they still not what youā€™re after?

Is this about setting up hotkeys with predefined arguments? (then you need property definitions).

Not sure there are any other means to ā€œpass argumentā€ to an operator.

1 Like

No worries, Iā€™ve probably not explained it terribly well! My goal is to speed up working with text sequences; I use them as captions in videos, and I use different colours to differentiate speakers, different positions for multi-line text, and change size and duration too. I imagine other folks who use text captions do these things frequently too!

So Iā€™d like to set up quicker ways of changing those, using presets. Iā€™ve got the operators set up, I just need an interface. Now, thereā€™s a few ways of doing this:

  • use a panel to set a property before calling a single operator as described in the link
  • create an operator for each preset and let the user bind hotkeys for each (I can do this with dynamic classes/operators presently)
  • do something else like a modal operator- invoke the operator with a key, then read a keypress to determine the preset

Iā€™m not sure which of these provides the most ideal combination of user experience and ā€˜cleanā€™ code; and there may be other approaches too that I havenā€™t considered.

I was hoping in my initial question that there was a way to pass an argument (colour, size, location, duration) to an operator when binding a hotkey; but if thereā€™s not I have other approaches I can use :slight_smile:

Thanks. That does provide more context.

To answer your original question regarding hotkeys; when you create a hotkey, the object returned is whatā€™s called an OperatorProperties instance. You can set predefined arguments on these just like you would when defining button operators in layouts.

km = ...  # Sequencer keymap
kmi = km.keymap_items.new("sequencer.settextcolor", "G", "PRESS", ctrl=True, shift=True)
#                         bl_idname                 key  type     modifiers

kmi.properties.color = Colours.GREEN.value

When the hotkey is pressed. It will be as if the operator was called with bpy.ops.sequencer.settextcolor(color=Colours.GREEN.value).

An example that sets up ctrl+shift+g and ctrl+shift+b to set a text stripā€™s color to green/blue respectively.
The keymap is added and removed automatically when the operator is registered/unregistered.

Granted, the boilerplate is arguably more lines of code than the operator itself :joy:

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

    _keymaps = []
    color: bpy.props.FloatVectorProperty(size=4, default=(1, 1, 1, 1))

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

        return {'FINISHED'}

    @classmethod
    def register(cls):
        # Boilerplate
        wm = bpy.context.window_manager
        km = wm.keyconfigs.addon.keymaps.get("Sequencer")
        if km is None:
            km = wm.keyconfigs.addon.keymaps.new("Sequencer", space_type='SEQUENCE_EDITOR')

        # 'ctrl + shift + g' -> SetTextColour(color=Colours.GREEN.value)
        kmi = km.keymap_items.new(cls.bl_idname, 'G', 'PRESS', ctrl=True, shift=True)
        kmi.properties.color = Colours.GREEN.value
        cls._keymaps.append((km, kmi))

        # 'ctrl + shift + b' -> SetTextColour(color=Colours.BLUE.value)
        kmi = km.keymap_items.new(cls.bl_idname, 'B', 'PRESS', ctrl=True, shift=True)
        kmi.properties.color = Colours.BLUE.value
        cls._keymaps.append((km, kmi))

    @classmethod
    def unregister(cls):
        # Remove keymaps when operator is unregistered
        for km, kmi in cls._keymaps:
            km.keymap_items.remove(kmi)
        cls._keymaps.clear()
1 Like

Ah, interesting! It works well:

I wasnā€™t considering creating my own hotkeys, as Iā€™m not sure that addons defining hotkeys themselves is a great user experienceā€¦

ā€¦BUT itā€™s possible to set up user configuration for them as well Iā€™m sure :smiley: So I can let the user set up presets for various properties, let them define an appropriate hotkey / keycombo; then dynamically create the classes for the keymaps and the operators. Quite probably overengineered for something with an audience of one (me), but you never know.

At least thatā€™s the plan for the next stage- Iā€™m going to test hardcoded keymaps based on your examples first!

Cheers!

The thought occurs that if Iā€™m dynamically generating classes based on user definitions anyway, thereā€™s no real advantage to passing the property (eg colour) to a more generic version of the operator (eg set_text_colour); as opposed to just mapping their chosen binding to the specific operator for that property preset (eg set_text_colour_green).

Still though, itā€™s a useful technique to know!

To wrap up this thread and provide a tl;dr for those who come looking later: you canā€™t exactly call an operator with an argument in the same way you can with a function or method.

But you can pass options to operators with properties. This can be helpful if you want to call an operator with a limited preset set of options.

Setting a property from a panel

See @batFINGERā€™s post here, with code:

Setting a property with a keybinding

See @iceytheā€™s post in this thread, also with code:

Dynamically creating an operator for each ā€˜argumentā€™

I include this as another potential option: if you are going to have a small number of presets, you can dynamically create operators for each preset. I give an example in this thread:

(post #8, see also API reference on ā€˜Dynamic Class Definitionā€™)