4

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?

brockmann
  • 12,613
  • 4
  • 50
  • 93
  • Try context.area.tag_redraw() on your panel. – brockmann Sep 18 '21 at 07:58
  • 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' – Alexis.Rolland Sep 18 '21 at 12:21
  • 1
    Had some time to create a demo according to your requirements, see: https://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? – brockmann Sep 18 '21 at 13:26
  • 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? – Alexis.Rolland Sep 18 '21 at 14:24

1 Answers1

9

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?

brockmann
  • 12,613
  • 4
  • 50
  • 93
  • 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. – batFINGER Sep 18 '21 at 17:38