1
$\begingroup$

During the development of an add-on I had to generate an operator based on the information in a dictionary. As I didn't find anything on that topic, I thought it might be useful to add my solution for others to use...

This might come in useful, when you have a dictionary of data, a user needs to fill in, before some operator logic is executed, but you don't want to create an operator for every possible dictionary combination.

$\endgroup$
  • 1
    $\begingroup$ Hi. If you have an answer, even to your own question, this should be in the answer box below, not as part of the question. $\endgroup$ – Ray Mairlot Mar 6 '18 at 16:58
  • $\begingroup$ Thanks. Seems like I need to add an answer myself and then mark it as solved, right? $\endgroup$ – Mr. Anderson Mar 6 '18 at 17:00
  • $\begingroup$ You can mark it as solved if you want, but you may want to wait a few days and see if anyone has an alternative (maybe even better) solution. $\endgroup$ – Ray Mairlot Mar 6 '18 at 17:38
  • $\begingroup$ Good point. I need to wait two days anyway, before I can accept my own solution. $\endgroup$ – Mr. Anderson Mar 6 '18 at 17:59
2
$\begingroup$

As operators are not editable after creation, I achieved this by using a CollectionProperty with is hidden to the user. Instead, the temporary operator just shows the elements as single Properties of the type specified in the dictionary. The preset values for the Properties is taken from the dictionary, too.

That way, we can create operators "on the fly", which are created in the function createOperator and discarded, when the next temporary operator is created.

The Property type is handled in the calling operator by adding a type substring (such as 'b_' for boolean) to the name of the Property, which will show up in the GUI. Additional Properties could be added the same way.

WARNING: It might be dangerous to use these operators with Blenders undo functionality. Therefore, I disabled it for these generic operators. Also, the usage of eval is dangerous in terms of security as written here. So make sure, your dictionary does not contain user input or digest the operatorName properly.

I hope this is of use to anyone.

Happy coding, Mr. Anderson!

P.S.: New to the forum, so feedback is VERY appreciated! :)


Just for reference: I took some inspiration from this question on dynamic operator generation and this old thread at blenderartists on generating dynamic properties in operators. Especially, this trick by sambler helped alot. Generating Blender operators in a function comes from an older Blender Dev entry.

bl_info = {
    'version': (1, 0),
    'blender': (2, 79, 0),
    'author': 'Mr. Anderson',
    'name': 'Temporary Operator Showcase',
    'location': 'blender',
    'description': 'Creates operators when executing another operator, based on the contents of a dictionary.' ,
    'warning': 'This is a test only!',
    'category': 'test',
}

import bpy


custom_dict = {
    'spoon': {'bent': True, 'dynamic': False},
    'smiths': {'amount': 55, 'angry': True},
    'matrix': {'greenness': 0.5, 'speed': 6.5}
}


class PropertyEntry(bpy.types.PropertyGroup):
    name = bpy.props.StringProperty()
    intProp = bpy.props.IntProperty()
    boolProp = bpy.props.BoolProperty()
    stringProp = bpy.props.StringProperty()
    floatProp = bpy.props.FloatProperty()


def registerTempOperator(name):
    # check for previous temporary Operator and unregister it
    try:
        bpy.utils.unregister_class(TempOperator)
    except UnboundLocalError:
        pass
    blender_id_name = 'object.add_' + name

    # create the temporary operator class
    class TempOperator(bpy.types.Operator):
        """Temporary operator"""
        bl_idname = blender_id_name
        bl_label = 'Add a ' + name + ' object.'
        bl_description ='Adds a ' + name + ' object to the scene.'
        bl_options = {'REGISTER'}

        operator_data = bpy.props.CollectionProperty(type=PropertyEntry)
        operator_name = name

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

            # expose all properties in the collection to the user
            for i in range(len(self.operator_data)):
                propname = self.operator_data[i].name[2:]
                # choose the right Property type depending on name identifier
                if self.operator_data[i].name[0] == 'i':
                    layout.prop(self.operator_data[i], 'intProp', text=propname)
                elif self.operator_data[i].name[0] == 'b':
                    layout.prop(self.operator_data[i], 'boolProp', text=propname)
                elif self.operator_data[i].name[0] == 's':
                    layout.prop(self.operator_data[i], 'stringProp', text=propname)
                elif self.operator_data[i].name[0] == 'f':
                    layout.prop(self.operator_data[i], 'floatProp', text=propname)

        def invoke(self, context, event):
            data = custom_dict[self.operator_name]
            for key in data.keys():
                item = self.operator_data.add()
                prefix = ''
                if type(data[key]) is int:
                    item.intProp = data[key]
                    prefix = 'i'
                elif type(data[key]) is bool:
                    item.boolProp = data[key]
                    prefix = 'b'
                elif type(data[key]) is str:
                    item.stringProp = data[key]
                    prefix = 's'
                elif type(data[key]) is float:
                    item.floatProp = data[key]
                    prefix = 'f'

                # add the identifier to specify the data type
                item.name = prefix + '_' + key

            # show operator as popup window
            return context.window_manager.invoke_props_dialog(self)

        def execute(self, context):
            # TODO write the contents of operator_data somewhere here
            # or do something awesome

            print('Data is in here:')
            print(self.operator_data)
            print([i.name for i in self.operator_data])
            return {'FINISHED'}

    # register the temporary class and return the operatorBlId
    bpy.utils.register_class(TempOperator)
    return blender_id_name


def operatorList(self, context):
    items = []
    for item in custom_dict:
        items.append((item,) * 3)
    return items


class CallingOperator(bpy.types.Operator):
    """Operator which calls the temporary operator"""
    bl_idname = 'object.calling_operator'
    bl_label = 'Call operator'
    bl_description ='Call the temporary operators specified in custom_dict'
    bl_options = {'REGISTER'}

    op_list = bpy.props.EnumProperty(items=operatorList)

    def draw(self, context):
        layout = self.layout
        layout.prop(self, 'op_list', expand=True)

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

    def execute(self, context):
        operator_name = registerTempOperator(self.op_list)

        # invoke the other operator, based on its bl_idname
        operator = eval('bpy.ops.' + operator_name + "('INVOKE_DEFAULT')")
        return {'FINISHED'}


# just a basic panel to add the test operator to the 3D toolbar
class TestPanel(bpy.types.Panel):
    """Contains the operator to test."""
    bl_idname = "TOOLS_PT_TEST_TOOLS"                                                                                                                                                  
    bl_label = "TEST"                                                                                                                                                           
    bl_space_type = 'VIEW_3D'                                                                                                                                                            
    bl_region_type = 'TOOLS'                                                                                                                                                             
    bl_category = 'Tools'

    def draw(self, context):
        layout = self.layout
        layout.operator('object.calling_operator')


def register():
    bpy.utils.register_class(PropertyEntry)
    bpy.utils.register_class(CallingOperator)
    bpy.utils.register_class(TestPanel)

def unregister():
    bpy.utils.register_class(PropertyEntry)
    bpy.utils.register_class(CallingOperator)
    bpy.utils.unregister_class(TestPanel)

if __name__ == '__main__':
    register()
$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.