The load_post Handler and starting an Operator that uses modal

I am trying to, on load of a blend file, start an Operator that uses modal (and tag_redraw) to draw a text label alongside every mesh in the scene. (I got the drawing code etc. from the python template “Operator Modal Draw”.)

The problem is I don’t know how to smoothly start that process; there are NoneType errors atm.

I can make it happen by just running the operator via F3 in the 3D View, but not from the load_post handler.

Here is my sample code, and the errors follow:

bl_info = {
"name": "A Problem with drawing",
"author": "dbat",
"version": (1,0),
"blender": (4,0), # min vers supported
"location": "before load",
"description": "Asking for help with this."
}


import bpy
import addon_utils
import bgl
import blf
from bpy_extras.view3d_utils import location_3d_to_region_2d as vec_3d_2d
from bpy.app.handlers import persistent


def draw_callback_px(self, context):
    # Stupid example to just label all the objects.
    print("draw callback runs")
    font_id = 0  
    all_objs = [o for o in bpy.data.objects]

    for oj in all_objs:
        v3d = context.space_data
        rv3d = v3d.region_3d
        region = bpy.context.region

        vec = vec_3d_2d(region, rv3d, oj.location)
        x = vec[0]
        y = vec[1]
        
        # draw function
        blf.position(font_id, x, y, 0)
        blf.size(font_id, 10)
        blf.draw(font_id, f"I am {oj.name}")


class ModalDrawText(bpy.types.Operator):
    """Text linked to an object"""
    bl_idname = "view3d.modal_text"
    bl_label = "Show text in 3D Viewport next to the active object"

    def invoke(self, context, event):
        
        # The problem is that context.area is empty on startup, I think.
        #if not context.area: return ...? what? How do I rerun invoke on fail?

        if context.area.type == 'VIEW_3D':
            args = (self, context)
            self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_px, args, 'WINDOW', 'POST_PIXEL')
            context.window_manager.modal_handler_add(self)
            return {'RUNNING_MODAL'}
        else:
            self.report({'WARNING'}, "View3D not found, cannot run operator")
            return {'CANCELLED'}

    # Never gets to run.
    def modal(self, context, event):
        context.area.tag_redraw()
        if event.type in {'RIGHTMOUSE', 'ESC'}:
            bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
            return {'CANCELLED'}

        return {'PASS_THROUGH'}



@persistent
def load_handler(dummy):
    # Trying to start the label drawing code after the blend file is loaded.
    bpy.ops.view3d.modal_text('INVOKE_DEFAULT')



def register():
    bpy.utils.register_class(ModalDrawText)
    bpy.app.handlers.load_post.append(load_handler)
    

def unregister():
    bpy.utils.unregister_class(ModalDrawText)
    bpy.types.OUTLINER_MT_object.remove(outliner_object_menu_func)

if __name__ == "__main__":
    register()

The errors, on loading a file:

register_class(...):

AttributeError: 'NoneType' object has no attribute 'type'
Error: Python: Traceback (most recent call last):
  File "/media/donn/sea1tb/Projects/06.Blending/blender_addon_dev/addons/problem-drawing-labels/__init__.py", line 50, in invoke
    if context.area.type == 'VIEW_3D':
       ^^^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'type'
Location: /home/donn/bin/blenders/blender-4.4.0-linux-x64/4.4/scripts/modules/bpy/ops.py:107
Error in bpy.app.handlers.load_post[1]:
Traceback (most recent call last):
  File "/media/donn/sea1tb/Projects/06.Blending/blender_addon_dev/addons/problem-drawing-labels/__init__.py", line 73, in load_handler
    bpy.ops.view3d.modal_text('INVOKE_DEFAULT')
  File "/home/donn/bin/blenders/blender-4.4.0-linux-x64/4.4/scripts/modules/bpy/ops.py", line 107, in __call__
    ret = _op_call(self.idname_py(), kw, C_exec, C_undo)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The problem is that context.area is None. modal_text invoke is called in load_handler, but then immediately fails.

How do I “restart” that operator at some point when Blender is ready? Or is there a way to tell it that, actually, I meant you to go use the 3d view all along?

Thanks!

Hello, you are actually most of the way there with your script, and the load_post handler is there to avoid the exact error you’re getting, so at first I was confused.

Then I realized that the context might have been created, but you might not be ‘in’ the right spot for it, if that makes sense.

If you add 2 lines to your load_handler function it should work the way you want:

@persistent
def load_handler(dummy):

    # First tell Blender that the area we need is the 3D View
    # And that it can search all the areas on screen until it finds one
    area = [a for a in bpy.context.screen.areas if a.type == 'VIEW_3D'][0]

    # Then we're going to use the 3D View area context that we found
    # And override whatever the current context is with the 3D View context
    with bpy.context.temp_override(area=area):
        # Then call your operator from inside the override function
        bpy.ops.view3d.modal_text('INVOKE_DEFAULT')

I also got an error from this line: bpy.types.OUTLINER_MT_object.remove(outliner_object_menu_func), but I didn’t remove it in case it related to some other part of the script that didn’t get posted.

The context override syntax that I used above is a pretty common way to get around context errors, even if it is a little tricky to figure out at first.

Wow! So easy! You’re a genius. Thank you very much!