However there seems to be no way to add such a progress indicator for other purposes.
Now the menu bar at the top is actually the header of an area within the Info editor, so I tried to add a Panel to this header by specifying
'HEADER'
for its bl_region_type
. That didn't work: no errors but no visible header either.So after some digging around I came up with a different approach: replacing the
draw()
method of the Info header. After all, everything is Python and being a truly dynamic language means we can monkey patch anything.Basically we get the original
draw()
method, replace it with our own and call the original again. After this call we add a scene property to the layout of the header and use this float property to signal progress. The result looks like this:The relevant code looks like this:
# update function to tag all info areas for redraw def update(self, context): areas = context.window.screen.areas for area in areas: if area.type == 'INFO': area.tag_redraw() # a variable where we can store the original draw funtion info_header_draw = lambda s,c: None def register(): # a value between [0,100] will show the slider Scene.progress_indicator = FloatProperty( default=-1, subtype='PERCENTAGE', precision=1, min=-1, soft_min=0, soft_max=100, max=101, update=update) # the label in front of the slider can be configured Scene.progress_indicator_text = StringProperty( default="Progress", update=update) # save the original draw method of the Info header global info_header_draw info_header_draw = bpy.types.INFO_HT_header.draw # create a new draw function def newdraw(self, context): global info_header_draw # first call the original stuff info_header_draw(self, context) # then add the prop that acts as a progress indicator if (context.scene.progress_indicator >= 0 and context.scene.progress_indicator <= 100) : self.layout.separator() text = context.scene.progress_indicator_text self.layout.prop(context.scene, "progress_indicator", text=text, slider=True) # replace it bpy.types.INFO_HT_header.draw = newdraw
The
register()
function defines two new scene properties: progress_indicator
to hold a value in the range [0,100]
which will be shown to indicate progress and progress_indicator_text
to hold a configurable label. They refer to an update()
function that will be called every time that the value of the property is changed. The update()
function just tags any area the is an INFO editor (theoretically there could be more than one) for redraw which will cause the draw()
method to be called for any of its regions, including the header region.Line 30 stores a reference to the original
draw()
method of the the Info header. Next we define a new method newdraw()
that will call the original draw()
method (line 36) and then add the new scene property progress_indicator
but only if it has a value between zero and 100.The new function is then used to replace the existing draw function.
How to use the progress indicator
Long running operations are probably best implemented as modal operators and using the progress indicator from a modal operator is very simple. An example of such an operator is shown below (which also starts a timer that will send timer events to the modal operator. The operator will stop after 9 timer ticks and update the progress indicator on each tick. After the final tick it will set the value to 101 which will stop the progress indicator from being displayed:class TestProgressModal(bpy.types.Operator): bl_idname = 'scene.testprogressmodal' bl_label = 'Test Progress Modal' bl_options = {'REGISTER'} def modal(self, context, event): if event.type == 'TIMER': self.ticks += 1 if self.ticks > 9: context.scene.progress_indicator = 101 # done context.window_manager.event_timer_remove(self.timer) return {'CANCELLED'} context.scene.progress_indicator = self.ticks*10 return {'RUNNING_MODAL'} def invoke(self, context, event): self.ticks = 0 context.scene.progress_indicator_text = "Heavy modal job" context.scene.progress_indicator = 0 wm = context.window_manager self.timer = wm.event_timer_add(1.0, context.window) wm.modal_handler_add(self) return {'RUNNING_MODAL'}
It is possible to update the progress indicator from a long running non-modal's
execute()
method as well but although the update functions associated with the scene properties will be called and hence the area tagged for redraw, an actual redraw is only initiated after the operator finishes. There is a way around with a documented but unsupported hack as shown in the code below (line 13):class TestProgress(bpy.types.Operator): bl_idname = 'scene.testprogress' bl_label = 'Test Progress' bl_options = {'REGISTER'} def execute(self, context): context.scene.progress_indicator_text = "Heavy job" context.scene.progress_indicator = 0 for tick in range(10): sleep(1) # placeholder for heavy work context.scene.progress_indicator = tick*10 # see https://docs.blender.org/api/current/info_gotcha.html bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) context.scene.progress_indicator = 101 # done return {"FINISHED"}
why are you running a function inside the register?
ReplyDeleteNot quite sure what you mean: I am not running a function inside the register function but I do define a new one newdraw() that I then use to replace the original draw function of the INFO_MT_header. I am not sure this will work in 2.92
DeleteI do see newdraw inside the register, is that correct?
DeleteIm trying to get this to work in 2.91 and it keeps failing>
Keep returning this error
bpy_struct.__new__(type): expected a single argument
I tried change INFO_MT_header to VIEW3D_HT_tool_header with i do see the progressbar. Im not sure where INFO_MT_Header should show it, i dont see it with that
Deleteit should show up in the right hand side of the header, above the gizmo's . Maybe you forgot to change the update() function? because the correct area needs to be tagged for redraw. Anyway, I updated the code and tested it with 2.92. I documented the changes in a small blog post https://blog.michelanders.nl/2021/05/progress-indicator-updated.html
DeleteI saw the newdraw function inside the register function.
ReplyDeleteThank you so much for posting this! I know this code is 5+ years old at this point, but it's still extremely helpful. Let's hope that Blender makes this easier at some point in the future!
ReplyDelete