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?