Operator is invalid, callback isn't removed
At issue here is the draw callback belonging to the space is persistent when another file is opened, whereas a modal operator is not. The solution below removes the draw callback if the operator becomes invalid.
Asked
Removing space draw callbacks after losing the handle?
with relation to having issues like in question. If the handle is attached to the operator and the operator is "lost" there is no way to remove the draw callback. Well no one has answered how to .
If you were to put in the line
print(repr(self))
into your draw call back method,
All going well, and run from text editor (main thread)
<class '__main__.Test_operator'>
if the operator is removed will see
<bpy_struct, Test_operator invalid>
Once this is the case error shown in question will begin. Without going into great details the "rna" is how blender objects store their properties under the hood. When the instance of the blender operator is no longer required its rna is trashed.
Using repr
avoids using a try catch clause. Would be keen to find out if there is a nicer way to check for this.
As a workaround to resolve issue of lost handle, when using operator as an argument, and owner of properties to draw, could set up a class variable list of operators and handles. When
repr(op).endswith("invalid>")
is true, remove its callback via handle, remove from handles... and avoid the error message.
Test script. Have made a class method to check the validity, and validate the list.
import bpy, blf
class Test_operator(bpy.types.Operator):
bl_idname = "object.test_operator"
bl_label = "test"
handles = []
@classmethod
def validate(cls):
invalids = [(op, handle) for op, handle in cls.handles if repr(op).endswith("invalid>")]
valid = not(invalids)
while invalids:
op, handle = invalids.pop()
bpy.types.SpaceView3D.draw_handler_remove(handle, 'WINDOW')
cls.handles.remove((op, handle))
return valid
@staticmethod
def draw_callback_px(self, context):
font_id = 0 # XXX, need to find out how best to get this.
# check handles
if not Test_operator.validate():
return
blf.position(font_id, 15, 30, 0)
blf.size(font_id, 50, 72)
blf.draw(font_id, self.s)
def modal(self, context, event):
"some code..."
return {'PASS_THROUGH'}
def invoke(self, context, event):
self.s = "hello"
args = (self, context)
self.handles.append((self, bpy.types.SpaceView3D.draw_handler_add(
Test_operator.draw_callback_px, args, 'WINDOW', 'POST_PIXEL'
)))
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
def register():
bpy.utils.register_class(Test_operator)
def unregister():
bpy.utils.unregister_class(Test_operator)
if __name__ == "__main__":
register()
Window Manager
IMO would instead make a drawing class, similarly to link atop answer, or as a mixin class or as a wrapper for the callback.
Adding further to How do you remove a draw handler after it's been added? can leverage the window manager to keep a context reference to the draw handler classes.
Here is a quick cobble together by I think you can see where this is going. Can communicate with the draw callback via context from your operator when it is run again.
import bpy
from bpy.types import PropertyGroup
from bpy.props import PointerProperty
import blf
class DrawingClass:
def __init__(self, context, prop):
self.prop = prop
self.handle = bpy.types.SpaceView3D.draw_handler_add(
self.draw_text_callback,(context,),
'WINDOW', 'POST_PIXEL')
def draw_text_callback(self, context):
font_id = 0 # XXX, need to find out how best to get this.
# draw some text
blf.position(font_id, 15, 50, 0)
blf.size(font_id, 20, 72)
blf.draw(font_id, "%s %s" % (context.scene.name, self.prop))
def remove_handle(self):
bpy.types.SpaceView3D.draw_handler_remove(self.handle, 'WINDOW')
class DrawCallBacks(PropertyGroup):
_dc = []
def add(self, cb):
self._dc.append(cb)
def register():
bpy.utils.register_class(DrawCallBacks)
bpy.types.WindowManager.callbacks = PointerProperty(type=DrawCallBacks)
if __name__ == "__main__":
register()
# test
wm = bpy.context.window_manager
wm.callbacks.add(DrawingClass(bpy.context, "FoogleDoodle"))
Can open a new file go to console
>>> C.window_manager.callbacks._dc
[<__main__.DrawingClass object at 0x7fed5b609a30>]
>>> for cb in C.window_manager.callbacks._dc:
... cb.prop
... cb.prop = "MumbleGrunt"
...
'FoogleDoodle'
and the callback prints "MumbleGrunt" to screen.