Merge branch 'blender2.7'
[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 # ***** END GPL LICENSE BLOCK *****
18
19 # <pep8 compliant>
20
21 import bpy
22
23
24 def build_property_typemap(skip_classes, skip_typemap):
25
26     property_typemap = {}
27
28     for attr in dir(bpy.types):
29         cls = getattr(bpy.types, attr)
30         if issubclass(cls, skip_classes):
31             continue
32
33         # # to support skip-save we can't get all props
34         # properties = cls.bl_rna.properties.keys()
35         properties = []
36         for prop_id, prop in cls.bl_rna.properties.items():
37             if not prop.is_skip_save:
38                 properties.append(prop_id)
39
40         properties.remove("rna_type")
41         property_typemap[attr] = properties
42
43     if skip_typemap:
44         for cls_name, properties_blacklist in skip_typemap.items():
45             properties = property_typemap.get(cls_name)
46             if properties is not None:
47                 for prop_id in properties_blacklist:
48                     try:
49                         properties.remove(prop_id)
50                     except:
51                         print("skip_typemap unknown prop_id '%s.%s'" % (cls_name, prop_id))
52             else:
53                 print("skip_typemap unknown class '%s'" % cls_name)
54
55     return property_typemap
56
57
58 def print_ln(data):
59     print(data, end="")
60
61
62 def rna2xml(
63         fw=print_ln,
64         root_node="",
65         root_rna=None,  # must be set
66         root_rna_skip=set(),
67         root_ident="",
68         ident_val="  ",
69         skip_classes=(
70             bpy.types.Operator,
71             bpy.types.Panel,
72             bpy.types.KeyingSet,
73             bpy.types.Header,
74             bpy.types.PropertyGroup,
75         ),
76         skip_typemap=None,
77         pretty_format=True,
78         method='DATA',
79 ):
80     from xml.sax.saxutils import quoteattr
81     property_typemap = build_property_typemap(skip_classes, skip_typemap)
82
83     # don't follow properties of this type, just reference them by name
84     # they MUST have a unique 'name' property.
85     # 'ID' covers most types
86     referenced_classes = (
87         bpy.types.ID,
88         bpy.types.Bone,
89         bpy.types.ActionGroup,
90         bpy.types.PoseBone,
91         bpy.types.Node,
92         bpy.types.Sequence,
93     )
94
95     def number_to_str(val, val_type):
96         if val_type == int:
97             return "%d" % val
98         elif val_type == float:
99             return "%.6g" % val
100         elif val_type == bool:
101             return "TRUE" if val else "FALSE"
102         else:
103             raise NotImplemented("this type is not a number %s" % val_type)
104
105     def rna2xml_node(ident, value, parent):
106         ident_next = ident + ident_val
107
108         # divide into attrs and nodes.
109         node_attrs = []
110         nodes_items = []
111         nodes_lists = []
112
113         value_type = type(value)
114
115         if issubclass(value_type, skip_classes):
116             return
117
118         # XXX, fixme, pointcache has eternal nested pointer to its self.
119         if value == parent:
120             return
121
122         value_type_name = value_type.__name__
123         for prop in property_typemap[value_type_name]:
124
125             subvalue = getattr(value, prop)
126             subvalue_type = type(subvalue)
127
128             if subvalue_type in {int, bool, float}:
129                 node_attrs.append("%s=\"%s\"" % (prop, number_to_str(subvalue, subvalue_type)))
130             elif subvalue_type is str:
131                 node_attrs.append("%s=%s" % (prop, quoteattr(subvalue)))
132             elif subvalue_type is set:
133                 node_attrs.append("%s=%s" % (prop, quoteattr("{" + ",".join(list(subvalue)) + "}")))
134             elif subvalue is None:
135                 node_attrs.append("%s=\"NONE\"" % prop)
136             elif issubclass(subvalue_type, referenced_classes):
137                 # special case, ID's are always referenced.
138                 node_attrs.append("%s=%s" % (prop, quoteattr(subvalue_type.__name__ + "::" + subvalue.name)))
139             else:
140                 try:
141                     subvalue_ls = list(subvalue)
142                 except:
143                     subvalue_ls = None
144
145                 if subvalue_ls is None:
146                     nodes_items.append((prop, subvalue, subvalue_type))
147                 else:
148                     # check if the list contains native types
149                     subvalue_rna = value.path_resolve(prop, False)
150                     if type(subvalue_rna).__name__ == "bpy_prop_array":
151                         # check if this is a 0-1 color (rgb, rgba)
152                         # in that case write as a hexadecimal
153                         prop_rna = value.bl_rna.properties[prop]
154                         if (prop_rna.subtype == 'COLOR_GAMMA' and
155                                 prop_rna.hard_min == 0.0 and
156                                 prop_rna.hard_max == 1.0 and
157                                 prop_rna.array_length in {3, 4}):
158                             # -----
159                             # color
160                             array_value = "#" + "".join(("%.2x" % int(v * 255) for v in subvalue_rna))
161
162                         else:
163                             # default
164                             def str_recursive(s):
165                                 subsubvalue_type = type(s)
166                                 if subsubvalue_type in {int, float, bool}:
167                                     return number_to_str(s, subsubvalue_type)
168                                 else:
169                                     return " ".join([str_recursive(si) for si in s])
170
171                             array_value = " ".join(str_recursive(v) for v in subvalue_rna)
172
173                         node_attrs.append("%s=\"%s\"" % (prop, array_value))
174                     else:
175                         nodes_lists.append((prop, subvalue_ls, subvalue_type))
176
177         # declare + attributes
178         if pretty_format:
179             if node_attrs:
180                 fw("%s<%s\n" % (ident, value_type_name))
181                 for node_attr in node_attrs:
182                     fw("%s%s\n" % (ident_next, node_attr))
183                 fw("%s>\n" % (ident_next,))
184             else:
185                 fw("%s<%s>\n" % (ident, value_type_name))
186         else:
187             fw("%s<%s %s>\n" % (ident, value_type_name, " ".join(node_attrs)))
188
189         # unique members
190         for prop, subvalue, subvalue_type in nodes_items:
191             fw("%s<%s>\n" % (ident_next, prop))  # XXX, this is awkward, how best to solve?
192             rna2xml_node(ident_next + ident_val, subvalue, value)
193             fw("%s</%s>\n" % (ident_next, prop))  # XXX, need to check on this.
194
195         # list members
196         for prop, subvalue, subvalue_type in nodes_lists:
197             fw("%s<%s>\n" % (ident_next, prop))
198             for subvalue_item in subvalue:
199                 if subvalue_item is not None:
200                     rna2xml_node(ident_next + ident_val, subvalue_item, value)
201             fw("%s</%s>\n" % (ident_next, prop))
202
203         fw("%s</%s>\n" % (ident, value_type_name))
204
205     # -------------------------------------------------------------------------
206     # needs re-working to be generic
207
208     if root_node:
209         fw("%s<%s>\n" % (root_ident, root_node))
210
211     # bpy.data
212     if method == 'DATA':
213         ident = root_ident + ident_val
214         for attr in dir(root_rna):
215
216             # exceptions
217             if attr.startswith("_"):
218                 continue
219             elif attr in root_rna_skip:
220                 continue
221
222             value = getattr(root_rna, attr)
223             try:
224                 ls = value[:]
225             except:
226                 ls = None
227
228             if type(ls) == list:
229                 fw("%s<%s>\n" % (ident, attr))
230                 for blend_id in ls:
231                     rna2xml_node(ident + ident_val, blend_id, None)
232                 fw("%s</%s>\n" % (ident_val, attr))
233     # any attribute
234     elif method == 'ATTR':
235         rna2xml_node(root_ident, root_rna, None)
236
237     if root_node:
238         fw("%s</%s>\n" % (root_ident, root_node))
239
240
241 def xml2rna(root_xml,
242             root_rna=None,  # must be set
243             ):
244
245     def rna2xml_node(xml_node, value):
246         # print("evaluating:", xml_node.nodeName)
247
248         # ---------------------------------------------------------------------
249         # Simple attributes
250
251         for attr in xml_node.attributes.keys():
252             # print("  ", attr)
253             subvalue = getattr(value, attr, Ellipsis)
254
255             if subvalue is Ellipsis:
256                 print("%s.%s not found" % (type(value).__name__, attr))
257             else:
258                 value_xml = xml_node.attributes[attr].value
259
260                 subvalue_type = type(subvalue)
261                 tp_name = 'UNKNOWN'
262                 if subvalue_type == float:
263                     value_xml_coerce = float(value_xml)
264                     tp_name = 'FLOAT'
265                 elif subvalue_type == int:
266                     value_xml_coerce = int(value_xml)
267                     tp_name = 'INT'
268                 elif subvalue_type == bool:
269                     value_xml_coerce = {'TRUE': True, 'FALSE': False}[value_xml]
270                     tp_name = 'BOOL'
271                 elif subvalue_type == str:
272                     value_xml_coerce = value_xml
273                     tp_name = 'STR'
274                 elif hasattr(subvalue, "__len__"):
275                     if value_xml.startswith("#"):
276                         # read hexadecimal value as float array
277                         value_xml_split = value_xml[1:]
278                         value_xml_coerce = [int(value_xml_split[i:i + 2], 16) /
279                                             255 for i in range(0, len(value_xml_split), 2)]
280                         del value_xml_split
281                     else:
282                         value_xml_split = value_xml.split()
283                         try:
284                             value_xml_coerce = [int(v) for v in value_xml_split]
285                         except ValueError:
286                             try:
287                                 value_xml_coerce = [float(v) for v in value_xml_split]
288                             except ValueError:  # bool vector property
289                                 value_xml_coerce = [{'TRUE': True, 'FALSE': False}[v] for v in value_xml_split]
290                         del value_xml_split
291                     tp_name = 'ARRAY'
292
293 #                print("  %s.%s (%s) --- %s" % (type(value).__name__, attr, tp_name, subvalue_type))
294                 try:
295                     setattr(value, attr, value_xml_coerce)
296                 except ValueError:
297                     # size mismatch
298                     val = getattr(value, attr)
299                     if len(val) < len(value_xml_coerce):
300                         setattr(value, attr, value_xml_coerce[:len(val)])
301                     else:
302                         setattr(value, attr, list(value_xml_coerce) + list(val)[len(value_xml_coerce):])
303
304         # ---------------------------------------------------------------------
305         # Complex attributes
306         for child_xml in xml_node.childNodes:
307             if child_xml.nodeType == child_xml.ELEMENT_NODE:
308                 # print()
309                 # print(child_xml.nodeName)
310                 subvalue = getattr(value, child_xml.nodeName, None)
311                 if subvalue is not None:
312
313                     elems = []
314                     for child_xml_real in child_xml.childNodes:
315                         if child_xml_real.nodeType == child_xml_real.ELEMENT_NODE:
316                             elems.append(child_xml_real)
317                     del child_xml_real
318
319                     if hasattr(subvalue, "__len__"):
320                         # Collection
321                         if len(elems) != len(subvalue):
322                             print("Size Mismatch! collection:", child_xml.nodeName)
323                         else:
324                             for i in range(len(elems)):
325                                 child_xml_real = elems[i]
326                                 subsubvalue = subvalue[i]
327
328                                 if child_xml_real is None or subsubvalue is None:
329                                     print("None found %s - %d collection:", (child_xml.nodeName, i))
330                                 else:
331                                     rna2xml_node(child_xml_real, subsubvalue)
332
333                     else:
334                         # print(elems)
335
336                         if len(elems) == 1:
337                             # sub node named by its type
338                             child_xml_real, = elems
339
340                             # print(child_xml_real, subvalue)
341                             rna2xml_node(child_xml_real, subvalue)
342                         else:
343                             # empty is valid too
344                             pass
345
346     rna2xml_node(root_xml, root_rna)
347
348
349 # -----------------------------------------------------------------------------
350 # Utility function used by presets.
351 # The idea is you can run a preset like a script with a few args.
352 #
353 # This roughly matches the operator 'bpy.ops.script.python_file_run'
354
355
356 def _get_context_val(context, path):
357     path_full = "context." + path
358     try:
359         value = eval(path_full)
360     except:
361         import traceback
362         traceback.print_exc()
363         print("Error: %r could not be found" % path_full)
364
365         value = Ellipsis
366
367     return value
368
369
370 def xml_file_run(context, filepath, rna_map):
371
372     import xml.dom.minidom
373
374     xml_nodes = xml.dom.minidom.parse(filepath)
375     bpy_xml = xml_nodes.getElementsByTagName("bpy")[0]
376
377     for rna_path, xml_tag in rna_map:
378
379         # first get xml
380         # TODO, error check
381         xml_node = bpy_xml.getElementsByTagName(xml_tag)[0]
382
383         value = _get_context_val(context, rna_path)
384
385         if value is not Ellipsis and value is not None:
386             print("  loading XML: %r -> %r" % (filepath, rna_path))
387             xml2rna(xml_node, root_rna=value)
388
389
390 def xml_file_write(context, filepath, rna_map, skip_typemap=None):
391
392     file = open(filepath, "w", encoding="utf-8")
393     fw = file.write
394
395     fw("<bpy>\n")
396
397     for rna_path, _xml_tag in rna_map:
398         # xml_tag is ignored, we get this from the rna
399         value = _get_context_val(context, rna_path)
400         rna2xml(fw,
401                 root_rna=value,
402                 method='ATTR',
403                 root_ident="  ",
404                 ident_val="  ",
405                 skip_typemap=skip_typemap,
406                 )
407
408     fw("</bpy>\n")
409     file.close()