3
$\begingroup$

I have a few custom properties in my add-on to store state data of my job which is executed in the cloud.

class Properties(bpy.types.PropertyGroup):
    """Add-on properties."""
    # Job
    job_id: bpy.props.StringProperty(name="Id", default="", description="Id of the job",)
    job_title: bpy.props.StringProperty(name="Title", default="", description="Title of the job",)
    job_status: bpy.props.StringProperty(name="Status", default="", description="Status of the job",)
    job_progress: bpy.props.FloatProperty(name="Progress", default=0.0,  max=100.0, subtype="PERCENTAGE", description="Progress percentage of the job",)

When I execute a job from the add-on, I also run a separate thread that listen to my API using a websocket connection / GraphQL subscription. When the subscription receives a message, it calls a callback method (running in the separate thread) to updates the job progress and job status properties. Here are the methods.

class CRAFT_OT_subscribe_job(bpy.types.Operator):
    """Subscribe to a job"""
    bl_idname = "craft.subscribe_job"
    bl_label = "Subscribe Job"
    bl_options = {"REGISTER"}

    @staticmethod
    def update_job_progress(job):
        # Update the status and progress percentage of the job
        bpy.context.scene.craft_addon.job_status = job["status"]
        bpy.context.scene.craft_addon.job_progress = float(job["progress"])*100

        # Redraw panel
        for region in bpy.context.area.regions:
            if region.type == "UI":
                region.tag_redraw()
    
    def execute(self, context):
        # Subscribe job and output
        job_id = int(context.scene.craft_addon.job_id)
        client = bpy.app.driver_namespace.get("craftclient")
        client.subscribe_job(job_id=job_id, callback=self.update_job_progress) # This starts a new thread
        return {"FINISHED"}

The job progress and job status properties are updated properly but they are not refreshed in the N panel UI until user moves the mouse over it. I have tried to call tag_redraw() as mentioned in this answer but since my static method update_job_progress() runs in a separate thread, it triggers the error.

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

Any suggestion how I could force redraw the N panel or just the corresponding custom property from a separate thread?

$\endgroup$
4
  • $\begingroup$ Try context.area.tag_redraw() on your panel. $\endgroup$
    – brockmann
    Sep 18 at 7:58
  • $\begingroup$ I tried bpy.context.area.tag_redraw() ratther than context.area.tag_redraw() because I don't have the context in my callback method update_job_progress. I get the same error: AttributeError: 'NoneType' object has no attribute 'tag_redraw' $\endgroup$ Sep 18 at 12:21
  • 1
    $\begingroup$ Had some time to create a demo according to your requirements, see: pasteall.org/t98x/raw In short: you can use the update method of the float property and tag the area that way. Does that work for you? $\endgroup$
    – brockmann
    Sep 18 at 13:26
  • $\begingroup$ Thank you so much for looking into it @brockmann. I still faced the error for region in context.area.regions: AttributeError: 'NoneType' object has no attribute 'regions' with your method, most likely because contrariwise to your script, mine is multi-threaded. That said, it seems using the update method of the FloatProperty is enough to force the refresh of the UI, even if the method does nothing :) Don't you want to write an answer so that I vote it up? $\endgroup$ Sep 18 at 14:24
6
$\begingroup$

You can use the update method of the FloatProperty and tag the area that way:

def update_callback(self, context):
    for region in context.area.regions:
        if region.type == "UI":
            region.tag_redraw()
    return None

class MySettings(bpy.types.PropertyGroup):

    my_float: bpy.props.FloatProperty(
        update=update_callback)
    ...

enter image description here

Demo using the Operator Modal Timer template that comes with Blender:

import bpy

def ui_update(self, context):
    for region in context.area.regions:
        if region.type == "UI":
            region.tag_redraw()
    return None

class MySettings(bpy.types.PropertyGroup):

    my_float: bpy.props.FloatProperty(
        name="Float",
        description="Float property",
        default = 0,
        update=ui_update)


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 modal(self, context, event):
        if event.type in {'RIGHTMOUSE', 'ESC'}:
            self.cancel(context)
            return {'CANCELLED'}

        if event.type == 'TIMER':
            context.scene.my_tool.my_float += .1
        return {'PASS_THROUGH'}

    def execute(self, context):
        
        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)


class OBJECT_PT_panel(bpy.types.Panel):
    bl_label = "My Panel"
    bl_space_type = "VIEW_3D"   
    bl_region_type = "UI"
    bl_category = "Tools"
    bl_context = "objectmode" 

    def draw(self, context):
        layout = self.layout
        scene = context.scene
        mytool = scene.my_tool
        layout.prop(mytool, "my_float")
        layout.operator(ModalTimerOperator.bl_idname)


classes = (
    MySettings,
    ModalTimerOperator,
    OBJECT_PT_panel,
)

addon_keymaps = []

def register():
    from bpy.utils import register_class
    for cls in classes:
        register_class(cls)
        
    bpy.types.Scene.my_tool = bpy.props.PointerProperty(type=MySettings)

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

    del bpy.types.Scene.my_tool


if __name__ == "__main__":
    register()

Layout code ripped from: How to create a custom UI?

$\endgroup$
1
  • $\begingroup$ Perhaps a bpy.app.timer would emulate another thread better. A draw callback on the space that contains, while the timer is running, the panel could also be an option here. $\endgroup$
    – batFINGER
    Sep 18 at 17:38

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.