fix [#27148] *Invalid Path* in all "operator presets" dropdowns
[blender.git] / release / scripts / startup / bl_operators / presets.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 class AddPresetBase():
25     '''Base preset class, only for subclassing
26     subclasses must define
27      - preset_values
28      - preset_subdir '''
29     # bl_idname = "script.preset_base_add"
30     # bl_label = "Add a Python Preset"
31     bl_options = {'REGISTER'}  # only because invoke_props_popup requires.
32
33     name = bpy.props.StringProperty(name="Name", description="Name of the preset, used to make the path name", maxlen=64, default="")
34     remove_active = bpy.props.BoolProperty(default=False, options={'HIDDEN'})
35
36     @staticmethod
37     def as_filename(name):  # could reuse for other presets
38         for char in " !@#$%^&*(){}:\";'[]<>,.\\/?":
39             name = name.replace(char, '_')
40         return name.lower().strip()
41
42     def execute(self, context):
43         import os
44
45         if hasattr(self, "pre_cb"):
46             self.pre_cb(context)
47
48         preset_menu_class = getattr(bpy.types, self.preset_menu)
49
50         if not self.remove_active:
51             name = self.name.strip()
52             if not name:
53                 return {'FINISHED'}
54
55             filename = self.as_filename(name)
56
57             target_path = bpy.utils.user_resource('SCRIPTS', os.path.join("presets", self.preset_subdir), create=True)
58
59             if not target_path:
60                 self.report({'WARNING'}, "Failed to create presets path")
61                 return {'CANCELLED'}
62
63             filepath = os.path.join(target_path, filename) + ".py"
64
65             if hasattr(self, "add"):
66                 self.add(context, filepath)
67             else:
68                 file_preset = open(filepath, 'w')
69                 file_preset.write("import bpy\n")
70
71                 if hasattr(self, "preset_defines"):
72                     for rna_path in self.preset_defines:
73                         exec(rna_path)
74                         file_preset.write("%s\n" % rna_path)
75                     file_preset.write("\n")
76
77                 for rna_path in self.preset_values:
78                     value = eval(rna_path)
79                     # convert thin wrapped sequences to simple lists to repr()
80                     try:
81                         value = value[:]
82                     except:
83                         pass
84
85                     file_preset.write("%s = %r\n" % (rna_path, value))
86
87                 file_preset.close()
88
89             preset_menu_class.bl_label = bpy.path.display_name(filename)
90
91         else:
92             preset_active = preset_menu_class.bl_label
93
94             # fairly sloppy but convenient.
95             filepath = bpy.utils.preset_find(preset_active, self.preset_subdir)
96
97             if not filepath:
98                 filepath = bpy.utils.preset_find(preset_active, self.preset_subdir, display_name=True)
99
100             if not filepath:
101                 return {'CANCELLED'}
102
103             if hasattr(self, "remove"):
104                 self.remove(context, filepath)
105             else:
106                 try:
107                     os.remove(filepath)
108                 except:
109                     import traceback
110                     traceback.print_exc()
111
112             # XXX, stupid!
113             preset_menu_class.bl_label = "Presets"
114
115         if hasattr(self, "post_cb"):
116             self.post_cb(context)
117
118         return {'FINISHED'}
119
120     def check(self, context):
121         self.name = self.as_filename(self.name.strip())
122
123     def invoke(self, context, event):
124         if not self.remove_active:
125             wm = context.window_manager
126             return wm.invoke_props_dialog(self)
127         else:
128             return self.execute(context)
129
130
131 class ExecutePreset(bpy.types.Operator):
132     ''' Executes a preset '''
133     bl_idname = "script.execute_preset"
134     bl_label = "Execute a Python Preset"
135
136     filepath = bpy.props.StringProperty(name="Path", description="Path of the Python file to execute", maxlen=512, default="")
137     menu_idname = bpy.props.StringProperty(name="Menu ID Name", description="ID name of the menu this was called from", default="")
138
139     def execute(self, context):
140         from os.path import basename
141         filepath = self.filepath
142
143         # change the menu title to the most recently chosen option
144         preset_class = getattr(bpy.types, self.menu_idname)
145         preset_class.bl_label = bpy.path.display_name(basename(filepath))
146
147         # execute the preset using script.python_file_run
148         bpy.ops.script.python_file_run(filepath=filepath)
149         return {'FINISHED'}
150
151
152 class AddPresetRender(AddPresetBase, bpy.types.Operator):
153     '''Add a Render Preset'''
154     bl_idname = "render.preset_add"
155     bl_label = "Add Render Preset"
156     preset_menu = "RENDER_MT_presets"
157
158     preset_defines = [
159         "scene = bpy.context.scene"
160     ]
161
162     preset_values = [
163         "scene.render.field_order",
164         "scene.render.fps",
165         "scene.render.fps_base",
166         "scene.render.pixel_aspect_x",
167         "scene.render.pixel_aspect_y",
168         "scene.render.resolution_percentage",
169         "scene.render.resolution_x",
170         "scene.render.resolution_y",
171         "scene.render.use_fields",
172         "scene.render.use_fields_still",
173     ]
174
175     preset_subdir = "render"
176
177
178 class AddPresetSSS(AddPresetBase, bpy.types.Operator):
179     '''Add a Subsurface Scattering Preset'''
180     bl_idname = "material.sss_preset_add"
181     bl_label = "Add SSS Preset"
182     preset_menu = "MATERIAL_MT_sss_presets"
183
184     preset_defines = [
185         "material = (bpy.context.material.active_node_material if bpy.context.material.active_node_material else bpy.context.material)"
186     ]
187
188     preset_values = [
189         "material.subsurface_scattering.back",
190         "material.subsurface_scattering.color",
191         "material.subsurface_scattering.color_factor",
192         "material.subsurface_scattering.error_threshold",
193         "material.subsurface_scattering.front",
194         "material.subsurface_scattering.ior",
195         "material.subsurface_scattering.radius",
196         "material.subsurface_scattering.scale",
197         "material.subsurface_scattering.texture_factor",
198     ]
199
200     preset_subdir = "sss"
201
202
203 class AddPresetCloth(AddPresetBase, bpy.types.Operator):
204     '''Add a Cloth Preset'''
205     bl_idname = "cloth.preset_add"
206     bl_label = "Add Cloth Preset"
207     preset_menu = "CLOTH_MT_presets"
208
209     preset_defines = [
210         "cloth = bpy.context.cloth"
211     ]
212
213     preset_values = [
214         "cloth.settings.air_damping",
215         "cloth.settings.bending_stiffness",
216         "cloth.settings.mass",
217         "cloth.settings.quality",
218         "cloth.settings.spring_damping",
219         "cloth.settings.structural_stiffness",
220     ]
221
222     preset_subdir = "cloth"
223
224
225 class AddPresetSunSky(AddPresetBase, bpy.types.Operator):
226     '''Add a Sky & Atmosphere Preset'''
227     bl_idname = "lamp.sunsky_preset_add"
228     bl_label = "Add Sunsky Preset"
229     preset_menu = "LAMP_MT_sunsky_presets"
230
231     preset_defines = [
232         "sky = bpy.context.object.data.sky"
233     ]
234
235     preset_values = [
236         "sky.atmosphere_extinction",
237         "sky.atmosphere_inscattering",
238         "sky.atmosphere_turbidity",
239         "sky.backscattered_light",
240         "sky.horizon_brightness",
241         "sky.spread",
242         "sky.sun_brightness",
243         "sky.sun_intensity",
244         "sky.sun_size",
245         "sky.sky_blend",
246         "sky.sky_blend_type",
247         "sky.sky_color_space",
248         "sky.sky_exposure",
249     ]
250
251     preset_subdir = "sunsky"
252
253
254 class AddPresetInteraction(AddPresetBase, bpy.types.Operator):
255     '''Add an Application Interaction Preset'''
256     bl_idname = "wm.interaction_preset_add"
257     bl_label = "Add Interaction Preset"
258     preset_menu = "USERPREF_MT_interaction_presets"
259
260     preset_defines = [
261         "user_preferences = bpy.context.user_preferences"
262     ]
263
264     preset_values = [
265         "user_preferences.edit.use_drag_immediately",
266         "user_preferences.edit.use_insertkey_xyz_to_rgb",
267         "user_preferences.inputs.invert_mouse_zoom",
268         "user_preferences.inputs.select_mouse",
269         "user_preferences.inputs.use_emulate_numpad",
270         "user_preferences.inputs.use_mouse_continuous",
271         "user_preferences.inputs.use_mouse_emulate_3_button",
272         "user_preferences.inputs.view_rotate_method",
273         "user_preferences.inputs.view_zoom_axis",
274         "user_preferences.inputs.view_zoom_method",
275     ]
276
277     preset_subdir = "interaction"
278
279
280 class AddPresetKeyconfig(AddPresetBase, bpy.types.Operator):
281     '''Add a Keyconfig Preset'''
282     bl_idname = "wm.keyconfig_preset_add"
283     bl_label = "Add Keyconfig Preset"
284     preset_menu = "USERPREF_MT_keyconfigs"
285     preset_subdir = "keyconfig"
286
287     def add(self, context, filepath):
288         bpy.ops.wm.keyconfig_export(filepath=filepath)
289         bpy.utils.keyconfig_set(filepath)
290
291     def pre_cb(self, context):
292         keyconfigs = bpy.context.window_manager.keyconfigs
293         if self.remove_active:
294             preset_menu_class = getattr(bpy.types, self.preset_menu)
295             preset_menu_class.bl_label = keyconfigs.active.name
296
297     def post_cb(self, context):
298         keyconfigs = bpy.context.window_manager.keyconfigs
299         if self.remove_active:
300             keyconfigs.remove(keyconfigs.active)
301
302
303 class AddPresetOperator(AddPresetBase, bpy.types.Operator):
304     '''Add an Application Interaction Preset'''
305     bl_idname = "wm.operator_preset_add"
306     bl_label = "Operator Preset"
307     preset_menu = "WM_MT_operator_presets"
308
309     operator = bpy.props.StringProperty(name="Operator", maxlen=64, options={'HIDDEN'})
310
311     # XXX, not ideal
312     preset_defines = [
313         "op = bpy.context.space_data.operator",
314     ]
315
316     @property
317     def preset_subdir(self):
318         return __class__.operator_path(self.operator)
319
320     @property
321     def preset_values(self):
322         properties_blacklist = bpy.types.Operator.bl_rna.properties.keys()
323
324         prefix, suffix = self.operator.split("_OT_", 1)
325         operator_rna = getattr(getattr(bpy.ops, prefix.lower()), suffix).get_rna().bl_rna
326
327         ret = []
328         for prop_id, prop in operator_rna.properties.items():
329             if (not prop.is_hidden) and prop_id not in properties_blacklist:
330                 ret.append("op.%s" % prop_id)
331
332         return ret
333
334     @staticmethod
335     def operator_path(operator):
336         import os
337         prefix, suffix = operator.split("_OT_", 1)
338         return os.path.join("operator", "%s.%s" % (prefix.lower(), suffix))
339
340
341 class WM_MT_operator_presets(bpy.types.Menu):
342     bl_label = "Operator Presets"
343
344     def draw(self, context):
345         self.operator = context.space_data.operator.bl_idname
346         bpy.types.Menu.draw_preset(self, context)
347
348     @property
349     def preset_subdir(self):
350         return AddPresetOperator.operator_path(self.operator)
351
352     preset_operator = "script.execute_preset"