098f5b02c8ad170f3c730818ad33d0c5a572864a
[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 from bpy.types import Menu, Operator, Panel, WindowManager
23 from bpy.props import StringProperty, BoolProperty
24
25 # For preset popover menu
26 WindowManager.preset_name = StringProperty(
27     name="Preset Name",
28     description="Name for new preset",
29     default="New Preset"
30 )
31
32
33 class AddPresetBase:
34     """Base preset class, only for subclassing
35     subclasses must define
36      - preset_values
37      - preset_subdir """
38     # bl_idname = "script.preset_base_add"
39     # bl_label = "Add a Python Preset"
40
41     # only because invoke_props_popup requires. Also do not add to search menu.
42     bl_options = {'REGISTER', 'INTERNAL'}
43
44     name: StringProperty(
45         name="Name",
46         description="Name of the preset, used to make the path name",
47         maxlen=64,
48         options={'SKIP_SAVE'},
49     )
50     remove_name: BoolProperty(
51         default=False,
52         options={'HIDDEN', 'SKIP_SAVE'},
53     )
54     remove_active: BoolProperty(
55         default=False,
56         options={'HIDDEN', 'SKIP_SAVE'},
57     )
58
59     @staticmethod
60     def as_filename(name):  # could reuse for other presets
61
62         # lazy init maketrans
63         def maketrans_init():
64             cls = AddPresetBase
65             attr = "_as_filename_trans"
66
67             trans = getattr(cls, attr, None)
68             if trans is None:
69                 trans = str.maketrans({char: "_" for char in " !@#$%^&*(){}:\";'[]<>,.\\/?"})
70                 setattr(cls, attr, trans)
71             return trans
72
73         name = name.lower().strip()
74         name = bpy.path.display_name_to_filepath(name)
75         trans = maketrans_init()
76         return name.translate(trans)
77
78     def execute(self, context):
79         import os
80
81         if hasattr(self, "pre_cb"):
82             self.pre_cb(context)
83
84         preset_menu_class = getattr(bpy.types, self.preset_menu)
85
86         is_xml = getattr(preset_menu_class, "preset_type", None) == 'XML'
87
88         if is_xml:
89             ext = ".xml"
90         else:
91             ext = ".py"
92
93         name = self.name.strip()
94         if not (self.remove_name or self.remove_active):
95
96             if not name:
97                 return {'FINISHED'}
98
99             # Reset preset name
100             wm = bpy.data.window_managers[0]
101             if name == wm.preset_name:
102                 wm.preset_name = 'New Preset'
103
104             filename = self.as_filename(name)
105
106             target_path = os.path.join("presets", self.preset_subdir)
107             target_path = bpy.utils.user_resource('SCRIPTS',
108                                                   target_path,
109                                                   create=True)
110
111             if not target_path:
112                 self.report({'WARNING'}, "Failed to create presets path")
113                 return {'CANCELLED'}
114
115             filepath = os.path.join(target_path, filename) + ext
116
117             if hasattr(self, "add"):
118                 self.add(context, filepath)
119             else:
120                 print("Writing Preset: %r" % filepath)
121
122                 if is_xml:
123                     import rna_xml
124                     rna_xml.xml_file_write(context,
125                                            filepath,
126                                            preset_menu_class.preset_xml_map)
127                 else:
128
129                     def rna_recursive_attr_expand(value, rna_path_step, level):
130                         if isinstance(value, bpy.types.PropertyGroup):
131                             for sub_value_attr in value.bl_rna.properties.keys():
132                                 if sub_value_attr == "rna_type":
133                                     continue
134                                 sub_value = getattr(value, sub_value_attr)
135                                 rna_recursive_attr_expand(sub_value, "%s.%s" % (rna_path_step, sub_value_attr), level)
136                         elif type(value).__name__ == "bpy_prop_collection_idprop":  # could use nicer method
137                             file_preset.write("%s.clear()\n" % rna_path_step)
138                             for sub_value in value:
139                                 file_preset.write("item_sub_%d = %s.add()\n" % (level, rna_path_step))
140                                 rna_recursive_attr_expand(sub_value, "item_sub_%d" % level, level + 1)
141                         else:
142                             # convert thin wrapped sequences
143                             # to simple lists to repr()
144                             try:
145                                 value = value[:]
146                             except:
147                                 pass
148
149                             file_preset.write("%s = %r\n" % (rna_path_step, value))
150
151                     file_preset = open(filepath, 'w', encoding="utf-8")
152                     file_preset.write("import bpy\n")
153
154                     if hasattr(self, "preset_defines"):
155                         for rna_path in self.preset_defines:
156                             exec(rna_path)
157                             file_preset.write("%s\n" % rna_path)
158                         file_preset.write("\n")
159
160                     for rna_path in self.preset_values:
161                         value = eval(rna_path)
162                         rna_recursive_attr_expand(value, rna_path, 1)
163
164                     file_preset.close()
165
166             preset_menu_class.bl_label = bpy.path.display_name(filename)
167
168         else:
169             if self.remove_active:
170                 name = preset_menu_class.bl_label
171
172             # fairly sloppy but convenient.
173             filepath = bpy.utils.preset_find(name,
174                                              self.preset_subdir,
175                                              ext=ext)
176
177             if not filepath:
178                 filepath = bpy.utils.preset_find(name,
179                                                  self.preset_subdir,
180                                                  display_name=True,
181                                                  ext=ext)
182
183             if not filepath:
184                 return {'CANCELLED'}
185
186             try:
187                 if hasattr(self, "remove"):
188                     self.remove(context, filepath)
189                 else:
190                     os.remove(filepath)
191             except Exception as e:
192                 self.report({'ERROR'}, "Unable to remove preset: %r" % e)
193                 import traceback
194                 traceback.print_exc()
195                 return {'CANCELLED'}
196
197             # XXX, stupid!
198             preset_menu_class.bl_label = "Presets"
199
200         if hasattr(self, "post_cb"):
201             self.post_cb(context)
202
203         return {'FINISHED'}
204
205     def check(self, context):
206         self.name = self.as_filename(self.name.strip())
207
208     def invoke(self, context, event):
209         if not (self.remove_active or self.remove_name):
210             wm = context.window_manager
211             return wm.invoke_props_dialog(self)
212         else:
213             return self.execute(context)
214
215
216 class ExecutePreset(Operator):
217     """Execute a preset"""
218     bl_idname = "script.execute_preset"
219     bl_label = "Execute a Python Preset"
220
221     filepath: StringProperty(
222         subtype='FILE_PATH',
223         options={'SKIP_SAVE'},
224     )
225     menu_idname: StringProperty(
226         name="Menu ID Name",
227         description="ID name of the menu this was called from",
228         options={'SKIP_SAVE'},
229     )
230
231     def execute(self, context):
232         from os.path import basename, splitext
233         filepath = self.filepath
234
235         # change the menu title to the most recently chosen option
236         preset_class = getattr(bpy.types, self.menu_idname)
237         preset_class.bl_label = bpy.path.display_name(basename(filepath))
238
239         ext = splitext(filepath)[1].lower()
240
241         if ext not in {".py", ".xml"}:
242             self.report({'ERROR'}, "unknown filetype: %r" % ext)
243             return {'CANCELLED'}
244
245         if hasattr(preset_class, "reset_cb"):
246             preset_class.reset_cb(context)
247
248         if ext == ".py":
249             try:
250                 bpy.utils.execfile(filepath)
251             except Exception as ex:
252                 self.report({'ERROR'}, "Failed to execute the preset: " + repr(ex))
253
254         elif ext == ".xml":
255             import rna_xml
256             rna_xml.xml_file_run(context,
257                                  filepath,
258                                  preset_class.preset_xml_map)
259
260         if hasattr(preset_class, "post_cb"):
261             preset_class.post_cb(context)
262
263         return {'FINISHED'}
264
265
266 class AddPresetRender(AddPresetBase, Operator):
267     """Add or remove a Render Preset"""
268     bl_idname = "render.preset_add"
269     bl_label = "Add Render Preset"
270     preset_menu = "RENDER_PT_presets"
271
272     preset_defines = [
273         "scene = bpy.context.scene"
274     ]
275
276     preset_values = [
277         "scene.render.fps",
278         "scene.render.fps_base",
279         "scene.render.pixel_aspect_x",
280         "scene.render.pixel_aspect_y",
281         "scene.render.resolution_percentage",
282         "scene.render.resolution_x",
283         "scene.render.resolution_y",
284     ]
285
286     preset_subdir = "render"
287
288
289 class AddPresetCamera(AddPresetBase, Operator):
290     """Add or remove a Camera Preset"""
291     bl_idname = "camera.preset_add"
292     bl_label = "Add Camera Preset"
293     preset_menu = "CAMERA_PT_presets"
294
295     preset_defines = [
296         "cam = bpy.context.camera"
297     ]
298
299     preset_subdir = "camera"
300
301     use_focal_length: BoolProperty(
302         name="Include Focal Length",
303         description="Include focal length into the preset",
304         options={'SKIP_SAVE'},
305     )
306
307     @property
308     def preset_values(self):
309         preset_values = [
310             "cam.sensor_width",
311             "cam.sensor_height",
312             "cam.sensor_fit"
313         ]
314         if self.use_focal_length:
315             preset_values.append("cam.lens")
316             preset_values.append("cam.lens_unit")
317         return preset_values
318
319
320 class AddPresetSafeAreas(AddPresetBase, Operator):
321     """Add or remove a Safe Areas Preset"""
322     bl_idname = "safe_areas.preset_add"
323     bl_label = "Add Safe Area Preset"
324     preset_menu = "SAFE_AREAS_PT_presets"
325
326     preset_defines = [
327         "safe_areas = bpy.context.scene.safe_areas"
328     ]
329
330     preset_values = [
331         "safe_areas.title",
332         "safe_areas.action",
333         "safe_areas.title_center",
334         "safe_areas.action_center",
335     ]
336
337     preset_subdir = "safe_areas"
338
339
340 class AddPresetCloth(AddPresetBase, Operator):
341     """Add or remove a Cloth Preset"""
342     bl_idname = "cloth.preset_add"
343     bl_label = "Add Cloth Preset"
344     preset_menu = "CLOTH_PT_presets"
345
346     preset_defines = [
347         "cloth = bpy.context.cloth"
348     ]
349
350     preset_values = [
351         "cloth.settings.quality",
352         "cloth.settings.mass",
353         "cloth.settings.air_damping",
354         "cloth.settings.bending_model",
355         "cloth.settings.tension_stiffness",
356         "cloth.settings.compression_stiffness",
357         "cloth.settings.shear_stiffness",
358         "cloth.settings.bending_stiffness",
359         "cloth.settings.tension_damping",
360         "cloth.settings.compression_damping",
361         "cloth.settings.shear_damping",
362         "cloth.settings.bending_damping",
363     ]
364
365     preset_subdir = "cloth"
366
367
368 class AddPresetFluid(AddPresetBase, Operator):
369     """Add or remove a Fluid Preset"""
370     bl_idname = "fluid.preset_add"
371     bl_label = "Add Fluid Preset"
372     preset_menu = "FLUID_PT_presets"
373
374     preset_defines = [
375         "fluid = bpy.context.fluid"
376     ]
377
378     preset_values = [
379         "fluid.settings.viscosity_base",
380         "fluid.settings.viscosity_exponent",
381     ]
382
383     preset_subdir = "fluid"
384
385
386 class AddPresetHairDynamics(AddPresetBase, Operator):
387     """Add or remove a Hair Dynamics Preset"""
388     bl_idname = "particle.hair_dynamics_preset_add"
389     bl_label = "Add Hair Dynamics Preset"
390     preset_menu = "PARTICLE_PT_hair_dynamics_presets"
391
392     preset_defines = [
393         "psys = bpy.context.particle_system",
394         "cloth = bpy.context.particle_system.cloth",
395         "settings = bpy.context.particle_system.cloth.settings",
396         "collision = bpy.context.particle_system.cloth.collision_settings",
397     ]
398
399     preset_subdir = "hair_dynamics"
400
401     preset_values = [
402         "settings.quality",
403         "settings.mass",
404         "settings.bending_stiffness",
405         "psys.settings.bending_random",
406         "settings.bending_damping",
407         "settings.air_damping",
408         "settings.internal_friction",
409         "settings.density_target",
410         "settings.density_strength",
411         "settings.voxel_cell_size",
412         "settings.pin_stiffness",
413     ]
414
415
416 class AddPresetTrackingCamera(AddPresetBase, Operator):
417     """Add or remove a Tracking Camera Intrinsics Preset"""
418     bl_idname = "clip.camera_preset_add"
419     bl_label = "Add Camera Preset"
420     preset_menu = "CLIP_PT_camera_presets"
421
422     preset_defines = [
423         "camera = bpy.context.edit_movieclip.tracking.camera"
424     ]
425
426     preset_subdir = "tracking_camera"
427
428     use_focal_length: BoolProperty(
429         name="Include Focal Length",
430         description="Include focal length into the preset",
431         options={'SKIP_SAVE'},
432         default=True
433     )
434
435     @property
436     def preset_values(self):
437         preset_values = [
438             "camera.sensor_width",
439             "camera.pixel_aspect",
440             "camera.k1",
441             "camera.k2",
442             "camera.k3"
443         ]
444         if self.use_focal_length:
445             preset_values.append("camera.units")
446             preset_values.append("camera.focal_length")
447         return preset_values
448
449
450 class AddPresetTrackingTrackColor(AddPresetBase, Operator):
451     """Add or remove a Clip Track Color Preset"""
452     bl_idname = "clip.track_color_preset_add"
453     bl_label = "Add Track Color Preset"
454     preset_menu = "CLIP_PT_track_color_presets"
455
456     preset_defines = [
457         "track = bpy.context.edit_movieclip.tracking.tracks.active"
458     ]
459
460     preset_values = [
461         "track.color",
462         "track.use_custom_color"
463     ]
464
465     preset_subdir = "tracking_track_color"
466
467
468 class AddPresetTrackingSettings(AddPresetBase, Operator):
469     """Add or remove a motion tracking settings preset"""
470     bl_idname = "clip.tracking_settings_preset_add"
471     bl_label = "Add Tracking Settings Preset"
472     preset_menu = "CLIP_PT_tracking_settings_presets"
473
474     preset_defines = [
475         "settings = bpy.context.edit_movieclip.tracking.settings"
476     ]
477
478     preset_values = [
479         "settings.default_correlation_min",
480         "settings.default_pattern_size",
481         "settings.default_search_size",
482         "settings.default_frames_limit",
483         "settings.default_pattern_match",
484         "settings.default_margin",
485         "settings.default_motion_model",
486         "settings.use_default_brute",
487         "settings.use_default_normalization",
488         "settings.use_default_mask",
489         "settings.use_default_red_channel",
490         "settings.use_default_green_channel",
491         "settings.use_default_blue_channel"
492         "settings.default_weight"
493     ]
494
495     preset_subdir = "tracking_settings"
496
497
498 class AddPresetNodeColor(AddPresetBase, Operator):
499     """Add or remove a Node Color Preset"""
500     bl_idname = "node.node_color_preset_add"
501     bl_label = "Add Node Color Preset"
502     preset_menu = "NODE_PT_node_color_presets"
503
504     preset_defines = [
505         "node = bpy.context.active_node"
506     ]
507
508     preset_values = [
509         "node.color",
510         "node.use_custom_color"
511     ]
512
513     preset_subdir = "node_color"
514
515
516 class AddPresetInterfaceTheme(AddPresetBase, Operator):
517     """Add or remove a theme preset"""
518     bl_idname = "wm.interface_theme_preset_add"
519     bl_label = "Add Theme Preset"
520     preset_menu = "USERPREF_MT_interface_theme_presets"
521     preset_subdir = "interface_theme"
522
523
524 class AddPresetKeyconfig(AddPresetBase, Operator):
525     """Add or remove a Key-config Preset"""
526     bl_idname = "wm.keyconfig_preset_add"
527     bl_label = "Add Keyconfig Preset"
528     preset_menu = "USERPREF_MT_keyconfigs"
529     preset_subdir = "keyconfig"
530
531     def add(self, context, filepath):
532         bpy.ops.preferences.keyconfig_export(filepath=filepath)
533         bpy.utils.keyconfig_set(filepath)
534
535     def pre_cb(self, context):
536         keyconfigs = bpy.context.window_manager.keyconfigs
537         if self.remove_active:
538             preset_menu_class = getattr(bpy.types, self.preset_menu)
539             preset_menu_class.bl_label = keyconfigs.active.name
540
541     def post_cb(self, context):
542         keyconfigs = bpy.context.window_manager.keyconfigs
543         if self.remove_active:
544             keyconfigs.remove(keyconfigs.active)
545
546
547 class AddPresetOperator(AddPresetBase, Operator):
548     """Add or remove an Operator Preset"""
549     bl_idname = "wm.operator_preset_add"
550     bl_label = "Operator Preset"
551     preset_menu = "WM_MT_operator_presets"
552
553     operator: StringProperty(
554         name="Operator",
555         maxlen=64,
556         options={'HIDDEN', 'SKIP_SAVE'},
557     )
558
559     preset_defines = [
560         "op = bpy.context.active_operator",
561     ]
562
563     @property
564     def preset_subdir(self):
565         return AddPresetOperator.operator_path(self.operator)
566
567     @property
568     def preset_values(self):
569         properties_blacklist = Operator.bl_rna.properties.keys()
570
571         prefix, suffix = self.operator.split("_OT_", 1)
572         op = getattr(getattr(bpy.ops, prefix.lower()), suffix)
573         operator_rna = op.get_rna_type()
574         del op
575
576         ret = []
577         for prop_id, prop in operator_rna.properties.items():
578             if not (prop.is_hidden or prop.is_skip_save):
579                 if prop_id not in properties_blacklist:
580                     ret.append("op.%s" % prop_id)
581
582         return ret
583
584     @staticmethod
585     def operator_path(operator):
586         import os
587         prefix, suffix = operator.split("_OT_", 1)
588         return os.path.join("operator", "%s.%s" % (prefix.lower(), suffix))
589
590
591 class WM_MT_operator_presets(Menu):
592     bl_label = "Operator Presets"
593
594     def draw(self, context):
595         self.operator = context.active_operator.bl_idname
596
597         # dummy 'default' menu item
598         layout = self.layout
599         layout.operator("wm.operator_defaults")
600         layout.separator()
601
602         Menu.draw_preset(self, context)
603
604     @property
605     def preset_subdir(self):
606         return AddPresetOperator.operator_path(self.operator)
607
608     preset_operator = "script.execute_preset"
609
610
611 class AddPresetGpencilBrush(AddPresetBase, Operator):
612     """Add or remove grease pencil brush preset"""
613     bl_idname = "scene.gpencil_brush_preset_add"
614     bl_label = "Add Grease Pencil Brush Preset"
615     preset_menu = "VIEW3D_PT_gpencil_brush_presets"
616
617     preset_defines = [
618         "brush = bpy.context.tool_settings.gpencil_paint.brush",
619         "settings = brush.gpencil_settings"
620     ]
621
622     preset_values = [
623         "settings.input_samples",
624         "settings.active_smooth_factor",
625         "settings.angle",
626         "settings.angle_factor",
627         "settings.use_settings_stabilizer",
628         "brush.smooth_stroke_radius",
629         "brush.smooth_stroke_factor",
630         "settings.pen_smooth_factor",
631         "settings.pen_smooth_steps",
632         "settings.pen_thick_smooth_factor",
633         "settings.pen_thick_smooth_steps",
634         "settings.pen_subdivision_steps",
635         "settings.random_subdiv",
636         "settings.use_settings_random",
637         "settings.random_pressure",
638         "settings.random_strength",
639         "settings.uv_random",
640         "settings.pen_jitter",
641         "settings.use_jitter_pressure",
642         "settings.trim",
643     ]
644
645     preset_subdir = "gpencil_brush"
646
647
648 class AddPresetGpencilMaterial(AddPresetBase, Operator):
649     """Add or remove grease pencil material preset"""
650     bl_idname = "scene.gpencil_material_preset_add"
651     bl_label = "Add Grease Pencil Material Preset"
652     preset_menu = "MATERIAL_PT_gpencil_material_presets"
653
654     preset_defines = [
655         "material = bpy.context.object.active_material",
656         "gpcolor = material.grease_pencil"
657     ]
658
659     preset_values = [
660         "gpcolor.mode",
661         "gpcolor.stroke_style",
662         "gpcolor.color",
663         "gpcolor.stroke_image",
664         "gpcolor.pixel_size",
665         "gpcolor.use_stroke_pattern",
666         "gpcolor.fill_style",
667         "gpcolor.fill_color",
668         "gpcolor.fill_image",
669         "gpcolor.gradient_type",
670         "gpcolor.mix_color",
671         "gpcolor.mix_factor",
672         "gpcolor.flip",
673         "gpcolor.pattern_shift",
674         "gpcolor.pattern_scale",
675         "gpcolor.pattern_radius",
676         "gpcolor.pattern_angle",
677         "gpcolor.pattern_gridsize",
678         "gpcolor.use_fill_pattern",
679         "gpcolor.texture_offset",
680         "gpcolor.texture_scale",
681         "gpcolor.texture_angle",
682         "gpcolor.texture_opacity",
683         "gpcolor.texture_clamp",
684         "gpcolor.texture_mix",
685         "gpcolor.mix_factor",
686         "gpcolor.show_stroke",
687         "gpcolor.show_fill",
688     ]
689
690     preset_subdir = "gpencil_material"
691
692
693 classes = (
694     AddPresetCamera,
695     AddPresetCloth,
696     AddPresetFluid,
697     AddPresetHairDynamics,
698     AddPresetInterfaceTheme,
699     AddPresetKeyconfig,
700     AddPresetNodeColor,
701     AddPresetOperator,
702     AddPresetRender,
703     AddPresetSafeAreas,
704     AddPresetTrackingCamera,
705     AddPresetTrackingSettings,
706     AddPresetTrackingTrackColor,
707     AddPresetGpencilBrush,
708     AddPresetGpencilMaterial,
709     ExecutePreset,
710     WM_MT_operator_presets,
711 )