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