This does what I wanted. There are 5 things needed to make it work:
- Your operator needs
bl_options = {'REGISTER', 'UNDO'}
, without this the operator properties UI doesn't even show when running via the search menu (F3)
- In invoke you need to register yourself as modal
context.window_manager.modal_handler_add(self)
invoke
needs to return {'RUNNING_MODAL'}
- You need to implement
modal
though it can just return {'FINISHED'}
- 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;
- You need
bl_options = {'REGISTER', 'UNDO'}
so it's eligible for the redo panel
context.window_manager.modal_handler_add(self)
to trigger the window manager to treat it as a user action which can be redone
{'RUNNING_MODAL'}
to run the modal state
{'FINISHED'}
to finish the modal operator immediately; and cause the "redo" UI to show
- 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'.