5
$\begingroup$

I want a panel in the bottom left to accept "create options"; like what you get when adding an mesh object.

I've got a panel but no combination of bl_space_type bl_region_type and bl_context I try seems to work


I found the answer to this. Panels down there are associated with the current operation. https://b3d.interplanety.org/en/calling-functions-by-pressing-buttons-in-blender-custom-ui/

Though I can't figure out the relationship between the operation and the panel e.g. how Blender knows which panel to show for the operation. I also haven't found a way from python to call the operator and get it to show the UI e.g. if I do bpy.ops.test.test_op() it runs my operator but never shows the UI; I want to show the UI, like it does when run using F3

$\endgroup$
1
  • 1
    $\begingroup$ Hello, I'd suggest to study the template under Templates>Python>Operator Mesh Add $\endgroup$
    – Gorgious
    Commented Aug 22, 2020 at 22:04

2 Answers 2

8
$\begingroup$

This does what I wanted. There are 5 things needed to make it work:

  1. Your operator needs bl_options = {'REGISTER', 'UNDO'}, without this the operator properties UI doesn't even show when running via the search menu (F3)
  2. In invoke you need to register yourself as modal context.window_manager.modal_handler_add(self)
  3. invoke needs to return {'RUNNING_MODAL'}
  4. You need to implement modal though it can just return {'FINISHED'}
  5. When invoking from python you need to override the context

If you do all that, your able to run it from Python and get the operator properties UI; exactly as you get when running the operator via the UI

import bpy


class SimpleMouseOperator(bpy.types.Operator):
    """ This operator shows the mouse location,
        this string is used for the tooltip and API docs
    """
    bl_idname = "wm.mouse_position"
    bl_label = "Invoke Mouse Operator"
    bl_options = {'REGISTER', 'UNDO'} # this is needed so we actally get a UI

    x: bpy.props.IntProperty()
    y: bpy.props.IntProperty()

    def execute(self, context):
        # rather than printing, use the report function,
        # this way the message appears in the header,
        self.report({'INFO'}, "Mouse coords are %d %d" % (self.x, self.y))
        return {'FINISHED'}

    def invoke(self, context, event):
        self.x = event.mouse_x
        self.y = event.mouse_y
        # add it into the WM list and return modal
        context.window_manager.modal_handler_add(self)
        return {'RUNNING_MODAL'}
    
    def modal(self, context, event):
        # this doesn't need to do anything
        return {'FINISHED'}


bpy.utils.register_class(SimpleMouseOperator)

for window in bpy.context.window_manager.windows:
    screen = window.screen

    for area in screen.areas:
        if area.type == 'VIEW_3D':
            override = {'window': window, 'screen': screen, 'area': area}
            # pass the override - otherwise it doesn't work
            bpy.ops.wm.mouse_position(override, 'INVOKE_DEFAULT')
            break

The details of why this works are interesting, and as I now think I fully understand them, I'll provide some background.

The panel I'm trying to get activated isn't really a panel at all; it's the "last action"/"redo" UI. Blender automatically displays this when a user run operation finishes. So when you run an "add mesh cube" command from the UI menu, that actually completes immediately, and the dialog you see (which lets you change parameters) is the "redo" UI. When you run a modal command; like "grab"; the operator starts running when you press "G" (invoke returns 'RUNNING_MODAL') and is running the modal method until you click out; when it returns 'FINISHED' and the "redo" panel is shown.

This should explain why in the example above we return 'FINISHED' straight away from modal. We don't want actual modal operation, and we want the panel - so we need it to finish straight away. But why do modal at all? read on ...

Looking through the C code the "redo" UI is triggered in wm_operator_finished. For the UI to show, the operator being finished has to pass various checks:

  • repeat == 0; to exclude various type of running
  • wm->op_undo_depth == 0; used to catch nested operator execution
  • must have 'REGISTER' and 'UNDO'

These checks are spread between wm_operator_register_check and WM_operator_last_redo.

Knowing this it should be clear why we need to set the bl_options. Now, I think the reason you don't need any of this magic when run via the UI (a button/menu or search) is the wm->op_undo_depth == 0 check. This check appears to be there to catch nested operator calls i.e. when the UI calls one operator and that calls another. Thus makes sense as if you called bpy.ops.mesh.primitive_circle_add via the UI, you want the UI to show that operators redo, not one which it called internally e.g. bpy.ops.object.add.

So why does making it modal fix that; we'll it's a hack. When an operator is modal it takes a different code path (using wm_handler_operator_call) which bypasses most of these checks. This makes sense as a modal operator is obviously a UI event the user might want to "redo".

Finally to show the UI we need a window manager, area and region. Some contexts already have these, but others (like callbacks from timers/bpy.msgbus.subscribe_rna don't). In those cases you need to override the context as shown above.

So in order;

  1. You need bl_options = {'REGISTER', 'UNDO'} so it's eligible for the redo panel
  2. context.window_manager.modal_handler_add(self) to trigger the window manager to treat it as a user action which can be redone
  3. {'RUNNING_MODAL'} to run the modal state
  4. {'FINISHED'} to finish the modal operator immediately; and cause the "redo" UI to show
  5. Overriding the context is actually optional; but you'll need in some cases.

One final point of note is that you might want your invoke to be be more nuanced if you want to be able to run it from python without UI. In that case you can simply override the context; add a new top level string field and if that's set, run modal, if not just have invoke call execute and return 'FINISHED'.

$\endgroup$
7
$\begingroup$

While mountainstorm's answer is functional, you actually don't need so much to get the redo UI to show up. (At least not in Blender 2.9 - I haven't tried this in earlier versions but the release notes don't indicate any changes around this.)

The requirements are simply:

  1. Your operator's bl_options must include 'REGISTER' and 'UNDO'. (Register indicates to show the redo panel; undo is needed because redo is just an undo of the last operation, followed by re-execution of the operator.)
  2. Your operator needs to have at least one property defined.

That means this is a minimal operator that will display the redo panel when executed:

import bpy

class EXAMPLE_OT_DefaultIntOperator(bpy.types.Operator):
    bl_idname = "example.redo_operator"
    bl_label = "Example Redo Operator"
    bl_options = {'REGISTER', 'UNDO'}

    my_prop: bpy.props.IntProperty(name = "My Property")

    def execute(self, context):
        self.report({'INFO'}, f"my_prop: {self.my_prop}")
        return {'FINISHED'}

Example operator panel, with the header "Example Redo Operator" and a single integer property labeled "My Property".

This uses the default operator drawing logic, with one property per row. If you want, you can also control exactly what's drawn by defining the draw(self, context) method in your operator. The following code defines two operators, one matching the earlier example, and another using a custom draw method to enable/disable a property input based on another property's value.

import bpy

class EXAMPLE_OT_DefaultIntOperator(bpy.types.Operator):
    bl_idname = "example.redo_operator"
    bl_label = "Example Redo Operator"
    bl_options = {'REGISTER', 'UNDO'}

    my_prop: bpy.props.IntProperty(name = "My Property")

    def execute(self, context):
        self.report({'INFO'}, f"my_prop: {self.my_prop}")
        return {'FINISHED'}

class EXAMPLE_OT_CustomDrawOperator(bpy.types.Operator):
    bl_idname = "example.redo_operator_with_draw"
    bl_label = "Example Redo With Custom Draw"
    bl_options = {'REGISTER', 'UNDO'}

    dependent_prop: bpy.props.IntProperty(name = "My Property")
    flag_prop: bpy.props.BoolProperty(name = "Use Int")

    def execute(self, context):
        msg = f"dependent_prop: {self.dependent_prop}" if self.flag_prop else "Nothing to report"
        self.report({'INFO'}, msg)

        return {'FINISHED'}
    
    def draw(self, context):
        self.layout.use_property_split = True

        row = self.layout.row()
        row.prop(self, "flag_prop")
        
        sub = row.row()
        sub.enabled = self.flag_prop
        sub.prop(self, "dependent_prop", text="")

bpy.utils.register_class(EXAMPLE_OT_DefaultIntOperator)
bpy.utils.register_class(EXAMPLE_OT_CustomDrawOperator)

# If you're using Blender 2.9, operators only show in search if they're registered in a menu, 
# so you need everything below this point. You can find your ops in the Object menu within the 3D viewport.
# See https://wiki.blender.org/wiki/Reference/Release_Notes/2.90/Python_API#Compatibility
def basic_op_menu_func(self, context):
    self.layout.operator("example.redo_operator")

def custom_op_menu_func(self, context):
    self.layout.operator("example.redo_operator_with_draw")

bpy.types.VIEW3D_MT_object.append(basic_op_menu_func)
bpy.types.VIEW3D_MT_object.append(custom_op_menu_func)

Second operator panel with the header "Example Redo With Custom Draw", showing an unfilled checkbox labeled "Use Int" and a disabled integer field next to it.

As mountainstorm noted, you may need to override the execution context if you're calling the operator from a script and still want the redo panel to appear. If you're just creating a UI button via UILayout.operator, or executing your operator from a menu, then this should be unnecessary.

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .