2
$\begingroup$

I was searching for a way to get notified when the user of my addon interact with for example a particle setting or texture setting. I found out about bpy.mgsbus but there’s literally too little info about it anywhere. Here’s what I found so far:

import bpy

handle = object()
sub_particles = bpy.types.ParticleSettings

def notify_test(*args):
    print("Notify changed!", args)

bpy.msgbus.subscribe_rna(key=sub_particles, owner=handle, args=(), notify=notify_test,)
bpy.msgbus.publish_rna  (key=sub_particles )

This code above is working fine for subscribing to any change user is making in any particle settings change, but how can we make it persistent, meaning being active when the addon is enabled. I am aware that an msgbus is not an handler. I saw an args options=set() from the function bpy.msgbus.subscribe_rna i'm doing some experimentation right now with options={"PERSISTENT"}.

The other issue I have is that the code bpy.msgbus.clear_by_owner didn't work for me as I didn't figure out what's in the args yet. So we could add as a question: How to remove the msgbus once set? and also we could add How to make sure that the msgbus is not active? as it seems there's also no lists of active msgbus available. Information we can get from the console:

#CLEAR BY OWNER
bpy.msgbus.clear_by_owner(owner)
.. function:: clear_by_owner(owner)
Clear all subscribers using this owner.

#PUBLISH RNA
bpy.msgbus.publish_rna(data, owner, args, notify)
.. function:: publish_rna(data, owner, args, notify)
:arg key: Represents the type of data being subscribed to
   Arguments include
   - :class:`bpy.types.Property` instance.
   - :class:`bpy.types.Struct` type.
   - (:class:`bpy.types.Struct`, str) type and property name.
:type key: Muliple
Notify subscribers of changes to this property
(this typically doesnt need to be called explicitly since 
changes will automatically publish updates).

#SUBSCRIBE RNA
bpy.msgbus.subscribe_rna(data, owner, args, notify, options=set()
.. function:: subscribe_rna(data, owner, args, notify, options=set())
:arg key: Represents the type of data being subscribed to
   Arguments include
   - :class:`bpy.types.Property` instance.
   - :class:`bpy.types.Struct` type.
   - (:class:`bpy.types.Struct`, str) type and property name.
:type key: Muliple
:arg owner: Handle for this subscription (compared by identity).
:type owner: Any type.
:arg options: Change the behavior of the subscriber.
   - ``PERSISTENT`` when set, the subscriber will be kept when remapping ID data.
:type options: set of str.
Returns a new vector int property definition.
$\endgroup$
2
3
$\begingroup$

The PERSISTENT option doesn't make the subscription remain active when loading files. It is for ensuring that the subscriber remains registered when remapping ID data. Subscriptions will be removed when loading a new file. Persistent application handler appended to bpy.app.handlers.load_post can be used to call bpy.msgbus.subscribe_rna() when a new file is loaded.

bpy.msgbus.clear_by_owner() requires the owner that you've also passed to bpy.msgbus.subscribe_rna() as argument. It will clear all subscribers by this owner.

Below is an add-on that demonstrates the use of the msgbus. It creates a panel "msgbus" with two operators that allow to register/unregister a handler for the message bus. Once registered, whenever the active object changes, it prints information about it to the terminal/console.

Add-on panel

bl_info = {
    "name": "Example add-on persistent msgbus",
    "author": "Robert Guetzkow",
    "version": (1, 0),
    "blender": (2, 81, 0),
    "location": "View3D > Sidebar > My own addon",
    "description": "Example add-on that uses msgbus",
    "warning": "",
    "wiki_url": "",
    "category": "3D View"}

import bpy
from bpy.app.handlers import persistent


# For more information see source/blender/python/intern/bpy_msgbus.c
# and https://developer.blender.org/P563


# Object that will store the handle to the msgbus subscription
subscription_owner = object()


def notification_handler(*args):
    print(f"Object: {bpy.context.object.name}, Location: {bpy.context.object.location}, Args: {args}")


def subscribe_active_obj(subscription_owner):
    # What the subscription is for, in this example the active object.
    # Note that for properties it may be necessary to use `path_resolve`
    # as shown in https://developer.blender.org/P563
    subscribe_to = bpy.types.LayerObjects, "active"

    # Subscribe to the msgbus to call the handler when the key is modified.
    # Please note that not every UI interaction will publish an update,
    # despite modifying the key you've subscribed to. For instance, when
    # subscribed to `bpy.types.Object, "location"`, changing the location of 
    # an object through the move tool won't call the handler automatically, 
    # while updating the location property in the "Tranform" panel in the 
    # sidebar will. The publishing of an update can be forced through 
    # `bpy.msgbus.publish_rna()`.
    # The "PERSISTENT" option ensures that the subscriber remains registered
    # when remapping ID data, it does not mean that the subscription remains 
    # persistent when loading a different file. This can be accomplished with
    # a persistent application handler that is called on file load.
    bpy.msgbus.subscribe_rna(
        key=subscribe_to,
        owner=subscription_owner,
        args=("a", "b", "c"),
        notify=notification_handler,
        options={"PERSISTENT",}
    )

    # Register the persistent handler, ensures that the subscription will happen 
    # when a new file is loaded.
    if load_handler not in bpy.app.handlers.load_post:
        bpy.app.handlers.load_post.append(load_handler)


def unsubscribe_active_obj(subscription_owner):
    # Clear all subscribers by this owner
    if subscription_owner is not None:
        bpy.msgbus.clear_by_owner(subscription_owner)

    # Unregister the persistent handler.
    if load_handler in bpy.app.handlers.load_post:
        bpy.app.handlers.load_post.remove(load_handler)


@persistent
def load_handler(dummy):
    subscribe_active_obj(subscription_owner)


class SCENE_OT_msgbus_register_active_obj(bpy.types.Operator):
    bl_idname = "scene.msgbus_register_active_obj"
    bl_label = "Register Handler"
    bl_description = "Register msbus handler for the active object."
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        subscribe_active_obj(subscription_owner)
        return {"FINISHED"}


class SCENE_OT_msgbus_unregister_active_obj(bpy.types.Operator):
    bl_idname = "scene.msgbus_unregister_active_obj"
    bl_label = "Unregister Handler"
    bl_description = "Unregister msbus handler for the active object."
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):     
        unsubscribe_active_obj(subscription_owner)
        return {"FINISHED"}


class EXAMPLE_PT_panel(bpy.types.Panel):
    bl_label = "msgbus"
    bl_category = "Active Object"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"

    def draw(self, context):
        layout = self.layout
        layout.operator(SCENE_OT_msgbus_register_active_obj.bl_idname)
        layout.operator(SCENE_OT_msgbus_unregister_active_obj.bl_idname)


classes = (EXAMPLE_PT_panel, 
           SCENE_OT_msgbus_register_active_obj, 
           SCENE_OT_msgbus_unregister_active_obj)


def register():
    for cls in classes:
        bpy.utils.register_class(cls)

def unregister():
    for cls in classes:
        bpy.utils.unregister_class(cls)

    # Unsubscribe and remove handle
    unsubscribe_active_obj(subscription_owner)


if __name__ == "__main__":
    register()

Please note that the message bus doesn't notify for every kind of update.

The message bus system is triggered by updates via the RNA system. This means that the following updates will result in a notification on the message bus:

  • Changes via the Python API, for example some_object.location.x += 3.
  • Changes via the sliders, fields, and buttons in the user interface.

The following updates do not trigger message bus notifications:

  • Moving objects in the 3D Viewport.
  • Changes performed by the animation system.

The implementation of the message bus can found in bpy_msgbus.c.

$\endgroup$

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.