fix for presets reusing previous options (which broke preset add/remove)
[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-80 compliant>
20
21 import bpy
22 from bpy.types import Menu, Operator
23
24
25 class AddPresetBase():
26     '''Base preset class, only for subclassing
27     subclasses must define
28      - preset_values
29      - preset_subdir '''
30     # bl_idname = "script.preset_base_add"
31     # bl_label = "Add a Python Preset"
32     bl_options = {'REGISTER'}  # only because invoke_props_popup requires.
33
34     name = bpy.props.StringProperty(
35             name="Name",
36             description="Name of the preset, used to make the path name",
37             maxlen=64,
38             options={'SKIP_SAVE'},
39             )
40     remove_active = bpy.props.BoolProperty(
41             default=False,
42             options={'HIDDEN', 'SKIP_SAVE'},
43             )
44
45     @staticmethod
46     def as_filename(name):  # could reuse for other presets
47         for char in " !@#$%^&*(){}:\";'[]<>,.\\/?":
48             name = name.replace(char, '_')
49         return name.lower().strip()
50
51     def execute(self, context):
52         import os
53
54         if hasattr(self, "pre_cb"):
55             self.pre_cb(context)
56
57         preset_menu_class = getattr(bpy.types, self.preset_menu)
58
59         is_xml = getattr(preset_menu_class, "preset_type", None) == 'XML'
60
61         if is_xml:
62             ext = ".xml"
63         else:
64             ext = ".py"
65
66         if not self.remove_active:
67             name = self.name.strip()
68             if not name:
69                 return {'FINISHED'}
70
71             filename = self.as_filename(name)
72
73             target_path = os.path.join("presets", self.preset_subdir)
74             target_path = bpy.utils.user_resource('SCRIPTS',
75                                                   target_path,
76                                                   create=True)
77
78             if not target_path:
79                 self.report({'WARNING'}, "Failed to create presets path")
80                 return {'CANCELLED'}
81
82             filepath = os.path.join(target_path, filename) + ext
83
84             if hasattr(self, "add"):
85                 self.add(context, filepath)
86             else:
87                 print("Writing Preset: %r" % filepath)
88
89                 if is_xml:
90                     import rna_xml
91                     rna_xml.xml_file_write(context,
92                                            filepath,
93                                            preset_menu_class.preset_xml_map)
94                 else:
95                     file_preset = open(filepath, 'w')
96                     file_preset.write("import bpy\n")
97
98                     if hasattr(self, "preset_defines"):
99                         for rna_path in self.preset_defines:
100                             exec(rna_path)
101                             file_preset.write("%s\n" % rna_path)
102                         file_preset.write("\n")
103
104                     for rna_path in self.preset_values:
105                         value = eval(rna_path)
106                         # convert thin wrapped sequences
107                         # to simple lists to repr()
108                         try:
109                             value = value[:]
110                         except:
111                             pass
112
113                         file_preset.write("%s = %r\n" % (rna_path, value))
114
115                     file_preset.close()
116
117             preset_menu_class.bl_label = bpy.path.display_name(filename)
118
119         else:
120             preset_active = preset_menu_class.bl_label
121
122             # fairly sloppy but convenient.
123             filepath = bpy.utils.preset_find(preset_active,
124                                              self.preset_subdir,
125                                              ext=ext)
126
127             if not filepath:
128                 filepath = bpy.utils.preset_find(preset_active,
129                                                  self.preset_subdir,
130                                                  display_name=True,
131                                                  ext=ext)
132
133             if not filepath:
134                 return {'CANCELLED'}
135
136             if hasattr(self, "remove"):
137                 self.remove(context, filepath)
138             else:
139                 try:
140                     os.remove(filepath)
141                 except:
142                     import traceback
143                     traceback.print_exc()
144
145             # XXX, stupid!
146             preset_menu_class.bl_label = "Presets"
147
148         if hasattr(self, "post_cb"):
149             self.post_cb(context)
150
151         return {'FINISHED'}
152
153     def check(self, context):
154         self.name = self.as_filename(self.name.strip())
155
156     def invoke(self, context, event):
157         if not self.remove_active:
158             wm = context.window_manager
159             return wm.invoke_props_dialog(self)
160         else:
161             return self.execute(context)
162
163
164 class ExecutePreset(Operator):
165     '''Execute a preset'''
166     bl_idname = "script.execute_preset"
167     bl_label = "Execute a Python Preset"
168
169     filepath = bpy.props.StringProperty(
170             name="Path",
171             description="Path of the Python file to execute",
172             maxlen=512,
173             )
174     menu_idname = bpy.props.StringProperty(
175             name="Menu ID Name",
176             description="ID name of the menu this was called from",
177             )
178
179     def execute(self, context):
180         from os.path import basename, splitext
181         filepath = self.filepath
182
183         # change the menu title to the most recently chosen option
184         preset_class = getattr(bpy.types, self.menu_idname)
185         preset_class.bl_label = bpy.path.display_name(basename(filepath))
186
187         ext = splitext(filepath)[1].lower()
188
189         # execute the preset using script.python_file_run
190         if ext == ".py":
191             bpy.ops.script.python_file_run(filepath=filepath)
192         elif ext == ".xml":
193             import rna_xml
194             rna_xml.xml_file_run(context,
195                                  filepath,
196                                  preset_class.preset_xml_map)
197         else:
198             self.report({'ERROR'}, "unknown filetype: %r" % ext)
199             return {'CANCELLED '}
200
201         return {'FINISHED'}
202
203
204 class AddPresetRender(AddPresetBase, Operator):
205     '''Add a Render Preset'''
206     bl_idname = "render.preset_add"
207     bl_label = "Add Render Preset"
208     preset_menu = "RENDER_MT_presets"
209
210     preset_defines = [
211         "scene = bpy.context.scene"
212     ]
213
214     preset_values = [
215         "scene.render.field_order",
216         "scene.render.fps",
217         "scene.render.fps_base",
218         "scene.render.pixel_aspect_x",
219         "scene.render.pixel_aspect_y",
220         "scene.render.resolution_percentage",
221         "scene.render.resolution_x",
222         "scene.render.resolution_y",
223         "scene.render.use_fields",
224         "scene.render.use_fields_still",
225     ]
226
227     preset_subdir = "render"
228
229
230 class AddPresetCamera(AddPresetBase, Operator):
231     '''Add a Camera Preset'''
232     bl_idname = "camera.preset_add"
233     bl_label = "Add Camera Preset"
234     preset_menu = "CAMERA_MT_presets"
235
236     preset_defines = [
237         "cam = bpy.context.object.data"
238     ]
239
240     preset_values = [
241         "cam.sensor_width",
242         "cam.sensor_height",
243         "cam.sensor_fit"
244     ]
245
246     preset_subdir = "camera"
247
248
249 class AddPresetSSS(AddPresetBase, Operator):
250     '''Add a Subsurface Scattering Preset'''
251     bl_idname = "material.sss_preset_add"
252     bl_label = "Add SSS Preset"
253     preset_menu = "MATERIAL_MT_sss_presets"
254
255     preset_defines = [
256         ("material = "
257          "bpy.context.material.active_node_material "
258          "if bpy.context.material.active_node_material "
259          "else bpy.context.material")
260     ]
261
262     preset_values = [
263         "material.subsurface_scattering.back",
264         "material.subsurface_scattering.color",
265         "material.subsurface_scattering.color_factor",
266         "material.subsurface_scattering.error_threshold",
267         "material.subsurface_scattering.front",
268         "material.subsurface_scattering.ior",
269         "material.subsurface_scattering.radius",
270         "material.subsurface_scattering.scale",
271         "material.subsurface_scattering.texture_factor",
272     ]
273
274     preset_subdir = "sss"
275
276
277 class AddPresetCloth(AddPresetBase, Operator):
278     '''Add a Cloth Preset'''
279     bl_idname = "cloth.preset_add"
280     bl_label = "Add Cloth Preset"
281     preset_menu = "CLOTH_MT_presets"
282
283     preset_defines = [
284         "cloth = bpy.context.cloth"
285     ]
286
287     preset_values = [
288         "cloth.settings.air_damping",
289         "cloth.settings.bending_stiffness",
290         "cloth.settings.mass",
291         "cloth.settings.quality",
292         "cloth.settings.spring_damping",
293         "cloth.settings.structural_stiffness",
294     ]
295
296     preset_subdir = "cloth"
297
298
299 class AddPresetSunSky(AddPresetBase, Operator):
300     '''Add a Sky & Atmosphere Preset'''
301     bl_idname = "lamp.sunsky_preset_add"
302     bl_label = "Add Sunsky Preset"
303     preset_menu = "LAMP_MT_sunsky_presets"
304
305     preset_defines = [
306         "sky = bpy.context.object.data.sky"
307     ]
308
309     preset_values = [
310         "sky.atmosphere_extinction",
311         "sky.atmosphere_inscattering",
312         "sky.atmosphere_turbidity",
313         "sky.backscattered_light",
314         "sky.horizon_brightness",
315         "sky.spread",
316         "sky.sun_brightness",
317         "sky.sun_intensity",
318         "sky.sun_size",
319         "sky.sky_blend",
320         "sky.sky_blend_type",
321         "sky.sky_color_space",
322         "sky.sky_exposure",
323     ]
324
325     preset_subdir = "sunsky"
326
327
328 class AddPresetInteraction(AddPresetBase, Operator):
329     '''Add an Application Interaction Preset'''
330     bl_idname = "wm.interaction_preset_add"
331     bl_label = "Add Interaction Preset"
332     preset_menu = "USERPREF_MT_interaction_presets"
333
334     preset_defines = [
335         "user_preferences = bpy.context.user_preferences"
336     ]
337
338     preset_values = [
339         "user_preferences.edit.use_drag_immediately",
340         "user_preferences.edit.use_insertkey_xyz_to_rgb",
341         "user_preferences.inputs.invert_mouse_zoom",
342         "user_preferences.inputs.select_mouse",
343         "user_preferences.inputs.use_emulate_numpad",
344         "user_preferences.inputs.use_mouse_continuous",
345         "user_preferences.inputs.use_mouse_emulate_3_button",
346         "user_preferences.inputs.view_rotate_method",
347         "user_preferences.inputs.view_zoom_axis",
348         "user_preferences.inputs.view_zoom_method",
349     ]
350
351     preset_subdir = "interaction"
352
353
354 class AddPresetTrackingCamera(AddPresetBase, Operator):
355     '''Add a Tracking Camera Intrinsics  Preset'''
356     bl_idname = "clip.camera_preset_add"
357     bl_label = "Add Camera Preset"
358     preset_menu = "CLIP_MT_camera_presets"
359
360     preset_defines = [
361         "camera = bpy.context.edit_movieclip.tracking.camera"
362     ]
363
364     preset_values = [
365         "camera.sensor_width",
366         "camera.units",
367         "camera.focal_length",
368         "camera.pixel_aspect",
369         "camera.k1",
370         "camera.k2",
371         "camera.k3"
372     ]
373
374     preset_subdir = "tracking_camera"
375
376
377 class AddPresetTrackingTrackColor(AddPresetBase, Operator):
378     '''Add a Clip Track Color Preset'''
379     bl_idname = "clip.track_color_preset_add"
380     bl_label = "Add Track Color Preset"
381     preset_menu = "CLIP_MT_track_color_presets"
382
383     preset_defines = [
384         "track = bpy.context.edit_movieclip.tracking.tracks.active"
385     ]
386
387     preset_values = [
388         "track.color",
389         "track.use_custom_color"
390     ]
391
392     preset_subdir = "tracking_track_color"
393
394
395 class AddPresetTrackingSettings(AddPresetBase, Operator):
396     '''Add a motion tracking settings preset'''
397     bl_idname = "clip.tracking_settings_preset_add"
398     bl_label = "Add Tracking Settings Preset"
399     preset_menu = "CLIP_MT_tracking_settings_presets"
400
401     preset_defines = [
402         "settings = bpy.context.edit_movieclip.tracking.settings"
403     ]
404
405     preset_values = [
406         "settings.default_tracker",
407         "settings.default_pyramid_levels",
408         "settings.default_correlation_min",
409         "settings.default_pattern_size",
410         "settings.default_search_size",
411         "settings.default_frames_limit",
412         "settings.default_pattern_match",
413         "settings.default_margin",
414         "settings.use_default_red_channel",
415         "settings.use_default_green_channel",
416         "settings.use_default_blue_channel"
417     ]
418
419     preset_subdir = "tracking_settings"
420
421
422 class AddPresetInterfaceTheme(AddPresetBase, Operator):
423     '''Add a theme preset'''
424     bl_idname = "wm.interface_theme_preset_add"
425     bl_label = "Add Tracking Settings Preset"
426     preset_menu = "USERPREF_MT_interface_theme_presets"
427     preset_subdir = "interface_theme"
428
429
430 class AddPresetKeyconfig(AddPresetBase, Operator):
431     '''Add a Key-config Preset'''
432     bl_idname = "wm.keyconfig_preset_add"
433     bl_label = "Add Keyconfig Preset"
434     preset_menu = "USERPREF_MT_keyconfigs"
435     preset_subdir = "keyconfig"
436
437     def add(self, context, filepath):
438         bpy.ops.wm.keyconfig_export(filepath=filepath)
439         bpy.utils.keyconfig_set(filepath)
440
441     def pre_cb(self, context):
442         keyconfigs = bpy.context.window_manager.keyconfigs
443         if self.remove_active:
444             preset_menu_class = getattr(bpy.types, self.preset_menu)
445             preset_menu_class.bl_label = keyconfigs.active.name
446
447     def post_cb(self, context):
448         keyconfigs = bpy.context.window_manager.keyconfigs
449         if self.remove_active:
450             keyconfigs.remove(keyconfigs.active)
451
452
453 class AddPresetOperator(AddPresetBase, Operator):
454     '''Add an Application Interaction Preset'''
455     bl_idname = "wm.operator_preset_add"
456     bl_label = "Operator Preset"
457     preset_menu = "WM_MT_operator_presets"
458
459     operator = bpy.props.StringProperty(
460             name="Operator",
461             maxlen=64,
462             options={'HIDDEN'},
463             )
464
465     preset_defines = [
466         "op = bpy.context.active_operator",
467     ]
468
469     @property
470     def preset_subdir(self):
471         return AddPresetOperator.operator_path(self.operator)
472
473     @property
474     def preset_values(self):
475         properties_blacklist = Operator.bl_rna.properties.keys()
476
477         prefix, suffix = self.operator.split("_OT_", 1)
478         op = getattr(getattr(bpy.ops, prefix.lower()), suffix)
479         operator_rna = op.get_rna().bl_rna
480         del op
481
482         ret = []
483         for prop_id, prop in operator_rna.properties.items():
484             if not (prop.is_hidden or prop.is_skip_save):
485                 if prop_id not in properties_blacklist:
486                     ret.append("op.%s" % prop_id)
487
488         return ret
489
490     @staticmethod
491     def operator_path(operator):
492         import os
493         prefix, suffix = operator.split("_OT_", 1)
494         return os.path.join("operator", "%s.%s" % (prefix.lower(), suffix))
495
496
497 class WM_MT_operator_presets(Menu):
498     bl_label = "Operator Presets"
499
500     def draw(self, context):
501         self.operator = context.active_operator.bl_idname
502         Menu.draw_preset(self, context)
503
504     @property
505     def preset_subdir(self):
506         return AddPresetOperator.operator_path(self.operator)
507
508     preset_operator = "script.execute_preset"