3
$\begingroup$

I am trying to find my way around events and modals and I would expect that a mouseclick would provide the appropriate context.

I've used this answer as a starting point but it looks like context.area is always Ǹone in a mouse event.

Full code here:

import bpy
from bpy_extras.view3d_utils import region_2d_to_vector_3d, region_2d_to_location_3d

class ModalTimerOperator(bpy.types.Operator):
    """Operator which runs its self from a timer"""
    bl_idname = "wm.modal_timer_operator"
    bl_label = "Add Sphere on Click"

    _timer = None

    def modal(self, context, event):
        if event.type in {'RIGHTMOUSE', 'ESC'}:
            self.cancel(context)
            return {'CANCELLED'}

        if event.type == 'LEFTMOUSE' and event.value == 'PRESS':
            # left click
            print(context.area.type)
            if context.area.type == 'VIEW_3D':
                region = context.region
                r3d = context.space_data.region_3d
                x, y = event.mouse_region_x, event.mouse_region_y
                view = region_2d_to_vector_3d(region, r3d, (x, y))
                loc = region_2d_to_location_3d(region, r3d, (x, y), view)
                bpy.ops.mesh.primitive_uv_sphere_add(location=loc)
        
        if event.type == 'TIMER':
            #print("timer")
            pass


        return {'PASS_THROUGH'}

    def execute(self, context):
        if context.area.type != 'VIEW_3D':
            print("Must use in a 3d region")
            return {'CANCELLED'}
        
        wm = context.window_manager
        self._timer = wm.event_timer_add(0.1, window=context.window)
        wm.modal_handler_add(self)
        return {'RUNNING_MODAL'}

    def cancel(self, context):
        wm = context.window_manager
        wm.event_timer_remove(self._timer)


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


def unregister():
    bpy.utils.unregister_class(ModalTimerOperator)


if __name__ == "__main__":
    register()

    # test call
    bpy.ops.wm.modal_timer_operator()

As a result I keep getting an error

AttributeError: 'NoneType' object has no attribute 'type'

at the line containing

if context.area.type != 'VIEW_3D':

Why is that?

$\endgroup$
4
  • $\begingroup$ I would need to see more code than the one line to even guess. Can you cut and paste your script to your question? As a guess, 'context' can be something other than a window context, in which case it wouldn't have an area. Mouse events are window manager events so I would suspect a window manager context instead. $\endgroup$ Commented May 1, 2022 at 14:47
  • $\begingroup$ @MartyFouts - done! $\endgroup$
    – simone
    Commented May 1, 2022 at 15:15
  • $\begingroup$ How do you run the script? By click the run script button?, at that time context.area.type != 'VIEW_3D', so it return {'CANCELLED'} $\endgroup$
    – X Y
    Commented May 1, 2022 at 17:20
  • $\begingroup$ You can run the operator in viewport. But when you switch the workspace or load a new blend file without close the operator, it will cause error. $\endgroup$
    – X Y
    Commented May 1, 2022 at 17:24

1 Answer 1

3
$\begingroup$

You may want to watch this tutorial about modal operators; but the upshot is that mouse events happen in window manager context so you don't have an area. Instead you need to access the area type through context.space_data.type. Here's a very simple modal operator, taken from that tutorial that should help you work out what you want:

class SFA_OT_ViewOperator(bpy.types.Operator):
    """Translate the view using mouse events"""
    bl_idname = "view3d.modal_operator"
    bl_label = "Simple View Operator"

    offset: FloatVectorProperty(
        name="Offset",
        size=3,
    )

    def modal(self, context, event):
        v3d = context.space_data
        rv3d = v3d.region_3d

        if event.type == 'MOUSEMOVE':
            mouse_pos = Vector((event.mouse_x, event.mouse_y, 0.0))
            mouse_move = self._initial_mouse - mouse_pos
            self.offset = mouse_move * 0.02
            self.execute(context)
            context.area.header_text_set("Offset %.4f %.4f %.4f" % tuple(self.offset))

        elif event.type == 'LEFTMOUSE':
            context.area.header_text_set(None)
            return {'FINISHED'}

        elif event.type in {'RIGHTMOUSE', 'ESC'}:
            v3d = context.space_data
            rv3d = v3d.region_3d
            rv3d.view_location = self._initial_location
            context.area.header_text_set(None)
            return {'CANCELLED'}
        
        elif event.type in {'WHEELUPMOUSE', 'WHEELDOWNMOUSE'}:
            return {'PASS_THROUGH'}

        return {'RUNNING_MODAL'}

    def invoke(self, context, event):

        if context.space_data.type == 'VIEW_3D':
            self.report({'WARNING'}, "Active space must be a View3d")
            return {'CANCELLED'}

        v3d = context.space_data
        rv3d = v3d.region_3d

        if rv3d.view_perspective == 'CAMERA':
            rv3d.view_perspective = 'PERSP'

        self._initial_mouse = Vector((event.mouse_x, event.mouse_y, 0.0))
        self._initial_location = rv3d.view_location.copy()

        context.window_manager.modal_handler_add(self)
        return {'RUNNING_MODAL'}

    def execute(self, context):
        v3d = context.space_data
        rv3d = v3d.region_3d

        rv3d.view_location = self._initial_location + Vector(self.offset)
$\endgroup$
3
  • 1
    $\begingroup$ This works - one has to be extra careful as to what gets captured and what not. It took me a while to find out that you need a catch-all else that returns PASS_THROUGH at the end in order not to block all other UI interactions $\endgroup$
    – simone
    Commented May 2, 2022 at 19:25
  • $\begingroup$ This example assumes you want to continue to run modal until you explicitly cancel or finish. That's why it ends by returning RUNNING_MODAL if it doesn't explicitly handle the event. But yes, if you want to pass on any event you don't handle just change the last elif to an else. $\endgroup$ Commented May 2, 2022 at 23:27
  • $\begingroup$ Thanks! I just added the comment as a note for anyone that might stumble upon the same pitfall - no criticism implied. Thanks for this and all the other answers $\endgroup$
    – simone
    Commented May 3, 2022 at 5:16

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .