9019728c014f49b78b4572f6305d2d68f190b3c5
[blender.git] / release / scripts / modules / rna_xml.py
1 # ***** BEGIN GPL LICENSE BLOCK *****
2 #
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 #
17 # Contributor(s): Campbell Barton
18 #
19 # ***** END GPL LICENSE BLOCK *****
20
21 # <pep8 compliant>
22
23 import bpy
24
25
26 def build_property_typemap(skip_classes):
27
28     property_typemap = {}
29
30     for attr in dir(bpy.types):
31         cls = getattr(bpy.types, attr)
32         if issubclass(cls, skip_classes):
33             continue
34
35         properties = cls.bl_rna.properties.keys()
36         properties.remove("rna_type")
37         property_typemap[attr] = properties
38
39     return property_typemap
40
41
42 def print_ln(data):
43     print(data, end="")
44
45
46 def rna2xml(fw=print_ln,
47             root_node="",
48             root_rna=None,  # must be set
49             root_rna_skip=set(),
50             ident_val="    ",
51             skip_classes=(bpy.types.Operator,
52                           bpy.types.Panel,
53                           bpy.types.KeyingSet,
54                           bpy.types.Header,
55                           ),
56             pretty_format=True,
57             method='DATA'):
58
59     from xml.sax.saxutils import quoteattr
60     property_typemap = build_property_typemap(skip_classes)
61
62     def number_to_str(val, val_type):
63         if val_type == int:
64             return "%d" % val
65         elif val_type == float:
66             return "%.6g" % val
67         elif val_type == bool:
68             return "TRUE" if val else "FALSE"
69         else:
70             raise NotImplemented("this type is not a number %s" % val_type)
71
72     def rna2xml_node(ident, value, parent):
73         ident_next = ident + ident_val
74
75         # divide into attrs and nodes.
76         node_attrs = []
77         nodes_items = []
78         nodes_lists = []
79
80         value_type = type(value)
81
82         if issubclass(value_type, skip_classes):
83             return
84
85         # XXX, fixme, pointcache has eternal nested pointer to its self.
86         if value == parent:
87             return
88
89         value_type_name = value_type.__name__
90         for prop in property_typemap[value_type_name]:
91
92             subvalue = getattr(value, prop)
93             subvalue_type = type(subvalue)
94
95             if subvalue_type in (int, bool, float):
96                 node_attrs.append("%s=\"%s\"" % (prop, number_to_str(subvalue, subvalue_type)))
97             elif subvalue_type is str:
98                 node_attrs.append("%s=%s" % (prop, quoteattr(subvalue)))
99             elif subvalue_type == set:
100                 node_attrs.append("%s=%s" % (prop, quoteattr("{" + ",".join(list(subvalue)) + "}")))
101             elif subvalue is None:
102                 node_attrs.append("%s=\"NONE\"" % prop)
103             elif issubclass(subvalue_type, bpy.types.ID):
104                 # special case, ID's are always referenced.
105                 node_attrs.append("%s=%s" % (prop, quoteattr(subvalue_type.__name__ + "::" + subvalue.name)))
106             else:
107                 try:
108                     subvalue_ls = list(subvalue)
109                 except:
110                     subvalue_ls = None
111
112                 if subvalue_ls is None:
113                     nodes_items.append((prop, subvalue, subvalue_type))
114                 else:
115                     # check if the list contains native types
116                     subvalue_rna = value.path_resolve(prop, False)
117                     if type(subvalue_rna).__name__ == "bpy_prop_array":
118                         # check if this is a 0-1 color (rgb, rgba)
119                         # in that case write as a hexidecimal
120                         prop_rna = value.bl_rna.properties[prop]
121                         if (prop_rna.subtype == 'COLOR_GAMMA' and
122                                 prop_rna.hard_min == 0.0 and
123                                 prop_rna.hard_max == 1.0 and
124                                 prop_rna.array_length in {3, 4}):
125                             # -----
126                             # color
127                             array_value = "#" + "".join(("%.2x" % int(v * 255) for v in subvalue_rna))
128
129                         else:
130                             # default
131                             def str_recursive(s):
132                                 subsubvalue_type = type(s)
133                                 if subsubvalue_type in (int, float, bool):
134                                     return number_to_str(s, subsubvalue_type)
135                                 else:
136                                     return " ".join([str_recursive(si) for si in s])
137                             
138                             array_value = " ".join(str_recursive(v) for v in subvalue_rna)
139
140                         node_attrs.append("%s=\"%s\"" % (prop, array_value))
141                     else:
142                         nodes_lists.append((prop, subvalue_ls, subvalue_type))
143
144         # declare + attributes
145         if pretty_format:
146             tmp_str = "<%s " % value_type_name
147             tmp_ident = "\n" + ident + (" " * len(tmp_str))
148
149             fw("%s%s%s>\n" % (ident, tmp_str, tmp_ident.join(node_attrs)))
150
151             del tmp_str
152             del tmp_ident
153         else:
154             fw("%s<%s %s>\n" % (ident, value_type_name, " ".join(node_attrs)))
155
156         # unique members
157         for prop, subvalue, subvalue_type in nodes_items:
158             fw("%s<%s>\n" % (ident_next, prop))  # XXX, this is awkward, how best to solve?
159             rna2xml_node(ident_next + ident_val, subvalue, value)
160             fw("%s</%s>\n" % (ident_next, prop))  # XXX, need to check on this.
161
162         # list members
163         for prop, subvalue, subvalue_type in nodes_lists:
164             fw("%s<%s>\n" % (ident_next, prop))
165             for subvalue_item in subvalue:
166                 if subvalue_item is not None:
167                     rna2xml_node(ident_next + ident_val, subvalue_item, value)
168             fw("%s</%s>\n" % (ident_next, prop))
169
170         fw("%s</%s>\n" % (ident, value_type_name))
171
172     # -------------------------------------------------------------------------
173     # needs re-workign to be generic
174
175     if root_node:
176         fw("<%s>\n" % root_node)
177
178     # bpy.data
179     if method == 'DATA':
180         for attr in dir(root_rna):
181
182             # exceptions
183             if attr.startswith("_"):
184                 continue
185             elif attr in root_rna_skip:
186                 continue
187
188             value = getattr(root_rna, attr)
189             try:
190                 ls = value[:]
191             except:
192                 ls = None
193
194             if type(ls) == list:
195                 fw("%s<%s>\n" % (ident_val, attr))
196                 for blend_id in ls:
197                     rna2xml_node(ident_val + ident_val, blend_id, None)
198                 fw("%s</%s>\n" % (ident_val, attr))
199     # any attribute
200     elif method == 'ATTR':
201         rna2xml_node("", root_rna, None)
202
203     if root_node:
204         fw("</%s>\n" % root_node)
205
206
207 def xml2rna(root_xml,
208             root_rna=None,  # must be set
209             ):
210
211     def rna2xml_node(xml_node, value):
212 #        print("evaluating:", xml_node.nodeName)
213
214         # ---------------------------------------------------------------------
215         # Simple attributes
216
217         for attr in xml_node.attributes.keys():
218 #            print("  ", attr)
219             subvalue = getattr(value, attr, Ellipsis)
220
221             if subvalue is Ellipsis:
222                 print("%s.%s not found" % (type(value).__name__, attr))
223             else:
224                 value_xml = xml_node.attributes[attr].value
225
226                 subvalue_type = type(subvalue)
227                 tp_name = 'UNKNOWN'
228                 if subvalue_type == float:
229                     value_xml_coerce = float(value_xml)
230                     tp_name = 'FLOAT'
231                 elif subvalue_type == int:
232                     value_xml_coerce = int(value_xml)
233                     tp_name = 'INT'
234                 elif subvalue_type == bool:
235                     value_xml_coerce = {'TRUE': True, 'FALSE': False}[value_xml]
236                     tp_name = 'BOOL'
237                 elif subvalue_type == str:
238                     value_xml_coerce = value_xml
239                     tp_name = 'STR'
240                 elif hasattr(subvalue, "__len__"):
241                     if value_xml.startswith("#"):
242                         # read hexidecimal value as float array
243                         value_xml_split = value_xml[1:]
244                         value_xml_coerce = [int(value_xml_split[i:i + 2], 16) / 255  for i in range(0, len(value_xml_split), 2)]
245                         del value_xml_split
246                     else:
247                         value_xml_split = value_xml.split()
248                         try:
249                             value_xml_coerce = [int(v) for v in value_xml_split]
250                         except ValueError:
251                             value_xml_coerce = [float(v) for v in value_xml_split]
252                         del value_xml_split
253                     tp_name = 'ARRAY'
254
255 #                print("  %s.%s (%s) --- %s" % (type(value).__name__, attr, tp_name, subvalue_type))
256                 setattr(value, attr, value_xml_coerce)
257
258         # ---------------------------------------------------------------------
259         # Complex attributes
260         for child_xml in xml_node.childNodes:
261             if child_xml.nodeType == child_xml.ELEMENT_NODE:
262                 # print()
263                 # print(child_xml.nodeName)
264                 subvalue = getattr(value, child_xml.nodeName, None)
265                 if subvalue is not None:
266
267                     elems = []
268                     for child_xml_real in child_xml.childNodes:
269                         if child_xml_real.nodeType == child_xml_real.ELEMENT_NODE:
270                             elems.append(child_xml_real)
271                     del child_xml_real
272
273                     if hasattr(subvalue, "__len__"):
274                         # Collection
275                         if len(elems) != len(subvalue):
276                             print("Size Mismatch! collection:", child_xml.nodeName)
277                         else:
278                             for i in range(len(elems)):
279                                 child_xml_real = elems[i]
280                                 subsubvalue = subvalue[i]
281
282                                 if child_xml_real is None or subsubvalue is None:
283                                     print("None found %s - %d collection:", (child_xml.nodeName, i))
284                                 else:
285                                     rna2xml_node(child_xml_real, subsubvalue)
286
287                     else:
288 #                        print(elems)
289
290                         if len(elems) == 1:
291                             # sub node named by its type
292                             child_xml_real, = elems
293
294                             # print(child_xml_real, subvalue)
295                             rna2xml_node(child_xml_real, subvalue)
296                         else:
297                             # empty is valid too
298                             pass
299
300     rna2xml_node(root_xml, root_rna)
301
302
303
304 # -----------------------------------------------------------------------------
305 # Utility function used by presets.
306 # The idea is you can run a preset like a script with a few args.
307 #
308 # This roughly matches the operator 'bpy.ops.script.python_file_run'
309
310 def _get_context_val(context, path):
311     path_full = "context." + path
312     try:
313         value = eval(path_full)
314     except:
315         import traceback
316         traceback.print_exc()
317         print("Error: %r could not be found" % path_full)
318
319         value = Ellipsis
320
321     return value
322
323 def xml_file_run(context, filepath, rna_map):
324
325     import xml.dom.minidom
326
327     xml_nodes = xml.dom.minidom.parse(filepath)
328     bpy_xml = xml_nodes.getElementsByTagName("bpy")[0]
329
330     for rna_path, xml_tag in rna_map:
331
332         # first get xml
333         # TODO, error check
334         xml_node = bpy_xml.getElementsByTagName(xml_tag)[0]
335
336         value = _get_context_val(context, rna_path)
337
338         if value is not Ellipsis and value is not None:
339             print("  loading XML: %r" % rna_path)
340             xml2rna(xml_node, root_rna=value)
341
342
343 def xml_file_write(context, filepath, rna_map):
344
345     file = open(filepath, 'w', encoding='utf-8')
346     fw = file.write
347
348     fw("<bpy>\n")
349
350     for rna_path, xml_tag in rna_map:
351         # xml_tag is ignored, we get this from the rna
352         value = _get_context_val(context, rna_path)
353         rna2xml(fw, root_rna=value, method='ATTR')
354
355     fw("</bpy>\n")
356     file.close()