There is probably a more elegant way
Actually there is not. We can only append and prepend. If every stock entry was a draw callback in draw._draw_funcs
, it would be possible to insert inbetween. Until then, there are only two hacks, which I wouldn't recommend for everyday use (rather stick to
iKlsR's solution):
- Wrapping all UILayout calls - wiki link
- Injecting modified layout code the hard way - included in addon code
Note that 1. works for a single addon only at a time, and 2. only works as long as there's just one addon replacing the code object of a certain bpy type.
Content from Blender wiki, by ideasman42:
This page shows how to intercept every draw call in blenders python ui which can be used for some UI tricks.
First heres an example of how to do just that.
Tested to work with Blender 2.70
Intercept UI draw/poll
This is a simple example of how to intercept poll and draw functions, without making any behavioral changes.
classes = ["Panel", "Menu", "Header"]
def draw_override(func_orig, self_real, context):
print("override draw:", self_real)
ret = func_orig(self_real, context)
return ret
def poll_override(func_orig, cls, context):
print("override poll:", func_orig.__self__)
ret = func_orig(context)
return ret
import bpy
for cls_name in classes:
cls = getattr(bpy.types, cls_name)
for subcls in cls.__subclasses__():
if "draw" in subcls.__dict__: # dont want to get parents draw()
def replace_draw():
# function also serves to hold draw_orig in a local namespace
draw_orig = subcls.draw
def draw(self, context):
return draw_override(draw_orig, self, context)
subcls.draw = draw
replace_draw()
if "poll" in subcls.__dict__: # dont want to get parents poll()
def replace_poll():
# function also serves to hold poll_orig in a local namespace
poll_orig = subcls.poll
def poll(cls, context):
return poll_override(poll_orig, cls, context)
subcls.poll = classmethod(poll)
replace_poll()
Override layout & functions
This is the extended from the above code but extended to subclass the layout type used within draw().
In this example operators and properties are filtered out based on their names, but all sorts of things are possible with this - modifying args to functions, changing text etc.
classes = ["Panel", "Menu", "Header"]
import bpy
UILayout = bpy.types.UILayout
op_blacklist = [
"render.render",
"object.modifier_add",
"object.forcefield_toggle",
]
prop_blacklist = [
"Object.location",
"Object.scale",
"Object.rotation_euler",
"RenderSettings.display_mode",
]
def filter_operator(op_id):
if op_id in op_blacklist:
return False
return True
def filter_prop(data, prop):
prop_id = "%s.%s" % (data.__class__.__name__, prop)
if prop_id in prop_blacklist:
return False
return True
class OperatorProperties_FAKE:
pass
class UILayout_FAKE(bpy.types.UILayout):
__slots__ = ()
def __getattribute__(self, attr):
# ensure we always pass down UILayout_FAKE instances
if attr in ("row", "split", "column", "box", "column_flow"):
real_func = UILayout.__getattribute__(self, attr)
def dummy_func(*args, **kw):
print(" wrapped", attr)
ret = real_func(*args, **kw)
return UILayout_FAKE(ret)
return dummy_func
elif attr in ("operator", "operator_menu_enum", "operator_enum"):
real_func = UILayout.__getattribute__(self, attr)
def dummy_func(*args, **kw):
print(" wrapped", attr)
if filter_operator(args[0]):
ret = real_func(*args, **kw)
else:
# UILayout.__getattribute__(self, "label")()
# may need to be set
ret = OperatorProperties_FAKE()
return ret
return dummy_func
elif attr in ("prop", "prop_enum"):
real_func = UILayout.__getattribute__(self, attr)
def dummy_func(*args, **kw):
print(" wrapped", attr)
if filter_prop(args[0], args[1]):
ret = real_func(*args, **kw)
else:
ret = None
return ret
return dummy_func
else:
return UILayout.__getattribute__(self, attr)
print(self, attr)
def operator(*args, **kw):
print("OP")
return super().operator(*args, **kw)
def draw_override(func_orig, self_real, context):
if 1:
class Wrapper(self_real.__class__):
def __getattribute__(self, attr):
if attr == "layout":
ret = self_real.layout
return UILayout_FAKE(ret)
else:
return super().__getattr__(self, attr)
@property
def layout(self):
ret = self_real.layout
print("wrapped")
return ret
print(1)
self_wrap = Wrapper(self_real)
ret = func_orig(self_wrap, context)
else:
# simple, no wrapping
ret = func_orig(self_wrap, context)
return ret
def poll_override(func_orig, context):
ret = func_orig(context)
return ret
for cls_name in classes:
cls = getattr(bpy.types, cls_name)
for subcls in cls.__subclasses__():
if "draw" in subcls.__dict__: # dont want to get parents draw()
def replace_draw():
# function also serves to hold draw_old in a local namespace
draw_orig = subcls.draw
def draw(self, context):
return draw_override(draw_orig, self, context)
subcls.draw = draw
replace_draw()
if "poll" in subcls.__dict__: # dont want to get parents poll()
def replace_poll():
# function also serves to hold draw_old in a local namespace
poll_orig = subcls.poll
def poll(context):
return poll_override(poll_orig, context)
subcls.poll = classmethod(poll)
replace_poll()
Relevant code from my experimental addon to inject an own menu after the Templates menu below.
It essentially determines the path of the file which defines the original layout getattr(bpy.types, "TEXT_HT_header").draw.__code__.co_filename
, looks for a hard-coded piece of code row.menu("TEXT_MT_templates")
starting from …draw.__code__.co_firstlineno - 1
, gets hold of the draw function code and inserts its own piece of code which adds the custom menu (it has to adjust the indention to be valid).
Then it executes the new code (string) in isolation exec("".join(lines), {}, l)
with locals backed by an initially empty object l = {}
. The compiled draw function can then be accessed by l['draw']
and the original draw function can be substituted: getattr(bpy.types, "TEXT_HT_header").draw = l['draw']
.
A reference to the original draw function should be stored so that it can be restored if necessary.
class DrawFuncStore:
bpy_type = "TEXT_HT_header"
bpy_type_class = getattr(bpy.types, bpy_type)
draw = None
def insert_menu():
insert_after = 'row.menu("TEXT_MT_templates")'
insert_code = ' row.menu("TEXT_MT_snippets")\n' \
' from bpy.app.translations import pgettext_iface as iface_\n' # meh...
DrawFuncStore.draw = DrawFuncStore.bpy_type_class.draw
#module = bpy_type_class.__module__
filepath = DrawFuncStore.bpy_type_class.draw.__code__.co_filename
if filepath == "<string>":
return
try:
file = open(filepath, "r")
lines = file.readlines()
except:
append_menu()
return
line_start = DrawFuncStore.bpy_type_class.draw.__code__.co_firstlineno - 1
for i in range(line_start, len(lines)):
line = lines[i]
if not line[0].isspace() and line.lstrip()[0] not in ("#", "\n", "\r"):
break
line_end = i
# Unindent draw func by one level, since it won't sit inside a class
lines = [l[4:] for l in lines[line_start:line_end]]
for i, line in enumerate(lines, 1):
if insert_after in line:
#print("FOUND INSERT LINE")
lines.insert(i, insert_code)
break
else:
append_menu()
return
# Debug output
#f = open("D:\\s.txt", "w").writelines(lines)
l = {}
exec("".join(lines), {}, l)
#print(l)
#bpy_type_class.draw.__code__ = code_object # Doesn't work, since a single func is not a module
DrawFuncStore.bpy_type_class.draw = l['draw'] # exec defined our custom draw() func!
def append_menu():
bpy.types.TEXT_MT_templates.prepend(draw_item)
def remove_menu():
if DrawFuncStore.draw is not None:
DrawFuncStore.bpy_type_class.draw = DrawFuncStore.draw
DrawFuncStore.draw = None
else:
bpy.types.TEXT_MT_templates.remove(draw_item)