Python Class Does Not Show Dialog When Called from Object Context Menu

I’m working with a short script and am currently appending an item to the Object menu (VIEW3D_MT_object). When I use it by calling it through that menu, the dialog comes up and everything works fine. But when I attach it to the object context menu (VIEW3D_MT_object_context_menu), and call it through that menu, the dialog does not come up.

The object context menu is a lot more convenient for me, so it’d be nice if the dialog would work from there. Is there a reason it won’t work from the object context menu? (Yes, currently that part of the register() function is commented out, but even when it’s not, and the item is appended to the object context menu, it won’t work.)

import bpy
class ShrinkageScaler(bpy.types.Operator):
	bl_idname = "object.dialog_operator"
	bl_label = "Shrinkage Scaler"

	shrink_rate: bpy.props.IntProperty(name="Clay Shrink Rate",
											description="Shrinkage rate for clay body",
											default=10, min=0, max=90)

	def execute(self, context):
		expand = 100 / (100 - self.shrink_rate)
		message = "Using shrinkage rate of %s and expansion rate of %s" % (self.shrink_rate, expand)
		self.report({'INFO'}, message)
		sel_objs = [obj for obj in bpy.context.selected_objects if obj.type == 'MESH']
		# bpy.ops.object.select_all(action='DESELECT')
		for obj in sel_objs:
			s = obj.scale
			obj.scale = (s[0] * expand, s[1] * expand, s[2] * expand)
		return {'FINISHED'}

	def invoke(self, context, event):
		wm = context.window_manager
		return wm.invoke_props_dialog(self)


# Only needed if you want to add into a dynamic menu.
def menu_func(self, context):
    self.layout.operator(ShrinkageScaler.bl_idname, text=ShrinkageScaler.bl_label)

def register():
    bpy.utils.register_class(ShrinkageScaler)
    bpy.types.VIEW3D_MT_object.append(menu_func)
    # bpy.types.VIEW3D_MT_object_context_menu.append(menu_func)

def unregister():
    bpy.utils.unregister_class(ShrinkageScaler)
    bpy.types.VIEW3D_MT_object.remove(menu_func)
    # bpy.types.VIEW3D_MT_object_context_menu.remove(menu_func)

if __name__ == "__main__":
    register()
    # unregister()```

It seems invoke is not called from context menus, only from menus.

Therefore I don’t think it’s possible to achieve what you want with a single operator. But in theory you can use in the context menu some second utility operator that will call your main operator with INVOKE_DEFAULT (see https://docs.blender.org/api/current/bpy.ops.html)

Update. Apparently you can just set operator_context (Writing a Macro in Python - Wondering if what I want to do is a major or minor task - #15 by ImaginaryTango), though I’d say it can be considered a Blender bug that operator_context is not reset for new functions appended to the menu.

1 Like

Is not a big problem… most operators will execute just fine if their context is set to INVOKE_DEFAULT

But anyway, one can allways reset the operator_context to EXEC_REGION_WIN, or even better:

def menu_func(self, context):
    ctx = self.layout.operator_context 
    self.layout.operator_context = "INVOKE_DEFAULT"
    self.layout.operator(SimpleOperator.bl_idname, text=SimpleOperator.bl_label)
    self.layout.operator_context  = ctx

Also possible would be to add a child UILayout to the self.layout, and set the operator_context to that child (for example using a self.layout.row())

Resetting context in menu function with self.layout.operator_context = ctx is unnecessary, see example below.

Snippet:

import bpy
class ShrinkageScaler(bpy.types.Operator):
	bl_idname = "object.dialog_operator"
	bl_label = "Shrinkage Scaler"

	shrink_rate: bpy.props.IntProperty(name="Clay Shrink Rate",
											description="Shrinkage rate for clay body",
											default=10, min=0, max=90)

	def execute(self, context):
		expand = 100 / (100 - self.shrink_rate)
		message = "Using shrinkage rate of %s and expansion rate of %s" % (self.shrink_rate, expand)
		self.report({'INFO'}, message)
		sel_objs = [obj for obj in bpy.context.selected_objects if obj.type == 'MESH']
		# bpy.ops.object.select_all(action='DESELECT')
		for obj in sel_objs:
			s = obj.scale
			obj.scale = (s[0] * expand, s[1] * expand, s[2] * expand)
		return {'FINISHED'}

	def invoke(self, context, event):
        print('invoke is called')
		wm = context.window_manager
		return wm.invoke_props_dialog(self)


# Only needed if you want to add into a dynamic menu.
def menu_func(self, context):
    self.layout.operator_context = "INVOKE_REGION_WIN"
    print(self, "1", self.layout.operator_context)
    self.layout.operator(ShrinkageScaler.bl_idname, text=ShrinkageScaler.bl_label)
    print(self, "1.5", self.layout.operator_context)

def menu_func2(self, context):
    print(self, "2", self.layout.operator_context)
    self.layout.operator(ShrinkageScaler.bl_idname, text=ShrinkageScaler.bl_label)

def register():
    bpy.utils.register_class(ShrinkageScaler)
    bpy.types.VIEW3D_MT_object_context_menu.append(menu_func)
    bpy.types.VIEW3D_MT_object_context_menu.append(menu_func2)

def unregister():
    bpy.utils.unregister_class(ShrinkageScaler)
    # bpy.types.VIEW3D_MT_object_context_menu.remove(menu_func)

if __name__ == "__main__":
    register()
    # unregister()```

The output for it is below. So the second function is getting EXEC_REGION_WIN without reset.

<bpy_struct, VIEW3D_MT_object_context_menu at 0x00000045AEFFF5D0> 1 INVOKE_REGION_WIN
<bpy_struct, VIEW3D_MT_object_context_menu at 0x00000045AEFFF5D0> 1.5 INVOKE_REGION_WIN
<bpy_struct, VIEW3D_MT_object_context_menu at 0x00000045AEFFF5D0> 2 EXEC_REGION_WIN

The problem I was talking about is that the reason why it’s EXEC_REGION_WIN instead of INVOKE_DEFAULT (which is the default value https://docs.blender.org/api/current/bpy.types.UILayout.html#bpy.types.UILayout.operator_context), it is the last operator_context used in VIEW3D_MT_object_context_menu.draw:
https://projects.blender.org/blender/blender/src/commit/f3b1c9b2cd3da6b665969fde083fa6015b80980e/scripts/startup/bl_ui/space_view3d.py#L3121
and it’s just not reset when menu_func is called. This is a problem because

  1. This special context has to be memorized
  2. Tomorrow Blender will change the draw function adding some other operator_context that’s required for the last operator and it will affect all functions that other addons added after it.
1 Like

That’s the cost of using classes that you don’t own… :sweat_smile:

One could also make a commit to add layout.operator_context = 'INVOKE_REGION_WIN' to the end of the draw calls of every menu… :woozy_face:

Oh, actually, I think I was wrong. Just tried to add layout.operator_context = 'INVOKE_REGION_WIN' to the draw() I’ve mentioned above and next function call is still using EXEC_REGION_WIN. It seems to be just a default way for all context menus.

1 Like

I appreciate all the help and discussion and have not yet had time to review it.

I apologize for bringing this up on another thread. I was summarizing what was left that I’d want to fix in a comment there and listed this issue - without thinking that would lead to discussion there and here.

I’ve gone over the posts here, but haven’t had time to read them carefully and think through it all. @Secrop, it looks to me like what you wrote in the other thread summarized what I need to do. Am I right that I need to:

  1. add INVOKE_DEFAULT (and I’m not quite sure how to do that - is it a separate function to call?)
  2. Add the following code:
def menu_func(self, context):
    self.layout.operator_context = "INVOKE_DEFAULT"
    self.layout.operator(SimpleOperator.bl_idname, text=SimpleOperator.bl_label)

And, since that includes referencing “INVOKE_DEFAULT”, is that all I need to do to get it to work from the context menu?

I’m going by what I think I’ve seen on a read-through, but it’ll be a couple days before I can sit and read this thread carefully and make sure I understand it all, so I may be making things too simple.

Basically, yes!

There are other contexts that you can also use based on what your operator needs… thought INVOKE_DEFAULT is enough for a simple scaling function.

You can read more about the different execution contexts here and here.

Okay, that I can work with now. I still want to understand the rest, but making the change and making it easier to work with will be nice.

So I need to include INVOKE_DEFAULT in the menu_func() function, but then, if I use that, don’t I need to include an actual INVOKE_DEFAULT() function and then have it call my execute() function? Or is that what using INVOKE_DEFAULT in the menu_func() function does? (Sets it up to use execute() as the default?)

Thank you for that - it’ll be helpful!

The INVOKE_* operator contexts will define that the operator should be invoked prior to execution (i.e. for user input). If the operator doesn’t have an invoke function, then the system will jump to the execute function.

Note that some operators can be called directly with EXEC_* context, even if they have an invoke method (for example, when calling an operator with all needed parameters)…
Other operators require the invoke call (like modal operators), and cannot work without it.