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