Research over this subject :
I have this but it seems that working in modal do not update the gui correctly between the tasks and it might freeze the interface anyway
so this should work in theory, but in practice it seems that there's few flaws because on how blender handle the freezing process.
(that perhaps could be resolved by using bpy.app.timers ?)
This technique consist of segmenting your code into multiple function, that you store in a dictionary of operations, then a modal operator will loop over your operations while updating the progress bar depending on you steps achievement.
import bpy
import time
#Operation Dict
#operation will need to be divided in multiple steps
sleep = 0
def _00():
print("f00")
time.sleep(sleep)
def _01():
print("f01")
time.sleep(sleep)
def _02():
print("f02")
time.sleep(sleep)
def _03():
print("f03")
time.sleep(sleep)
def _04():
print("f04")
time.sleep(sleep)
Operations = {
0:_00,
1:_01,
2:_02,
3:_03,
4:_04,
}
#Modal Operator
class EXAMPLE_OT_modal_operator(bpy.types.Operator):
bl_idname = "example.modal_operator"
bl_label = "Modal Operator"
step : bpy.props.IntProperty()
def modal(self, context, event):
global Operations
max_step = len(Operations.keys())
#Update Gui
context.object.progress = self.step/max_step*100
context.area.tag_redraw()
context.area.tag_redraw()
#Running Operations Steps In Order
if self.step < max_step:
print(f"Operations[{self.step}]()")
Operations[self.step]()
self.step += 1
return {'RUNNING_MODAL'}
#No More steps, Restore and Done
print("Finished")
self.step = 0
context.object.progress = 0
return {'FINISHED'}
def invoke(self, context, event):
print("Invoke")
#self.execute(context)
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
#Panel
class EXAMPLE_PT_panel(bpy.types.Panel):
bl_idname = "EXAMPLE_PT_panel"
bl_label = "Example"
bl_category = "Example"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_context = "objectmode"
def draw(self, context):
layout = self.layout
obj = context.object
if obj.progress:
layout.prop(bpy.context.object,"progress")
else:
ope = layout.row()
ope.operator_context = "INVOKE_DEFAULT"
ope.operator("example.modal_operator",text="Run Modal Operator")
return
#Registering Stuff...
bpy.types.Object.progress = bpy.props.FloatProperty(
name="Progress",
subtype="PERCENTAGE",
soft_min=0,
soft_max=100,
precision=0,
)
bpy.utils.register_class(EXAMPLE_PT_panel)
bpy.utils.register_class(EXAMPLE_OT_modal_operator)
Here is another test, same principle but this time using delayed function hoping that the delay between the operations might be enough to let blender "breath" so we can get a glimpse of the interface. it seems that this attempt is also failing and that blender interface simply like to keep in freeze state even if is is not supposed to
- note that this technique might be limiting as execution context within a delay function is highly limiting
Not sure if my research are of any help.
I'll try to explore more solutions sooner or later
import bpy
import time
import functools
#Operation Dict
#your operation work steps should divided into multiple function
sleep = 0
def _00():
print("f00")
time.sleep(sleep)
def _01():
print("f01")
time.sleep(sleep)
def _02():
print("f02")
time.sleep(sleep)
def _03():
print("f03")
time.sleep(sleep)
def _04():
print("f04")
time.sleep(sleep)
#then you store then all in a dict like this
Operations = {
0:_00,
1:_01,
2:_02,
3:_03,
4:_04,
}
#We need a function that refresh all areas
#we need this because we can't access context.area from a timer function
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()
#Then we run all steps of our Operation Dict in this operator
#the goal of this operator is to run the function from timers
#We need to run these from timers otherwise the interface might be locked and freeze when calculating
class EXAMPLE_OT_timer_operator(bpy.types.Operator):
bl_idname = "example.timer_operator"
bl_label = "Timer Operator"
interval : bpy.props.FloatProperty(default=0.1)
def execute(self, context):
global Operations
max_step = len(Operations.keys())
for i,(step,operation) in enumerate(Operations.items()):
i+=1
def generate_delay_fct(stp,):
"""fct factory needed otherwise same function for all timer"""
def fct():
"""delay is needed, so interface don't freeze and we have time to display progress bar"""
print("execute_op_with_delay ->"+str(stp))
Operations[stp]()
#Update Gui
bpy.context.object.progress = (stp/max_step)*100
refresh_all_areas()
return None
return fct
bpy.app.timers.register( generate_delay_fct(step,), first_interval=self.interval*i,)
continue
def last_step():
"""one last function is required in orter to restore the progress bar to 0 once all operations are done"""
bpy.context.object.progress = 0
refresh_all_areas()
return None
bpy.app.timers.register( last_step, first_interval=self.interval*(i+1),)
return {'FINISHED'}
#Panel
class EXAMPLE_PT_panel(bpy.types.Panel):
bl_idname = "EXAMPLE_PT_panel"
bl_label = "Example"
bl_category = "Example"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_context = "objectmode"
def draw(self, context):
layout = self.layout
obj = context.object
if obj.progress:
layout.prop(bpy.context.object,"progress")
else:
ope = layout.row()
ope.operator_context = "INVOKE_DEFAULT"
ope.operator("example.timer_operator",text="Run Timer Operator")
return
#Registering Stuff...
bpy.types.Object.progress = bpy.props.FloatProperty(
name="Progress",
subtype="PERCENTAGE",
soft_min=0,
soft_max=100,
precision=0,
)
bpy.utils.register_class(EXAMPLE_PT_panel)
bpy.utils.register_class(EXAMPLE_OT_timer_operator)
Solution Found
It seems that the first prototype i did was ok, and the second idea of timers was a good lead, what i needed in the end is an hybrid solution between the two.
by using a timer such as event_timer_add(), we could check for this timer in our events, then have a guaranteed unfreeze at this moment. But that's not enough, we need to extend this timeframe by just a bit! that's why i added a counter timer_count
that will ignore the first 10 modal loops
in the end this is working quite fine
import bpy
import time
#Operation Dict
#operation will need to be divided in multiple steps
sleep = 1
def _00():
print("f00")
time.sleep(sleep)
def _01():
print("f01")
time.sleep(sleep)
def _02():
print("f02")
time.sleep(sleep)
def _03():
print("f03")
time.sleep(sleep)
def _04():
print("f04")
time.sleep(sleep)
Operations = {
"First Step":_00,
"Second Step":_01,
"Running Stuff":_02,
"Wait a minute":_03,
"There's a problem":_04,
"ah no it's ok":_04,
"we are done":_04,
}
#Modal Operator
class EXAMPLE_OT_modal_operator(bpy.types.Operator):
bl_idname = "example.modal_operator"
bl_label = "Modal Operator"
def __init__(self):
self.step = 0
self.timer = None
self.done = False
self.max_step = None
self.timer_count = 0 #timer count, need to let a little bit of space between updates otherwise gui will not have time to update
def modal(self, context, event):
global Operations
#update progress bar
if not self.done:
print(f"Updating: {self.step+1}/{self.max_step}")
#update progess bar
context.object.progress = ((self.step+1)/(self.max_step))*100
#update label
context.object.progress_label = list(Operations.keys())[self.step]
#send update signal
context.area.tag_redraw()
#by running a timer at the same time of our modal operator
#we are guaranteed that update is done correctly in the interface
if event.type == 'TIMER':
#but wee need a little time off between timers to ensure that blender have time to breath, so we have updated inteface
self.timer_count +=1
if self.timer_count==10:
self.timer_count=0
if self.done:
print("Finished")
self.step = 0
context.object.progress = 0
context.window_manager.event_timer_remove(self.timer)
context.area.tag_redraw()
return {'FINISHED'}
if self.step < self.max_step:
#run step function
list(Operations.values())[self.step]()
self.step += 1
if self.step==self.max_step:
self.done=True
return {'RUNNING_MODAL'}
return {'RUNNING_MODAL'}
def invoke(self, context, event):
print("")
print("Invoke")
#terermine max step
global Operations
if self.max_step == None:
self.max_step = len(Operations.keys())
context.window_manager.modal_handler_add(self)
#run timer
self.timer = context.window_manager.event_timer_add(0.1, window=context.window)
return {'RUNNING_MODAL'}
#Panel
class EXAMPLE_PT_panel(bpy.types.Panel):
bl_idname = "EXAMPLE_PT_panel"
bl_label = "Example"
bl_category = "Example"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_context = "objectmode"
def draw(self, context):
layout = self.layout
obj = context.object
if obj.progress:
progress_bar = layout.row()
progress_bar.prop(bpy.context.object,"progress")
progress_lbl = layout.row()
progress_lbl.active = False
progress_lbl.label(text=bpy.context.object.progress_label)
else:
ope = layout.row()
ope.operator_context = "INVOKE_DEFAULT"
ope.operator("example.modal_operator",text="Run Modal Operator")
return
#Registering Stuff...
bpy.types.Object.progress = bpy.props.FloatProperty( name="Progress", subtype="PERCENTAGE",soft_min=0, soft_max=100, precision=0,)
bpy.types.Object.progress_label = bpy.props.StringProperty()
bpy.utils.register_class(EXAMPLE_PT_panel)
bpy.utils.register_class(EXAMPLE_OT_modal_operator)
Prop to sybren from the Grove3d, i did dig a little bit to discover his logic