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’)