Fix crash in outliner on cursor hover
[blender.git] / release / scripts / modules / animsys_refactor.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 """
22 This module has utility functions for renaming
23 rna values in fcurves and drivers.
24
25 Currently unused, but might become useful later again.
26 """
27
28 import sys
29 import bpy
30
31
32 IS_TESTING = False
33
34
35 def drepr(string):
36     # is there a less crappy way to do this in python?, re.escape also escapes
37     # single quotes strings so can't use it.
38     return '"%s"' % repr(string)[1:-1].replace("\"", "\\\"").replace("\\'", "'")
39
40
41 def classes_recursive(base_type, clss=None):
42     if clss is None:
43         clss = [base_type]
44     else:
45         clss.append(base_type)
46
47     for base_type_iter in base_type.__bases__:
48         if base_type_iter is not object:
49             classes_recursive(base_type_iter, clss)
50
51     return clss
52
53
54 class DataPathBuilder:
55     """Dummy class used to parse fcurve and driver data paths."""
56     __slots__ = ("data_path", )
57
58     def __init__(self, attrs):
59         self.data_path = attrs
60
61     def __getattr__(self, attr):
62         str_value = ".%s" % attr
63         return DataPathBuilder(self.data_path + (str_value, ))
64
65     def __getitem__(self, key):
66         if type(key) is int:
67             str_value = '[%d]' % key
68         elif type(key) is str:
69             str_value = '[%s]' % drepr(key)
70         else:
71             raise Exception("unsupported accessor %r of type %r (internal error)" % (key, type(key)))
72         return DataPathBuilder(self.data_path + (str_value, ))
73
74     def resolve(self, real_base, rna_update_from_map, fcurve, log):
75         """Return (attribute, value) pairs."""
76         pairs = []
77         base = real_base
78         for item in self.data_path:
79             if base is not Ellipsis:
80                 base_new = Ellipsis
81                 # find the new name
82                 if item.startswith("."):
83                     for class_name, item_new, options in (
84                             rna_update_from_map.get(item[1:], []) +
85                             [(None, item[1:], None)]
86                     ):
87                         if callable(item_new):
88                             # No type check here, callback is assumed to know what it's doing.
89                             base_new, item_new = item_new(base, class_name, item[1:], fcurve, options)
90                             if base_new is not Ellipsis:
91                                 break  # found, don't keep looking
92                         else:
93                             # Type check!
94                             type_ok = True
95                             if class_name is not None:
96                                 type_ok = False
97                                 for base_type in classes_recursive(type(base)):
98                                     if base_type.__name__ == class_name:
99                                         type_ok = True
100                                         break
101                             if type_ok:
102                                 try:
103                                     #print("base." + item_new)
104                                     base_new = eval("base." + item_new)
105                                     break  # found, don't keep looking
106                                 except:
107                                     pass
108                     item_new = "." + item_new
109                 else:
110                     item_new = item
111                     try:
112                         base_new = eval("base" + item_new)
113                     except:
114                         pass
115
116                 if base_new is Ellipsis:
117                     print("Failed to resolve data path:", self.data_path, file=log)
118                 base = base_new
119             else:
120                 item_new = item
121
122             pairs.append((item_new, base))
123         return pairs
124
125
126 def id_iter():
127     type_iter = type(bpy.data.objects)
128
129     for attr in dir(bpy.data):
130         data_iter = getattr(bpy.data, attr, None)
131         if type(data_iter) == type_iter:
132             for id_data in data_iter:
133                 if id_data.library is None:
134                     yield id_data
135
136
137 def anim_data_actions(anim_data):
138     actions = []
139     actions.append(anim_data.action)
140     for track in anim_data.nla_tracks:
141         for strip in track.strips:
142             actions.append(strip.action)
143
144     # filter out None
145     return [act for act in actions if act]
146
147
148 def find_path_new(id_data, data_path, rna_update_from_map, fcurve, log):
149     # note!, id_data can be ID type or a node tree
150     # ignore ID props for now
151     if data_path.startswith("["):
152         return data_path
153
154     # recursive path fixing, likely will be one in most cases.
155     data_path_builder = eval("DataPathBuilder(tuple())." + data_path)
156     data_resolve = data_path_builder.resolve(id_data, rna_update_from_map, fcurve, log)
157
158     path_new = [pair[0] for pair in data_resolve]
159
160     return "".join(path_new)[1:]  # skip the first "."
161
162
163 def update_data_paths(rna_update, log=sys.stdout):
164     """
165     rna_update triple [(class_name, from, to or to_callback, callback options), ...]
166     to_callback is a function with this signature: update_cb(base, class_name, old_path, fcurve, options)
167                 where base is current object, class_name is the expected type name of base (callback has to handle
168                 this), old_path it the org name of base's property, fcurve is the affected fcurve (!),
169                 and options is an opaque data.
170                 class_name, fcurve and options may be None!
171     """
172
173     rna_update_from_map = {}
174     for ren_class, ren_from, ren_to, options in rna_update:
175         rna_update_from_map.setdefault(ren_from, []).append((ren_class, ren_to, options))
176
177     for id_data in id_iter():
178         # check node-trees too
179         anim_data_ls = [(id_data, getattr(id_data, "animation_data", None))]
180         node_tree = getattr(id_data, "node_tree", None)
181         if node_tree:
182             anim_data_ls.append((node_tree, node_tree.animation_data))
183
184         for anim_data_base, anim_data in anim_data_ls:
185             if anim_data is None:
186                 continue
187
188             for fcurve in anim_data.drivers:
189                 data_path = fcurve.data_path
190                 data_path_new = find_path_new(anim_data_base, data_path, rna_update_from_map, fcurve, log)
191                 # print(data_path_new)
192                 if data_path_new != data_path:
193                     if not IS_TESTING:
194                         fcurve.data_path = data_path_new
195                         fcurve.driver.is_valid = True  # reset to allow this to work again
196                     print("driver-fcurve (%s): %s -> %s" % (id_data.name, data_path, data_path_new), file=log)
197
198                 for var in fcurve.driver.variables:
199                     if var.type == 'SINGLE_PROP':
200                         for tar in var.targets:
201                             id_data_other = tar.id
202                             data_path = tar.data_path
203
204                             if id_data_other and data_path:
205                                 data_path_new = find_path_new(id_data_other, data_path, rna_update_from_map, None, log)
206                                 # print(data_path_new)
207                                 if data_path_new != data_path:
208                                     if not IS_TESTING:
209                                         tar.data_path = data_path_new
210                                     print("driver (%s): %s -> %s" % (id_data_other.name, data_path, data_path_new),
211                                           file=log)
212
213             for action in anim_data_actions(anim_data):
214                 for fcu in action.fcurves:
215                     data_path = fcu.data_path
216                     data_path_new = find_path_new(anim_data_base, data_path, rna_update_from_map, fcu, log)
217                     # print(data_path_new)
218                     if data_path_new != data_path:
219                         if not IS_TESTING:
220                             fcu.data_path = data_path_new
221                         print("fcurve (%s): %s -> %s" % (id_data.name, data_path, data_path_new), file=log)
222
223
224 if __name__ == "__main__":
225
226     # Example, should be called externally
227     # (class, from, to or to_callback, callback_options)
228     replace_ls = [
229         ("AnimVizMotionPaths", "frame_after", "frame_after", None),
230         ("AnimVizMotionPaths", "frame_before", "frame_before", None),
231         ("AnimVizOnionSkinning", "frame_after", "frame_after", None),
232     ]
233
234     update_data_paths(replace_ls)