Skip to content

Quicker Text Editing in Blender (Part 4): User Preferences

  • by

“First, let’s take a look at the Preferences. What are your… preferences?”

Single panel from Penny Arcade's 2006 comic 'The Forbidden Fruit'. Close up of a hand on another hand holding an apple mouse. Speech bubble says:

"First, let's take a look at the Preferences. What are your... preferences?"
Image is from Penny Arcades 2006 comic ‘The Forbidden Fruit‘. It’s the first thing that came to my mind when I thought about the word ‘preferences’. My wish is that they take that as the compliment that I hope it is.

Context

I use Blender‘s VSE to make highlight videos, which involves editing a lot of text sequences for captions. This is a slightly tedious process, and so I’ve been writing an addon to make that quicker, easier and more streamlined and documenting the process here for any blender users who might also want to speed up an aspect of their workflow. Last time we had a script with hotkey bindings:

But now we want to make this useful to other people than me, which involves letting them choose their own colours.

Preferences

As with many other things, Blender has API access for addon preferences. The docs at that link give an example, but there’s a much better overview of addon preferences written by Nikita over at Blender3D. I won’t duplicate it in its entirety here, but will pull out relevant parts as it applies to this addon.

What do we need at a minimum?

This addon is about manipulating text sequences quicker and more efficiently. The most common action is setting the text, but it’s hard to get around that (as I mentioned in a previous part, throwing it at Voice to Text transcription service doesn’t work)! I commonly set colour (for speaker), position (for overlapping sentences), size (to fit a sentence in a line), and duration. All of those should be able to have presets which are user-defined. For each, the user should be able to define a hotkey to run the operator.

Now there’s more we can think about- defaults for each category, maximum number of presets definable, ability to save/export presets to file are a few off the top of my head. We’ll get to those.

A User-Friendly Panel

Using example templates from elsewhere, we can start sketching out panels. One for colours preset would at minimum need to let the user specify the exact colour (likely with a picker), and a name for it.

The first pass produces something very simple:

We can do better than that for the colour name, but at least we get a colour picker widget!

What if we tried an align‘d row() ?

Ah, hmm. Not exactly an improvement.

Blender’s Key Mapping Preferences

Unfortunately there’s no good “Blender UI Panel Layout 101” tutorial, so I checked Blender’s own code for doing key mappings. The draw_kmi() method seems to be where the action is:

def draw_kmi(display_keymaps, kc, km, kmi, layout, level):
    map_type = kmi.map_type

    col = _indented_layout(layout, level)

    if kmi.show_expanded:
        col = col.column(align=True)
        box = col.box()
    else:
        box = col.column()

    split = box.split()

    # header bar
    row = split.row(align=True)
    row.prop(kmi, "show_expanded", text="", emboss=False)  # (1)
    row.prop(kmi, "active", text="", emboss=False)  # (2)

    if km.is_modal:
        row.separator()
        row.prop(kmi, "propvalue", text="")
    else:
        row.label(text=kmi.name)  # (3)

    row = split.row()
    row.prop(kmi, "map_type", text="")   # (4)
    if map_type == 'KEYBOARD':
        row.prop(kmi, "type", text="", full_event=True)  # (5)
    elif map_type == 'MOUSE':
        row.prop(kmi, "type", text="", full_event=True)
    elif map_type == 'NDOF':
        row.prop(kmi, "type", text="", full_event=True)
    elif map_type == 'TWEAK':
        subrow = row.row()
        subrow.prop(kmi, "type", text="")
        subrow.prop(kmi, "value", text="")
    elif map_type == 'TIMER':
        row.prop(kmi, "type", text="")
    else:
        row.label()

    if (not kmi.is_user_defined) and kmi.is_user_modified:
        row.operator("preferences.keyitem_restore", text="", icon='BACK').item_id = kmi.id
    else:
        row.operator(
            "preferences.keyitem_remove",
            text="",
            # Abusing the tracking icon, but it works pretty well here.
            icon=('TRACKING_CLEAR_BACKWARDS' if kmi.is_user_defined else 'X')  # (6)
        ).item_id = kmi.id

(annotated with numbers to help compare with image below)

Clicking on the hotkey binding box (labelled ‘5’) listens for keys:

That panel prop seems to map to “type”, as in the KeyMapItem type, which seems to wait for events in the Event Types enum, which… makes me question what I’m doing here.

Wait, KeyMapItem ? We had code before for creating keymap items, maybe we could use that..?

def draw(self, context):
        layout = self.layout
        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')

        for kmi in km.keymap_items:
            row = layout.row(heading="Colour preset:")
            row.prop(self, "colour_name")
            row.prop(kmi.properties, "colour")
            row.prop(kmi, "type", text="", full_event=True)

gives us:

Ah, oh. The dynamic classes are based on the base class, and the base class registers keybindings, so with each subclass it re-registers those key bindings… Let’s sort that.

Better!

But we’re starting to run into issues- different implementations from different versions of this script are starting to step on each others’ toes. We should start clean.

List of Properties

This part of development it takes me a bit of time to wrap my head around- figuring out what structures / API methods wrap up the items we want to have. Dang, even expressing the concept is tricky. Let me explain a bit…

Let’s take one property: colour. We want a variable-length† list of presets so the user can define as many colours as they like. A quick search reveals PropertyGroup and CollectionProperty, as well as a BSE answer on how to use it. a CollectionProperty seems to be a Blender-flavoured list (this SO answer seems to back that up), and a PropertyGroup seems to be for related properties, so it would seem we want a CollectionProperty-list to contain our PropertyGroup-properties.


†: I had a look at how another addon, bl-leader-key, does it, but they didn’t have much luck either:

# Haven't found good way for dynamically creating settings in user-prefs.
# So on start create fixed number of properties.
BINDINGS_MAX = 25

We need to set up a new things:

  • a class for our PropertyGroup
  • an operator to add an item to it
  • a CollectionProperty property to our preferences
class NewQTEPreset(bpy.types.Operator):
    """Create a new QTE preset"""
    bl_idname = "qte.newpreset"
    bl_label = "Add a new preset"

    def execute(self, context):
        addonprefs = context.preferences.addons[__name__].preferences
        newpreset = addonprefs.presets.add()
        self.report({'INFO'}, "f{newpreset}")

        return {'FINISHED'}


class ColourPresets(bpy.types.PropertyGroup):
    colour_name: bpy.props.StringProperty(
        name="Name",
        description="Colour preset",
        default="Red"
    )

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


class QTEPreferences(bpy.types.AddonPreferences):
    bl_idname = __name__

    colour_name: bpy.props.StringProperty(
        name="Name",
        description="Colour preset",
        default="Red"
    )

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

    presets: bpy.props.CollectionProperty(type=ColourPresets)

    def draw(self, context):
        layout = self.layout

        for preset in self.presets:
            row = layout.row()
            row.prop(preset, "colour_name")
            row.prop(preset, "colour")

        layout.operator("qte.newpreset", icon='ADD')

which gets us closer to where we want to go:

That’s great, but we also need to add a key mapping (initially blank), which there’s no specific property for, so we can perhaps use a PointerProperty to ‘point’ to the KeyMapItem?

Unfortunately, no! The script fails registration, complaining:

TypeError: PointerProperty(...) expected an RNA type derived from ID or ID Property Group
(...)
ValueError: bpy_struct "ColourPresets" registration error: 'keymapitem' PointerProperty could not register (see previous error)

Normally when figuring out what you’re doing, whether in the context of a an unfamiliar API, or a new technique, or what have you you run into situations like this. It’s a new thing, you don’t have working knowledge of it, and you’re not sure where to go. So it’s worth taking stock: I am trying to figure out how to get a key mapping box (labelled 5 in the image above) into my preferences panel. But why? Can we zoom out a little? I want to do this because I want to have a mapping to quickly apply each preset (fair enough), and I think that a logical place to define the key mapping is with the preset definition itself. Now that might be a poor assumption- best practices for Blender addons may involve letting the user define the hotkeys through Blender’s own key bindings UI.

In light of that, I’ve asked a question on blenderartists to see how others would approach this- both the immediate issue of the PointerProperty-vis-a-vis-KeyMapItem property, and the broader ‘how to approach keybindings in a user addon’ question.

It seems a reasonable place to ask for assistance, especially as there doesn’t seem to be an obvious guide for such things. There is a document, tantalisingly titled ‘Human Interface Guidelines (Layouts)‘, but it’s incomplete.

Once I figure out the right approach (or receive an answer) we can move on with part 5- tying together user presets and hotkeys!

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