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$ May 1 at 14:47
  • $\begingroup$ @MartyFouts - done! $\endgroup$
    – simone
    May 1 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
    May 1 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
    May 1 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
    May 2 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$ May 2 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
    May 3 at 5:16

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.