4
$\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
    Commented Sep 18, 2021 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$ Commented Sep 18, 2021 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
    Commented Sep 18, 2021 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$ Commented Sep 18, 2021 at 14:24

1 Answer 1

9
$\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
    Commented Sep 18, 2021 at 17:38

You must log in to answer this question.

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