forked from bartvdbraak/blender
more files
This commit is contained in:
parent
cc2c57b793
commit
a91fdd61dc
201
release/scripts/modules/animsys_refactor.py
Executable file
201
release/scripts/modules/animsys_refactor.py
Executable file
@ -0,0 +1,201 @@
|
|||||||
|
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU General Public License
|
||||||
|
# as published by the Free Software Foundation; either version 2
|
||||||
|
# of the License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software Foundation,
|
||||||
|
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
# ##### END GPL LICENSE BLOCK #####
|
||||||
|
|
||||||
|
# <pep8 compliant>
|
||||||
|
|
||||||
|
"""
|
||||||
|
This module has utility functions for renaming
|
||||||
|
rna values in fcurves and drivers.
|
||||||
|
|
||||||
|
The main function to use is: update_data_paths(...)
|
||||||
|
"""
|
||||||
|
|
||||||
|
IS_TESTING = False
|
||||||
|
|
||||||
|
class DataPathBuilder(object):
|
||||||
|
__slots__ = ("data_path", )
|
||||||
|
""" Dummy class used to parse fcurve and driver data paths.
|
||||||
|
"""
|
||||||
|
def __init__(self, attrs):
|
||||||
|
self.data_path = attrs
|
||||||
|
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
str_value = ".%s" % attr
|
||||||
|
return DataPathBuilder(self.data_path + (str_value, ))
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
str_value = '["%s"]' % key
|
||||||
|
return DataPathBuilder(self.data_path + (str_value, ))
|
||||||
|
|
||||||
|
def resolve(self, real_base, rna_update_from_map=None):
|
||||||
|
""" Return (attribute, value) pairs.
|
||||||
|
"""
|
||||||
|
pairs = []
|
||||||
|
base = real_base
|
||||||
|
for item in self.data_path:
|
||||||
|
if base is not Ellipsis:
|
||||||
|
try:
|
||||||
|
# this only works when running with an old blender
|
||||||
|
# where the old path will resolve
|
||||||
|
base = eval("base" + item)
|
||||||
|
except:
|
||||||
|
base_new = Ellipsis
|
||||||
|
# guess the new name
|
||||||
|
if item.startswith("."):
|
||||||
|
for item_new in rna_update_from_map.get(item[1:], ()):
|
||||||
|
try:
|
||||||
|
print("base." + item_new)
|
||||||
|
base_new = eval("base." + item_new)
|
||||||
|
break # found, dont keep looking
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if base_new is Ellipsis:
|
||||||
|
print("Failed to resolve data path:", self.data_path)
|
||||||
|
base = base_new
|
||||||
|
|
||||||
|
pairs.append((item, base))
|
||||||
|
return pairs
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
|
||||||
|
def id_iter():
|
||||||
|
type_iter = type(bpy.data.objects)
|
||||||
|
|
||||||
|
for attr in dir(bpy.data):
|
||||||
|
data_iter = getattr(bpy.data, attr, None)
|
||||||
|
if type(data_iter) == type_iter:
|
||||||
|
for id_data in data_iter:
|
||||||
|
if id_data.library is None:
|
||||||
|
yield id_data
|
||||||
|
|
||||||
|
|
||||||
|
def anim_data_actions(anim_data):
|
||||||
|
actions = []
|
||||||
|
actions.append(anim_data.action)
|
||||||
|
for track in anim_data.nla_tracks:
|
||||||
|
for strip in track.strips:
|
||||||
|
actions.append(strip.action)
|
||||||
|
|
||||||
|
# filter out None
|
||||||
|
return [act for act in actions if act]
|
||||||
|
|
||||||
|
|
||||||
|
def classes_recursive(base_type, clss=None):
|
||||||
|
if clss is None:
|
||||||
|
clss = [base_type]
|
||||||
|
else:
|
||||||
|
clss.append(base_type)
|
||||||
|
|
||||||
|
for base_type_iter in base_type.__bases__:
|
||||||
|
if base_type_iter is not object:
|
||||||
|
classes_recursive(base_type_iter, clss)
|
||||||
|
|
||||||
|
return clss
|
||||||
|
|
||||||
|
|
||||||
|
def find_path_new(id_data, data_path, rna_update_dict, rna_update_from_map):
|
||||||
|
# ignore ID props for now
|
||||||
|
if data_path.startswith("["):
|
||||||
|
return data_path
|
||||||
|
|
||||||
|
# recursive path fixing, likely will be one in most cases.
|
||||||
|
data_path_builder = eval("DataPathBuilder(tuple())." + data_path)
|
||||||
|
data_resolve = data_path_builder.resolve(id_data, rna_update_from_map)
|
||||||
|
|
||||||
|
path_new = [pair[0] for pair in data_resolve]
|
||||||
|
|
||||||
|
# print(data_resolve)
|
||||||
|
data_base = id_data
|
||||||
|
|
||||||
|
for i, (attr, data) in enumerate(data_resolve):
|
||||||
|
if data is Ellipsis:
|
||||||
|
break
|
||||||
|
|
||||||
|
if attr.startswith("."):
|
||||||
|
# try all classes
|
||||||
|
for data_base_type in classes_recursive(type(data_base)):
|
||||||
|
attr_new = rna_update_dict.get(data_base_type.__name__, {}).get(attr[1:])
|
||||||
|
if attr_new:
|
||||||
|
path_new[i] = "." + attr_new
|
||||||
|
|
||||||
|
# set this as the base for further properties
|
||||||
|
data_base = data
|
||||||
|
|
||||||
|
data_path_new = "".join(path_new)[1:] # skip the first "."
|
||||||
|
return data_path_new
|
||||||
|
|
||||||
|
|
||||||
|
def update_data_paths(rna_update):
|
||||||
|
''' rna_update triple [(class_name, from, to), ...]
|
||||||
|
'''
|
||||||
|
|
||||||
|
# make a faster lookup dict
|
||||||
|
rna_update_dict = {}
|
||||||
|
for ren_class, ren_from, ren_to in rna_update:
|
||||||
|
rna_update_dict.setdefault(ren_class, {})[ren_from] = ren_to
|
||||||
|
|
||||||
|
rna_update_from_map = {}
|
||||||
|
for ren_class, ren_from, ren_to in rna_update:
|
||||||
|
rna_update_from_map.setdefault(ren_from, []).append(ren_to)
|
||||||
|
|
||||||
|
for id_data in id_iter():
|
||||||
|
anim_data = getattr(id_data, "animation_data", None)
|
||||||
|
if anim_data is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for fcurve in anim_data.drivers:
|
||||||
|
for var in fcurve.driver.variables:
|
||||||
|
if var.type == 'SINGLE_PROP':
|
||||||
|
for tar in var.targets:
|
||||||
|
id_data_other = tar.id
|
||||||
|
data_path = tar.data_path
|
||||||
|
|
||||||
|
if id_data_other and data_path:
|
||||||
|
data_path_new = find_path_new(id_data_other, data_path, rna_update_dict, rna_update_from_map)
|
||||||
|
# print(data_path_new)
|
||||||
|
if data_path_new != data_path:
|
||||||
|
if not IS_TESTING:
|
||||||
|
tar.data_path = data_path_new
|
||||||
|
print("driver (%s): %s -> %s" % (id_data_other.name, data_path, data_path_new))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
for action in anim_data_actions(anim_data):
|
||||||
|
for fcu in action.fcurves:
|
||||||
|
data_path = fcu.data_path
|
||||||
|
data_path_new = find_path_new(id_data, data_path, rna_update_dict, rna_update_from_map)
|
||||||
|
# print(data_path_new)
|
||||||
|
if data_path_new != data_path:
|
||||||
|
if not IS_TESTING:
|
||||||
|
fcu.data_path = data_path_new
|
||||||
|
print("fcurve (%s): %s -> %s" % (id_data.name, data_path, data_path_new))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
|
||||||
|
# Example, should be called externally
|
||||||
|
# (class, from, to)
|
||||||
|
replace_ls = [
|
||||||
|
('AnimVizMotionPaths', 'frame_after', 'frame_after'),
|
||||||
|
('AnimVizMotionPaths', 'frame_before', 'frame_before'),
|
||||||
|
('AnimVizOnionSkinning', 'frame_after', 'frame_after'),
|
||||||
|
]
|
||||||
|
|
||||||
|
update_data_paths(replace_ls)
|
204
release/scripts/modules/bpyml.py
Executable file
204
release/scripts/modules/bpyml.py
Executable file
@ -0,0 +1,204 @@
|
|||||||
|
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU General Public License
|
||||||
|
# as published by the Free Software Foundation; either version 2
|
||||||
|
# of the License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software Foundation,
|
||||||
|
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
# ##### END GPL LICENSE BLOCK #####
|
||||||
|
|
||||||
|
# <pep8 compliant>
|
||||||
|
|
||||||
|
"""
|
||||||
|
This module translates a python like XML representation into XML
|
||||||
|
or simple python blender/ui function calls.
|
||||||
|
|
||||||
|
sometag(arg=10) [
|
||||||
|
another()
|
||||||
|
another(key="value")
|
||||||
|
]
|
||||||
|
|
||||||
|
# converts into ...
|
||||||
|
|
||||||
|
<sometag arg="10">
|
||||||
|
<another/>
|
||||||
|
<another key="value" />
|
||||||
|
</sometag>
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
TAG, ARGS, CHILDREN = range(3)
|
||||||
|
class ReturnStore(tuple):
|
||||||
|
def __getitem__(self, key):
|
||||||
|
|
||||||
|
# single item get's
|
||||||
|
if type(key) is ReturnStore:
|
||||||
|
key = (key, )
|
||||||
|
|
||||||
|
if type(key) is tuple:
|
||||||
|
children = self[CHILDREN]
|
||||||
|
if children:
|
||||||
|
raise Exception("Only a single __getitem__ is allowed on the ReturnStore")
|
||||||
|
else:
|
||||||
|
children[:] = key
|
||||||
|
return self
|
||||||
|
else:
|
||||||
|
return tuple.__getitem__(self, key)
|
||||||
|
|
||||||
|
|
||||||
|
class FunctionStore(object):
|
||||||
|
def __call__(self, **kwargs):
|
||||||
|
return ReturnStore((self.__class__.__name__, kwargs, []))
|
||||||
|
|
||||||
|
|
||||||
|
def tag_vars(tags, module=__name__):
|
||||||
|
return {tag: type(tag, (FunctionStore, ), {"__module__": module})() for tag in tags}
|
||||||
|
|
||||||
|
|
||||||
|
def tag_module(mod_name, tags):
|
||||||
|
import sys
|
||||||
|
from types import ModuleType
|
||||||
|
mod = ModuleType(mod_name)
|
||||||
|
sys.modules[mod_name] = mod
|
||||||
|
dict_values = tag_vars(tags, mod_name)
|
||||||
|
mod.__dict__.update(dict_values)
|
||||||
|
return mod
|
||||||
|
|
||||||
|
|
||||||
|
def toxml(py_data, indent=" "):
|
||||||
|
|
||||||
|
if len(py_data) != 1 or type(py_data) != list:
|
||||||
|
raise Exception("Expected a list with one member")
|
||||||
|
|
||||||
|
def _to_xml(py_item, xml_node=None):
|
||||||
|
if xml_node is None:
|
||||||
|
xml_node = newdoc.createElement(py_item[TAG])
|
||||||
|
|
||||||
|
for key, value in py_item[ARGS].items():
|
||||||
|
xml_node.setAttribute(key, str(value))
|
||||||
|
|
||||||
|
for py_item_child in py_item[CHILDREN]:
|
||||||
|
xml_node.appendChild(_to_xml(py_item_child))
|
||||||
|
|
||||||
|
return xml_node
|
||||||
|
|
||||||
|
def _to_xml_iter(xml_parent, data_ls):
|
||||||
|
for py_item in data_ls:
|
||||||
|
xml_node = newdoc.createElement(py_item[TAG])
|
||||||
|
|
||||||
|
|
||||||
|
# ok if its empty
|
||||||
|
_to_xml_iter(xml_node, py_item[CHILDREN])
|
||||||
|
|
||||||
|
import xml.dom.minidom
|
||||||
|
impl = xml.dom.minidom.getDOMImplementation()
|
||||||
|
newdoc = impl.createDocument(None, py_data[0][TAG], None)
|
||||||
|
|
||||||
|
_to_xml(py_data[0], newdoc.documentElement)
|
||||||
|
|
||||||
|
return newdoc.documentElement.toprettyxml(indent=" ")
|
||||||
|
|
||||||
|
|
||||||
|
def fromxml(data):
|
||||||
|
def _fromxml_kwargs(xml_node):
|
||||||
|
kwargs = {}
|
||||||
|
for key, value in xml_node.attributes.items():
|
||||||
|
kwargs[key] = value
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
|
def _fromxml(xml_node):
|
||||||
|
py_item = (xml_node.tagName, _fromxml_kwargs(xml_node), [])
|
||||||
|
#_fromxml_iter(py_item, xml_node.childNodes)
|
||||||
|
for xml_node_child in xml_node.childNodes:
|
||||||
|
if xml_node_child.nodeType not in (xml_node_child.TEXT_NODE, xml_node_child.COMMENT_NODE):
|
||||||
|
py_item[CHILDREN].append(_fromxml(xml_node_child))
|
||||||
|
return py_item
|
||||||
|
|
||||||
|
import xml.dom.minidom
|
||||||
|
xml_doc = xml.dom.minidom.parseString(data)
|
||||||
|
return [_fromxml(xml_doc.documentElement)]
|
||||||
|
|
||||||
|
|
||||||
|
def topretty_py(py_data, indent=" "):
|
||||||
|
|
||||||
|
if len(py_data) != 1:
|
||||||
|
raise Exception("Expected a list with one member")
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
def _to_kwargs(kwargs):
|
||||||
|
return ", ".join([("%s=%s" % (key, repr(value))) for key, value in sorted(kwargs.items())])
|
||||||
|
|
||||||
|
def _topretty(py_item, indent_ctx, last):
|
||||||
|
if py_item[CHILDREN]:
|
||||||
|
lines.append("%s%s(%s) [" % (indent_ctx, py_item[TAG], _to_kwargs(py_item[ARGS])))
|
||||||
|
py_item_last = py_item[CHILDREN][-1]
|
||||||
|
for py_item_child in py_item[CHILDREN]:
|
||||||
|
_topretty(py_item_child, indent_ctx + indent, (py_item_child is py_item_last))
|
||||||
|
lines.append("%s]%s" % (indent_ctx, ("" if last else ",")))
|
||||||
|
else:
|
||||||
|
lines.append("%s%s(%s)%s" % (indent_ctx, py_item[TAG], _to_kwargs(py_item[ARGS]), ("" if last else ",")))
|
||||||
|
|
||||||
|
_topretty(py_data[0], "", True)
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# testing code.
|
||||||
|
|
||||||
|
tag_module("bpyml_test", ("ui", "prop", "row", "column", "active", "separator", "split"))
|
||||||
|
from bpyml_test import *
|
||||||
|
|
||||||
|
draw = [
|
||||||
|
ui() [
|
||||||
|
split() [
|
||||||
|
column() [
|
||||||
|
prop(data='context.scene.render', property='use_stamp_time', text='Time'),
|
||||||
|
prop(data='context.scene.render', property='use_stamp_date', text='Date'),
|
||||||
|
prop(data='context.scene.render', property='use_stamp_render_time', text='RenderTime'),
|
||||||
|
prop(data='context.scene.render', property='use_stamp_frame', text='Frame'),
|
||||||
|
prop(data='context.scene.render', property='use_stamp_scene', text='Scene'),
|
||||||
|
prop(data='context.scene.render', property='use_stamp_camera', text='Camera'),
|
||||||
|
prop(data='context.scene.render', property='use_stamp_filename', text='Filename'),
|
||||||
|
prop(data='context.scene.render', property='use_stamp_marker', text='Marker'),
|
||||||
|
prop(data='context.scene.render', property='use_stamp_sequencer_strip', text='Seq. Strip')
|
||||||
|
],
|
||||||
|
column() [
|
||||||
|
active(expr='context.scene.render.use_stamp'),
|
||||||
|
prop(data='context.scene.render', property='stamp_foreground', slider=True),
|
||||||
|
prop(data='context.scene.render', property='stamp_background', slider=True),
|
||||||
|
separator(),
|
||||||
|
prop(data='context.scene.render', property='stamp_font_size', text='Font Size')
|
||||||
|
]
|
||||||
|
],
|
||||||
|
split(percentage=0.2) [
|
||||||
|
prop(data='context.scene.render', property='use_stamp_note', text='Note'),
|
||||||
|
row() [
|
||||||
|
active(expr='context.scene.render.use_stamp_note'),
|
||||||
|
prop(data='context.scene.render', property='stamp_note_text', text='')
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
xml_data = toxml(draw)
|
||||||
|
print(xml_data) # xml version
|
||||||
|
|
||||||
|
py_data = fromxml(xml_data)
|
||||||
|
print(py_data) # converted back to py
|
||||||
|
|
||||||
|
xml_data = toxml(py_data)
|
||||||
|
print(xml_data) # again back to xml
|
||||||
|
|
||||||
|
py_data = fromxml(xml_data) # pretty python version
|
||||||
|
print(topretty_py(py_data))
|
100
release/scripts/modules/bpyml_ui.py
Executable file
100
release/scripts/modules/bpyml_ui.py
Executable file
@ -0,0 +1,100 @@
|
|||||||
|
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU General Public License
|
||||||
|
# as published by the Free Software Foundation; either version 2
|
||||||
|
# of the License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software Foundation,
|
||||||
|
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
# ##### END GPL LICENSE BLOCK #####
|
||||||
|
|
||||||
|
# <pep8 compliant>
|
||||||
|
|
||||||
|
|
||||||
|
import bpy as _bpy
|
||||||
|
import bpyml
|
||||||
|
from bpyml import TAG, ARGS, CHILDREN
|
||||||
|
from types import ModuleType
|
||||||
|
|
||||||
|
_uilayout_rna = _bpy.types.UILayout.bl_rna
|
||||||
|
|
||||||
|
_uilayout_tags = ["ui"] + \
|
||||||
|
_uilayout_rna.properties.keys() + \
|
||||||
|
_uilayout_rna.functions.keys()
|
||||||
|
|
||||||
|
# these need to be imported directly
|
||||||
|
# >>> from bpyml_ui.locals import *
|
||||||
|
locals = bpyml.tag_module("%s.locals" % __name__ , _uilayout_tags)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_rna(prop, value):
|
||||||
|
if prop.type == 'FLOAT':
|
||||||
|
value = float(value)
|
||||||
|
elif prop.type == 'INT':
|
||||||
|
value = int(value)
|
||||||
|
elif prop.type == 'BOOLEAN':
|
||||||
|
if value in (True, False):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if value not in ("True", "False"):
|
||||||
|
raise Exception("invalid bool value: %s" % value)
|
||||||
|
value = bool(value == "True")
|
||||||
|
elif prop.type in ('STRING', 'ENUM'):
|
||||||
|
pass
|
||||||
|
elif prop.type == 'POINTER':
|
||||||
|
value = eval("_bpy." + value)
|
||||||
|
else:
|
||||||
|
raise Exception("type not supported %s.%s" % (prop.identifier, prop.type))
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_rna_args(base, py_node):
|
||||||
|
rna_params = base.bl_rna.functions[py_node[TAG]].parameters
|
||||||
|
args = {}
|
||||||
|
for key, value in py_node[ARGS].items():
|
||||||
|
args[key] = _parse_rna(rna_params[key], value)
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
def _call_recursive(context, base, py_node):
|
||||||
|
prop = base.bl_rna.properties.get(py_node[TAG])
|
||||||
|
if py_node[TAG] in base.bl_rna.properties:
|
||||||
|
value = py_node[ARGS].get("expr")
|
||||||
|
if value:
|
||||||
|
value = eval(value, {"context": _bpy.context})
|
||||||
|
setattr(base, py_node[TAG], value)
|
||||||
|
else:
|
||||||
|
value = py_node[ARGS]['value'] # have to have this
|
||||||
|
setattr(base, name, value)
|
||||||
|
else:
|
||||||
|
args = _parse_rna_args(base, py_node)
|
||||||
|
func_new = getattr(base, py_node[TAG])
|
||||||
|
base_new = func_new(**args) # call blender func
|
||||||
|
if base_new is not None:
|
||||||
|
for py_node_child in py_node[CHILDREN]:
|
||||||
|
_call_recursive(context, base_new, py_node_child)
|
||||||
|
|
||||||
|
|
||||||
|
class BPyML_BaseUI():
|
||||||
|
'''
|
||||||
|
This is a mix-in class that defines a draw function
|
||||||
|
which checks for draw_data
|
||||||
|
'''
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
for py_node in self.draw_data[CHILDREN]:
|
||||||
|
_call_recursive(context, layout, py_node)
|
||||||
|
|
||||||
|
def draw_header(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
for py_node in self.draw_header_data[CHILDREN]:
|
||||||
|
_call_recursive(context, layout, py_node)
|
Loading…
Reference in New Issue
Block a user