When to initialize custom scene variables in an addon

I have this script that relies on a lot of custom scene properties. Initially I was able to default all of the floats in the function that registers the properties but when I turned it into an add-on the function started throwing an exception on registration (restricted context, preventing access to the scene during registration).

I removed those lines and it would register but I’d have problems with the enum properties. For example, with the following in the draw function:

if context.scene['data_path'] == 1 and (context.active_object.rotation_mode == 'QUATERNION' or context.active_object.rotation_mode == 'AXIS_ANGLE'):
    row.prop(scene,'vector4')

else:
   row.prop(scene,'vector3')

The data_path property didn’t have an integer value, even though I set a default during declaration, so it would throw an error until I selected an item from the data_path menu (location, rotation, or scale). But when I changed it from context.scene[‘data_path’] == 1 to context.scene.data_path == ‘Rotation’ it started working using the defaults set in the bpy.props.EnumProperty function.

Making those changes to the enum properties fixed all of those problems, the numeric values with a known default are set correctly, the boolean properties default to false like they’re supposed to, but I have trouble initializing variables that rely on a formula, specifically when the formula contains scene data like this:

scene = context.scene
scene.quarter = (60/scene.tempo) * (scene.render.fps/scene.render.fps_base)
scene.whole = scene.quarter * 4
...
scene.selected_note = scene.quarter

I have an update operator that you use to update the note values when you change the scene’s frame rate since I can’t seem to work that into a callback but is there any way to initialize it out of the box? Right now it’s set up so that the user has to manually refresh the values either with the operator or changing the time signature.

I’ve tried initializing the properties in the register function (throws an error when enabling the add-on), a boolean test in the draw function that calls an initialize function on the first run (throws an error since you can’t write to a property during a draw), and I also made an initialize operator where on registration you’ll just see an initialize button then when you press it the interface will appear with all of the values set to defaults, but I accomplish that by turning the panel’s draw function into one large if…else statement and I’m not sure what kind of impact that will have on performance.

initialize operator where on registration you’ll just see an initialize button then when you press it the interface will appear with all of the values set to defaults

that’s the way to go. Isn’t it just a single if/else statement in the draw method? Computers don’t really care how far they have to “jump” 'cause of if/else conditions, it’s really the amount of the conditions (if you had 100 criteria as condition for the if-block, that would slow it down, but a single check whether values are initilized or not, won’t hurt).

Nonetheless, there’s a hack to workaround the restricted context / writing to id classes in draw method prohibition:

import bpy

class MyColl(bpy.types.PropertyGroup):
    name = bpy.props.StringProperty()


class CustomMenuOp(bpy.types.Operator):
    bl_idname = "scene.change_index"
    bl_label = "Simple Object Operator"

    index = bpy.props.IntProperty()

    def execute(self, context):
        context.scene.col_idx = self.index
        return {'FINISHED'}

class CustomMenu(bpy.types.Menu):
    bl_label = "Custom Menu"
    bl_idname = "OBJECT_MT_custom_menu"

    def draw(self, context):
        layout = self.layout
        
        for i, elem in enumerate(context.scene.col):
            layout.operator("scene.change_index", text=elem.name).index = i

class HelloWorldPanel(bpy.types.Panel):
    """Creates a Panel in the Object properties window"""
    bl_label = "Hello World Panel"
    bl_idname = "OBJECT_PT_hello"
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "object"


    def draw(self, context):
        layout = self.layout
        
        col = context.scene.col
        idx = context.scene.col_idx
        
        if idx >= len(col):
            #context.scene.col_idx = len(col) - 1
            text = "(index error)"
        else:
            text = col[idx].name
            
        layout.menu("OBJECT_MT_custom_menu", text=text)

def collhack(scene):
    bpy.app.handlers.scene_update_pre.remove(collhack)
    
    try:
        scene.col.clear()
    except:
        pass
    
    item = scene.col.add()
    item.name = "Foobar"
    
    item = scene.col.add()
    item.name = "Second entry"
    

def register():
    bpy.utils.register_module(__name__)
    bpy.types.Scene.col = bpy.props.CollectionProperty(type=MyColl)
    bpy.types.Scene.col_idx = bpy.props.IntProperty(default=0)
    
    bpy.app.handlers.scene_update_pre.append(collhack)


def unregister():
    bpy.utils.unregister_module(__name__)
    del bpy.types.Scene.col
    del bpy.types.Scene.col_idx
    

if __name__ == "__main__":
    register()


I basically register a scene_update handler, which removes itself. Thus, it runs just once, and it does run after addon registration (scene update event occurs after that), so no restrictions to context at that moment. This has worked so far for me, but can’t guarantee it will in future, nor if it’s possible that the scene update event occurs while addon is still registering.

Thank you. I know the initialize button works. I’m a bit apprehensive about hack methods, especially involving scene handlers (which I’m nowhere close to understanding at this point), so I think I’ll stick with the button.

scene_update handlers allow you to register callback functions. They will be called before (scene_update_pre) or after (scene_update_post) a scene update. On windows, that event occurs like 20-30 times per second.