Using new layout.progress() function in 4.0

Hello, I was wondering how you are actually meant to implement the new layout.progress() reference introduced to the API in 4.0? There aren’t any examples provided, and since it is stored in UILayout it isn’t self explanatory how you would use it in an operator. I’ve attempted a few things but UILayout isn’t seemingly directly exposed to operators so I’m unsure how to interact with the function.

Here is the PR and API Reference:

Any help is appreciated, thanks!

1 Like

Thanks for bringing this up! I would also be interested in knowing how to use it! :slightly_smiling_face:

Seems pretty straightforward to me,

layout.progress(factor = 0.66, type = 'BAR')

where factor is the amount of progress that is filled in.
Like other layout functions you can use a custom property to drive it, so you could create a FloatProperty and just pass that in directly. IE)

bpy.types.Object.my_progress = bpy.props.FloatProperty()

and then in your draw function:

layout.progress(factor = context.active_object.my_progress, type = 'BAR')

then anytime your custom property was updated, the value of the progress bar would change.

I’m going to assume that this progress bar isn’t multithreaded and would be blocked by processing that is happening on the main thread, so I’m not 100% sure what the use case is for it. for example, it would make sense to have a progress bar for an exporter or importer, but given that all of that data crunching happens on the main thread I would imagine this progress bar will stall out at 0% and then magically appear at 100% when the heavy lift is finished. Hope that’s not the case, but the rest of the UI works that way so I’m not very optimistic that this is any different.

3 Likes

Thanks for this, I’m curious, should the draw function just be called by default? Where in the process of calling the Operator does that happen? My question makes the assumption that our hypothetical implementation is a simple long process and we want to give progression updates.

Not entirely related but if we can’t get easy multithreaded ui updates for Blender I wonder how hard it could be to implement a Qt processEvents style function that processes all of the events up to that point? It would refresh the UI and go back to a frozen state until the event ends or processEvents is once again processed. That would be obviously a slower implementation compared to something multithreaded but genuinely useful considering we have nothing else for non-baking/rendering and sometimes I/O tools. Oh well.

Layouts are only used in panels. An operator has a draw function for the redo panel

Yeah, that’s what my assumption was too… Thanks @testure, I appreciate the help. This loading progress thing is nice, I wish it were better supported. One step closer!

FWIW here’s a minimal working example that displays a progress bar filling in the UI. I used a modal operator and a timer but it can be driven by any property. You’ll need Version 4.0 and bpy.types.UILayout.progress. You’re kind of obligated to use a modal operator since a regular operator’s execution will block updating the UI until it finishes. Note autosave is disabled while a modal operator runs.

enter image description here

import bpy


class ModalTimerOperator(bpy.types.Operator):
    bl_idname = "wm.modal_timer_operator"
    bl_label = "Modal Timer Operator"

    _timer = None

    def modal(self, context, event):
        [a.tag_redraw() for a in context.screen.areas]
        if self._timer.time_duration > 3:
            context.window_manager.progress = 1
            return {'FINISHED'}
        context.window_manager.progress = self._timer.time_duration / 3
        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 progress_bar(self, context):
    row = self.layout.row()
    row.progress(
        factor=context.window_manager.progress,
        type="BAR",
        text="Operation in progress..." if context.window_manager.progress < 1 else "Operation Finished !"
    )
    row.scale_x = 2
    

def register():
    bpy.types.WindowManager.progress = bpy.props.FloatProperty()
    bpy.utils.register_class(ModalTimerOperator)
    bpy.types.TEXT_HT_header.append(progress_bar)


if __name__ == "__main__":
    register()
    bpy.ops.wm.modal_timer_operator()
6 Likes

I’m a novice whose sole experience is in doing “conventional” add-on operators, some of which loop through myriad objects and textures that can take quite long to process. I usually report their progress through print() to the console.

What would be the simplest way to refresh such a progress bar in an UI panel? Would it require converting the operator to Modal type, or are there easier ways to induce an UI update (for example, each 5% progress increment while inside a loop?

(Sorry if this is a very newbie question :sweat:)

The only known techniques as far as I know are described in this Q&A over on BSE and I used the first one in my previous post.

TLDR : use bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)

Here’s the second technique using a non-modal operator. You’ll notice your script takes significantly longer if you update the UI a lot. I suggest doing it sparsingly. Also the cursor flickers a lot, at least on windows. Credits to @Chebhou.

animation

import bpy


class ModalTimerOperator(bpy.types.Operator):
    bl_idname = "wm.timer_operator"
    bl_label = "Timer Operator"

    def execute(self, context):
        progress_bar.progress = 0
        for i in range(100):
            progress_bar.progress += 0.01
            bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
            bpy.ops.mesh.primitive_cube_add(size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
        return {'FINISHED'}


def progress_bar(self, context):
    row = self.layout.row()
    row.progress(
        factor=progress_bar.progress,
        type="BAR",
        text="Operation in progress..." if progress_bar.progress < 1 else "Operation Finished !"
    )
    row.scale_x = 2

progress_bar.progress = 0
    

def register():
    bpy.utils.register_class(ModalTimerOperator)
    bpy.types.TEXT_HT_header.append(progress_bar)


if __name__ == "__main__":
    register()
    bpy.ops.wm.timer_operator()
1 Like

I see. Thank you. I’ll explore both methods, as in my use case we are talking between 1 to 30/40 minutes of progression, where fairly spaced updates would be enough.

Hi, I have used your suggested method by defining:

import bpy
from bpy.types import Panel

bpy.types.Object.my_progress = bpy.props.FloatProperty()
bpy.types.Object.my_progress = 0
(...)

in the header of my panel.

In the operator I am updating the value

    def execute(self, context) -> Set[str]:
        bpy.types.Object.my_progress = 1

The value changes, however the UI doesn’t get automatically updated. Do I need to program a area tag refresh?

edit: yes, that works. Unless somebody knows a more elegant variant?

from this stack overflow thread

I snagged this function and call it whenever needed

def refresh_all_areas():
    for wm in bpy.data.window_managers:
        for w in wm.windows:
            for area in w.screen.areas:
                area.tag_redraw()

That works at least at the end of the process…but not during it :confused:

Another Edit:
Solution (for me)
I just used the complete modal example of that stack overflow example and it works great. Both with the operator of that example and with the New Type Progress Bar!