Cleanup: use keyword only args to rna_idprop_ui_create
[blender.git] / release / scripts / modules / rna_prop_ui.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 rna_idprop_ui_get(item, create=True):
25     try:
26         return item['_RNA_UI']
27     except:
28         if create:
29             item['_RNA_UI'] = {}
30             return item['_RNA_UI']
31         else:
32             return None
33
34
35 def rna_idprop_ui_del(item):
36     try:
37         del item['_RNA_UI']
38     except KeyError:
39         pass
40
41
42 def rna_idprop_quote_path(prop):
43     return "[\"%s\"]" % prop.replace("\"", "\\\"")
44
45
46 def rna_idprop_ui_prop_update(item, prop):
47     prop_path = rna_idprop_quote_path(prop)
48     prop_rna = item.path_resolve(prop_path, False)
49     if isinstance(prop_rna, bpy.types.bpy_prop):
50         prop_rna.update()
51
52
53 def rna_idprop_ui_prop_get(item, prop, create=True):
54
55     rna_ui = rna_idprop_ui_get(item, create)
56
57     if rna_ui is None:
58         return None
59
60     try:
61         return rna_ui[prop]
62     except:
63         rna_ui[prop] = {}
64         return rna_ui[prop]
65
66
67 def rna_idprop_ui_prop_clear(item, prop, remove=True):
68     rna_ui = rna_idprop_ui_get(item, False)
69
70     if rna_ui is None:
71         return
72
73     try:
74         del rna_ui[prop]
75     except KeyError:
76         pass
77     if remove and len(item.keys()) == 1:
78         rna_idprop_ui_del(item)
79
80
81 def rna_idprop_context_value(context, context_member, property_type):
82     space = context.space_data
83
84     if space is None or isinstance(space, bpy.types.SpaceProperties):
85         pin_id = space.pin_id
86     else:
87         pin_id = None
88
89     if pin_id and isinstance(pin_id, property_type):
90         rna_item = pin_id
91         context_member = "space_data.pin_id"
92     else:
93         rna_item = eval("context." + context_member)
94
95     return rna_item, context_member
96
97
98 def rna_idprop_has_properties(rna_item):
99     keys = rna_item.keys()
100     nbr_props = len(keys)
101     return (nbr_props > 1) or (nbr_props and '_RNA_UI' not in keys)
102
103
104 def rna_idprop_ui_prop_default_set(item, prop, value):
105     defvalue = None
106     try:
107         prop_type = type(item[prop])
108
109         if prop_type in {int, float}:
110             defvalue = prop_type(value)
111     except KeyError:
112         pass
113
114     if defvalue:
115         rna_ui = rna_idprop_ui_prop_get(item, prop, True)
116         rna_ui["default"] = defvalue
117     else:
118         rna_ui = rna_idprop_ui_prop_get(item, prop)
119         if rna_ui and "default" in rna_ui:
120             del rna_ui["default"]
121
122
123 def rna_idprop_ui_create(
124         item, prop, *, default,
125         min=0.0, max=1.0,
126         soft_min=None, soft_max=None,
127         description=None,
128         overridable=False,
129 ):
130     """Create and initialize a custom property with limits, defaults and other settings."""
131
132     proptype = type(default)
133
134     # Sanitize limits
135     if proptype is bool:
136         min = soft_min = False
137         max = soft_max = True
138
139     if soft_min is None:
140         soft_min = min
141     if soft_max is None:
142         soft_max = max
143
144     # Assign the value
145     item[prop] = default
146
147     rna_idprop_ui_prop_update(item, prop)
148
149     # Clear the UI settings
150     rna_ui_group = rna_idprop_ui_get(item, True)
151     rna_ui_group[prop] = {}
152     rna_ui = rna_ui_group[prop]
153
154     # Assign limits and default
155     if proptype in {int, float, bool}:
156         # The type must be exactly the same
157         rna_ui["min"] = proptype(min)
158         rna_ui["soft_min"] = proptype(soft_min)
159         rna_ui["max"] = proptype(max)
160         rna_ui["soft_max"] = proptype(soft_max)
161
162         if default:
163             rna_ui["default"] = default
164
165     # Assign other settings
166     if description is not None:
167         rna_ui["description"] = description
168
169     prop_path = rna_idprop_quote_path(prop)
170
171     item.property_overridable_static_set(prop_path, overridable)
172
173     return rna_ui
174
175
176 def draw(layout, context, context_member, property_type, use_edit=True):
177
178     def assign_props(prop, val, key):
179         prop.data_path = context_member
180         prop.property = key
181
182         try:
183             prop.value = str(val)
184         except:
185             pass
186
187     rna_item, context_member = rna_idprop_context_value(context, context_member, property_type)
188
189     # poll should really get this...
190     if not rna_item:
191         return
192
193     from bpy.utils import escape_identifier
194
195     if rna_item.id_data.library is not None:
196         use_edit = False
197
198     assert(isinstance(rna_item, property_type))
199
200     items = rna_item.items()
201     items.sort()
202
203     if use_edit:
204         row = layout.row()
205         props = row.operator("wm.properties_add", text="Add")
206         props.data_path = context_member
207         del row
208
209     show_developer_ui = context.preferences.view.show_developer_ui
210     rna_properties = {prop.identifier for prop in rna_item.bl_rna.properties if prop.is_runtime} if items else None
211
212     layout.use_property_split = True
213     layout.use_property_decorate = False  # No animation.
214
215     flow = layout.grid_flow(row_major=False, columns=0, even_columns=True, even_rows=False, align=True)
216
217     for key, val in items:
218
219         if key == '_RNA_UI':
220             continue
221
222         is_rna = (key in rna_properties)
223
224         # only show API defined for developers
225         if is_rna and not show_developer_ui:
226             continue
227
228         to_dict = getattr(val, "to_dict", None)
229         to_list = getattr(val, "to_list", None)
230
231         # val_orig = val  # UNUSED
232         if to_dict:
233             val = to_dict()
234             val_draw = str(val)
235         elif to_list:
236             val = to_list()
237             val_draw = str(val)
238         else:
239             val_draw = val
240
241         row = flow.row(align=True)
242         box = row.box()
243
244         if use_edit:
245             split = box.split(factor=0.75)
246             row = split.row(align=True)
247         else:
248             row = box.row(align=True)
249
250         row.alignment = 'RIGHT'
251
252         row.label(text=key, translate=False)
253
254         # explicit exception for arrays.
255         if to_dict or to_list:
256             row.label(text=val_draw, translate=False)
257         else:
258             if is_rna:
259                 row.prop(rna_item, key, text="")
260             else:
261                 row.prop(rna_item, '["%s"]' % escape_identifier(key), text="")
262
263         if use_edit:
264             row = split.row(align=True)
265             if not is_rna:
266                 props = row.operator("wm.properties_edit", text="Edit")
267                 assign_props(props, val_draw, key)
268                 props = row.operator("wm.properties_remove", text="", icon='REMOVE')
269                 assign_props(props, val_draw, key)
270             else:
271                 row.label(text="API Defined")
272
273     del flow
274
275
276 class PropertyPanel:
277     """
278     The subclass should have its own poll function
279     and the variable '_context_path' MUST be set.
280     """
281     bl_label = "Custom Properties"
282     bl_options = {'DEFAULT_CLOSED'}
283
284     @classmethod
285     def poll(cls, context):
286         rna_item, context_member = rna_idprop_context_value(context, cls._context_path, cls._property_type)
287         return bool(rna_item)
288
289     """
290     def draw_header(self, context):
291         rna_item, context_member = rna_idprop_context_value(context, self._context_path, self._property_type)
292         tot = len(rna_item.keys())
293         if tot:
294             self.layout().label(text="%d:" % tot)
295     """
296
297     def draw(self, context):
298         draw(self.layout, context, self._context_path, self._property_type)