Msgbus: how to find the view3D that triggered a View3DShading type change callback

I am able to register a callback for the Viewport Shading Method and my callback is called when the events happen, but I’m at a loss for how to determine which view3D area the even happened in. Given

owner = object()
subscribe_to = (bpy.types.View3DShading, "type")
def notify_test(*args):
    print("What goes", args)
    
bpy.msgbus.subscribe_rna(key=subscribe_to, owner=owner, args= ("here",), notify=notify_test,)

is there something I can use in args or is there some other way in the callback to determine what triggered the callback?

The msgbus library was developed by Campbel Barton ( @ideasman ). You could ask him on the #python channel on blender.chat

1 Like

If you find out be sure to let us know, I’ve often wondered this myself

1 Like

I don’t know if there is a way to get the active area with the API, but a manual way to do so can be done like this:

import bpy


class ModalTimerOperator(bpy.types.Operator):
    """Operator which runs its self from a timer"""
    bl_idname = "wm.modal_timer_operator"
    bl_label = "Modal Timer Operator"
    _timer = None

    def point_inside_rect(self, pos, rect):
        return pos[0] > rect[0] and pos[1] > rect[1] and pos[0] < rect[2] and pos[1] < rect[3]

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

        if event.type == 'TIMER':
            print('modal update')

            mousepos = [event.mouse_x, event.mouse_y]
            print(mousepos)

            for area in [a for a in bpy.context.screen.areas if a.type == 'VIEW_3D']:
                arearect = [area.x, area.y, area.width, area.height]
                print(area, arearect)

                if self.point_inside_rect(mousepos, arearect):
                    print('inside')

            
        return {'PASS_THROUGH'}

    def execute(self, context):
        wm = context.window_manager
        self._timer = wm.event_timer_add(2, window=context.window)
        wm.modal_handler_add(self)

        print('test point in rect', self.point_inside_rect([5, 5], [0, 0, 10, 10]))
        print('running')

        return {'RUNNING_MODAL'}

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

def menu_func(self, context):
    self.layout.operator(ModalTimerOperator.bl_idname, text=ModalTimerOperator.bl_label)

def register():
    bpy.utils.register_class(ModalTimerOperator)
    bpy.types.VIEW3D_MT_view.append(menu_func)

# Register and add to the "view" menu (required to also use F3 search "Modal Timer Operator" for quick access)
def unregister():
    bpy.utils.unregister_class(ModalTimerOperator)
    bpy.types.VIEW3D_MT_view.remove(menu_func)


if __name__ == "__main__":
    register()
    # test call
    bpy.ops.wm.modal_timer_operator()

If you split the 3D View left-right you would see the result printed. Once you run the modal once, and get the active area. Store it somewhere and cancel the operator.

hrm, it’s a nice idea- but this wouldn’t really help in the context of a msgbus callback though, because the property can be set from anywhere, what OP is asking is how to know which area’s RNA was accessed and this operator relies on the mouse being in the area (which it might not be when a msgbus callback is fired)

RNA subscription can be global, or specific to an RNA instance depending on the key passed it:

key = (bpy.types.View3DShading, "type")  # Global key

A key that subscribes to any shading type of all view3d windows, present and future.

But, assuming we had an instance of View3DShading, sh, we can create a subscription key that triggers only from this particular RNA:

sh = bpy.context.space_data.shading
key = sh.path_resolve("type", False)  # Instance key

A key that subscribes to a shading type of a given View3DShading instance.

We then could pass the area in the args keyword argument.

for area in bpy.context.screen.areas:
    if area.type == 'VIEW_3D':
        sh = area.spaces[0].shading
        key = sh.path_resolve("type", False)
        bpy.msgbus.subscribe_rna(key=key, owner=bpy, args=(area,), notify=notify_test,)

The downside of using instance keys, is that we have to do the bookkeeping ourselves, which means manually subscribing for each 3d view we would want this on, handle subscription to new areas created by the user, handle workspace changes, and remove invalid areas.

It’s certainly possible, but that’s a lot of code and cognitive overhead for something that should have been as simple as passing a set option to subscribe_rna to get the instance along with any other argument.

Full example:

import bpy


def v3d_shading_notify(area):
    sh = area.spaces.active.shading
    index = area.id_data.areas[:].index(area)
    print("area index", index,
        "shading changed to", sh.type)


def register_shading_changes():
    for area in bpy.context.screen.areas:
        if area.type == 'VIEW_3D':
            sh = area.spaces.active.shading
            key = sh.path_resolve("type", False)
    
            bpy.msgbus.subscribe_rna(
                key=key,
                owner=bpy,
                args=(area,),
                notify=v3d_shading_notify
            )

if __name__ == "__main__":
    bpy.msgbus.clear_by_owner(bpy)
    register_shading_changes()
4 Likes

Some way to detect viewport matrix changes? (pan, zoom, rotate) like you did with shading type changing.

I don’t think it’s possible using rna for bpy_prop_array types.

But it’s possible to register a draw callback in the 3d view that simply compares view_matrix against a previous value and act upon that. Draw callbacks run in the main thread, and can leverage the context to determine which area changed.

Is not draw_callbacks a overkill?
Draw_callbacks don’t blocks auto save feature like the modal operators?

thank you.

It’s been nearly 2 months and I’ve asked on four forums, including #python on blender.chat with no luck at all. It appears message bus wasn’t designed to do what I want, at least not directly.