Storing property in operator, which can be set by UI panel

I want to be able to have an operator with a property:

class VS_OT_Select(bpy.types.Operator):
    #...

    extend = bpy.props.BoolProperty(...)

    def execute(self, context):
        #...

And a panel with a button for the operator as well as setting the properties themselves:

class VS_PT_MyPanel(bpy.types.Panel):
    #...

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

        op = layout.operator(VS_OT_Select.bl_idname)
        layout.prop(op, 'extend')
        # or
        op.draw() # and I expose the properties myself

I keep getting attribute not found errors however I do it. I know properties can be drawn/exposed during a redo and after the operator is run. I also know I can create global scene variables to be referenced by the operator - that’s what I currently am doing - but I have lots of variables and I’d like to keep the addon’s register() clean. I’d like to be able to expose the operators properties in a UI panel beforehand so users can tweak before running. Is this possible within Blender at this point (2.9x)?

After extensive web searches, I found a dev forum that mentioned a hacky way of using keymaps: Why can’t we display Operator properties within a Panel? - Blender Developer Talk

In case the link breaks, I’ll add it here:

import bpy


class VIEW3D_PT_custom_redo_panel(bpy.types.Panel):
    bl_label = "Custom Redo Panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = "Tool"

    def draw(self, context):
        layout = self.layout
        col = layout.column()
        col.prop(kmi.properties, "location")
        col.prop(kmi.properties, "hidden")
        op = col.operator("object.simple_operator", text="Execute")
        op.location = kmi.properties.location
        op.hidden = kmi.properties.hidden


class OBJECT_OT_simple_operator(bpy.types.Operator):
    bl_idname = "object.simple_operator"
    bl_label = "Simple Object Operator"
    bl_options = {'REGISTER', 'UNDO'}

    location: bpy.props.FloatVectorProperty(size=3)
    hidden: bpy.props.BoolProperty()

    def execute(self, context):
        context.object.location = self.location
        context.object.hide_set(self.hidden)
        return {'FINISHED'}


if __name__ == "__main__":
    bpy.utils.register_class(VIEW3D_PT_custom_redo_panel)
    bpy.utils.register_class(OBJECT_OT_simple_operator)
    kmi = bpy.context.window_manager.keyconfigs.addon.keymaps['3D View'].keymap_items.new("object.simple_operator", "NONE", "ANY")
    kmi.active = False # I added this to prevent accidental startup

However, this still doesn’t answer the base question for me: is there a reason why operator properties cannot be accessed and edited directly in a UI panel yet, for architecture reasons perhaps?

What about using PropertyGroup for settings?

import bpy

#Operator settings
class SimpleOperatorPropertyGroup(bpy.types.PropertyGroup):
    location: bpy.props.FloatVectorProperty(name="Location", size=3)
    hidden: bpy.props.BoolProperty(name="Hidden")


#Draw layout for operator and panel
class OperatorDrawSettings:
    def draw(self, context):
        prop_grp = context.window_manager.simple_operator
        
        layout = self.layout
        col = layout.column()
        col.prop(prop_grp, "location")
        col.prop(prop_grp, "hidden")

        
#Common properties for panels
class View3DPanel:
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = "Tool"
    
    @classmethod
    def poll(cls, context):
        return (context.object is not None)

        
#Panel for operator                
class VIEW3D_PT_custom_redo_panel(View3DPanel, bpy.types.Panel):
    bl_label = "Custom Redo Panel"

    def draw(self, context):
        layout = self.layout
        col = layout.column()        
        col.operator("object.simple_operator", text="Execute Simple Operator")


#Sub panel for operator settings        
class VIEW3D_PT_custom_operator_settings_panel(View3DPanel, OperatorDrawSettings, bpy.types.Panel):
    bl_parent_id = "VIEW3D_PT_custom_redo_panel"    
    bl_label = "Simple Operator Settings"

    
#Object simple operator       
class OBJECT_OT_simple_operator(OperatorDrawSettings, bpy.types.Operator):
    bl_idname = "object.simple_operator"
    bl_label = "Simple Object Operator"
    bl_options = {'REGISTER', 'UNDO'}
    
        
    def execute(self, context):
        prop_grp = context.window_manager.simple_operator        
                
        context.object.location = prop_grp.location
        context.object.hide_set(prop_grp.hidden)
        return {'FINISHED'}


classes = (
    SimpleOperatorPropertyGroup,
    VIEW3D_PT_custom_redo_panel,
    VIEW3D_PT_custom_operator_settings_panel,    
    OBJECT_OT_simple_operator,          					 
)


def register():
    from bpy.utils import register_class
    for cls in classes:
        register_class(cls)

    bpy.types.WindowManager.simple_operator = bpy.props.PointerProperty(type=SimpleOperatorPropertyGroup)

    
def unregister():
    from bpy.utils import unregister_class
    for cls in reversed(classes):
        unregister_class(cls)
        
    del bpy.types.WindowManager.simple_operator        

    
if __name__ == "__main__":
    register()

Thank you for posting. I definitely like the organization of grouping them into a PointerProperty by operator over registering each prop by itself in register(). That will definitely be useful to others.

I still wish the higher goal is attainable of keeping property declarations within the operator. In other words, all props the operator needs are completely self-contained, and panels simply access the operator’s props directly for the user to tweak.

To extend this script we’re chaining, here’s my ideal:

import bpy

        
#Panel for operator                
class VIEW3D_PT_custom_panel(bpy.types.Panel):
    bl_label = "Custom Panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = "Tool"
    
    @classmethod
    def poll(cls, context):
        return (context.object is not None)

    def draw(self, context):
        layout = self.layout
        op = layout.operator("object.simple_operator", text="Execute Simple Operator")
        layout.prop(op, 'location')
        layout.prop(op, 'hidden')

    
#Object simple operator       
class OBJECT_OT_simple_operator(bpy.types.Operator):
    bl_idname = "object.simple_operator"
    bl_label = "Simple Object Operator"
    bl_options = {'REGISTER', 'UNDO'}
    
    location: bpy.props.FloatVectorProperty(name="Location", size=3)
    hidden: bpy.props.BoolProperty(name="Hidden")
    
        
    def execute(self, context):
        context.object.location = self.location
        context.object.hide_set(self.hidden)
        return {'FINISHED'}


classes = (
    VIEW3D_PT_custom_panel,  
    OBJECT_OT_simple_operator,          					 
)


def register():
    from bpy.utils import register_class
    for cls in classes:
        register_class(cls)

    
def unregister():
    from bpy.utils import unregister_class
    for cls in reversed(classes):
        unregister_class(cls) 

    
if __name__ == "__main__":
    register()

These operator properties can be made visible to the user in the UI panel:

image

However, they’re not editable here - they’re locked. I can certainly edit the properties in the redo panel after I run them or as a developer set the settings within the panel (op.hidden = True). However, depending on the settings the user chooses, some operators may run for relatively long time. I’d prefer the user to get to adjust the settings in the panel first before running the operator.

I guess the big question is, why are operator props unable to be tweaked within a UI panel directly?

2 Likes

I ran into this very issue. It turns out that every time you move the mouse, etc, the panel is redrawn and that recreates the operator resetting the properties to their default. You need to store the properties elsewhere. I’m following this tutorial and it works though I feel there could be a better place to store the properties than scene.

2 Likes

I appreciate the explanation - that explains why exposing the operator settings seems “locked” on the panel. It’s just reset constantly. The keymap solution does get around this. My current solution is just to have the props dialog pop up so users can choose settings before it runs (there are cases where it can take a while, so I don’t want users to have to readjust and wait longer):

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

As far as I understand it, an operator class is like a blueprint. There exists no instance of it until the operator is triggered by something, at which point Blender uses the blueprint to create the actual instance.
Also, the return value of layout.operator() is not an instance of the operator class. Instead, it’s an instance of OperatorProperties, which is later passed to the actual operator instance on creation.

I hope this clears things up a bit.

Thank you for that - I marked your answer as a solution, coupled with the fact that the panel is redrawn constantly is why the properties reset. It’s a good foundation so others can understand why that is :slight_smile:

I’d be curious to see if it makes sense for Blender to allow persisted operator-scope variables in panel layouts so they could be edited for an operator pre-execution (granted, my invoke solution is pretty close to that).

Actually you can, by exposing in UI last used operator’s properties.

import bpy

class ExampleModifierPanel(bpy.types.Panel):
    bl_label = "Example Modifier"
    bl_idname = "OBJECT_PT_example_modifier"
    bl_space_type = "PROPERTIES"
    bl_region_type = "WINDOW"
    bl_context = "modifier"

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        box.prop(context.window_manager.operator_properties_last("MESH_OT_primitive_cube_add"), "size")
        box.operator("mesh.primitive_cube_add")


def register():
    bpy.utils.register_class(ExampleModifierPanel)


if __name__ == "__main__":
    register()
2 Likes