3e08bd1fe6f35fe3efc8edfa31e9515390ce085d
[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 PresetMenu(Panel):
267     bl_space_type = 'PROPERTIES'
268     bl_region_type = 'HEADER'
269     bl_label = "Presets"
270     path_menu = Menu.path_menu
271
272     @classmethod
273     def draw_panel_header(cls, layout):
274         layout.emboss = 'NONE'
275         layout.popover(
276             panel=cls.__name__,
277             icon='PRESET',
278             text="",
279         )
280
281     @classmethod
282     def draw_menu(cls, layout, text=None):
283         if text is None:
284             text = cls.bl_label
285
286         layout.popover(
287             panel=cls.__name__,
288             icon='PRESET',
289             text=text,
290         )
291
292     def draw(self, context):
293         layout = self.layout
294         layout.emboss = 'PULLDOWN_MENU'
295         layout.operator_context = 'EXEC_DEFAULT'
296
297         Menu.draw_preset(self, context)
298
299
300 class AddPresetRender(AddPresetBase, Operator):
301     """Add or remove a Render Preset"""
302     bl_idname = "render.preset_add"
303     bl_label = "Add Render Preset"
304     preset_menu = "RENDER_PT_presets"
305
306     preset_defines = [
307         "scene = bpy.context.scene"
308     ]
309
310     preset_values = [
311         "scene.render.fps",
312         "scene.render.fps_base",
313         "scene.render.pixel_aspect_x",
314         "scene.render.pixel_aspect_y",
315         "scene.render.resolution_percentage",
316         "scene.render.resolution_x",
317         "scene.render.resolution_y",
318     ]
319
320     preset_subdir = "render"
321
322
323 class AddPresetCamera(AddPresetBase, Operator):
324     """Add or remove a Camera Preset"""
325     bl_idname = "camera.preset_add"
326     bl_label = "Add Camera Preset"
327     preset_menu = "CAMERA_PT_presets"
328
329     preset_defines = [
330         "cam = bpy.context.camera"
331     ]
332
333     preset_subdir = "camera"
334
335     use_focal_length: BoolProperty(
336         name="Include Focal Length",
337         description="Include focal length into the preset",
338         options={'SKIP_SAVE'},
339     )
340
341     @property
342     def preset_values(self):
343         preset_values = [
344             "cam.sensor_width",
345             "cam.sensor_height",
346             "cam.sensor_fit"
347         ]
348         if self.use_focal_length:
349             preset_values.append("cam.lens")
350             preset_values.append("cam.lens_unit")
351         return preset_values
352
353
354 class AddPresetSafeAreas(AddPresetBase, Operator):
355     """Add or remove a Safe Areas Preset"""
356     bl_idname = "safe_areas.preset_add"
357     bl_label = "Add Safe Area Preset"
358     preset_menu = "SAFE_AREAS_PT_presets"
359
360     preset_defines = [
361         "safe_areas = bpy.context.scene.safe_areas"
362     ]
363
364     preset_values = [
365         "safe_areas.title",
366         "safe_areas.action",
367         "safe_areas.title_center",
368         "safe_areas.action_center",
369     ]
370
371     preset_subdir = "safe_areas"
372
373
374 class AddPresetCloth(AddPresetBase, Operator):
375     """Add or remove a Cloth Preset"""
376     bl_idname = "cloth.preset_add"
377     bl_label = "Add Cloth Preset"
378     preset_menu = "CLOTH_PT_presets"
379
380     preset_defines = [
381         "cloth = bpy.context.cloth"
382     ]
383
384     preset_values = [
385         "cloth.settings.air_damping",
386         "cloth.settings.bending_stiffness",
387         "cloth.settings.mass",
388         "cloth.settings.quality",
389         "cloth.settings.spring_damping",
390         "cloth.settings.structural_stiffness",
391     ]
392
393     preset_subdir = "cloth"
394
395
396 class AddPresetFluid(AddPresetBase, Operator):
397     """Add or remove a Fluid Preset"""
398     bl_idname = "fluid.preset_add"
399     bl_label = "Add Fluid Preset"
400     preset_menu = "FLUID_PT_presets"
401
402     preset_defines = [
403         "fluid = bpy.context.fluid"
404     ]
405
406     preset_values = [
407         "fluid.settings.viscosity_base",
408         "fluid.settings.viscosity_exponent",
409     ]
410
411     preset_subdir = "fluid"
412
413
414 class AddPresetHairDynamics(AddPresetBase, Operator):
415     """Add or remove a Hair Dynamics Preset"""
416     bl_idname = "particle.hair_dynamics_preset_add"
417     bl_label = "Add Hair Dynamics Preset"
418     preset_menu = "PARTICLE_PT_hair_dynamics_presets"
419
420     preset_defines = [
421         "psys = bpy.context.particle_system",
422         "cloth = bpy.context.particle_system.cloth",
423         "settings = bpy.context.particle_system.cloth.settings",
424         "collision = bpy.context.particle_system.cloth.collision_settings",
425     ]
426
427     preset_subdir = "hair_dynamics"
428
429     preset_values = [
430         "settings.quality",
431         "settings.mass",
432         "settings.bending_stiffness",
433         "psys.settings.bending_random",
434         "settings.bending_damping",
435         "settings.air_damping",
436         "settings.internal_friction",
437         "settings.density_target",
438         "settings.density_strength",
439         "settings.voxel_cell_size",
440         "settings.pin_stiffness",
441     ]
442
443
444 class AddPresetInteraction(AddPresetBase, Operator):
445     """Add or remove an Application Interaction Preset"""
446     bl_idname = "wm.interaction_preset_add"
447     bl_label = "Add Interaction Preset"
448     preset_menu = "USERPREF_MT_interaction_presets"
449
450     preset_defines = [
451         "user_preferences = bpy.context.user_preferences"
452     ]
453
454     preset_values = [
455         "user_preferences.edit.use_drag_immediately",
456         "user_preferences.edit.use_insertkey_xyz_to_rgb",
457         "user_preferences.inputs.invert_mouse_zoom",
458         "user_preferences.inputs.select_mouse",
459         "user_preferences.inputs.use_emulate_numpad",
460         "user_preferences.inputs.use_mouse_continuous",
461         "user_preferences.inputs.use_mouse_emulate_3_button",
462         "user_preferences.inputs.view_rotate_method",
463         "user_preferences.inputs.view_zoom_axis",
464         "user_preferences.inputs.view_zoom_method",
465     ]
466
467     preset_subdir = "interaction"
468
469
470 class AddPresetTrackingCamera(AddPresetBase, Operator):
471     """Add or remove a Tracking Camera Intrinsics Preset"""
472     bl_idname = "clip.camera_preset_add"
473     bl_label = "Add Camera Preset"
474     preset_menu = "CLIP_PT_camera_presets"
475
476     preset_defines = [
477         "camera = bpy.context.edit_movieclip.tracking.camera"
478     ]
479
480     preset_subdir = "tracking_camera"
481
482     use_focal_length: BoolProperty(
483         name="Include Focal Length",
484         description="Include focal length into the preset",
485         options={'SKIP_SAVE'},
486         default=True
487     )
488
489     @property
490     def preset_values(self):
491         preset_values = [
492             "camera.sensor_width",
493             "camera.pixel_aspect",
494             "camera.k1",
495             "camera.k2",
496             "camera.k3"
497         ]
498         if self.use_focal_length:
499             preset_values.append("camera.units")
500             preset_values.append("camera.focal_length")
501         return preset_values
502
503
504 class AddPresetTrackingTrackColor(AddPresetBase, Operator):
505     """Add or remove a Clip Track Color Preset"""
506     bl_idname = "clip.track_color_preset_add"
507     bl_label = "Add Track Color Preset"
508     preset_menu = "CLIP_PT_track_color_presets"
509
510     preset_defines = [
511         "track = bpy.context.edit_movieclip.tracking.tracks.active"
512     ]
513
514     preset_values = [
515         "track.color",
516         "track.use_custom_color"
517     ]
518
519     preset_subdir = "tracking_track_color"
520
521
522 class AddPresetTrackingSettings(AddPresetBase, Operator):
523     """Add or remove a motion tracking settings preset"""
524     bl_idname = "clip.tracking_settings_preset_add"
525     bl_label = "Add Tracking Settings Preset"
526     preset_menu = "CLIP_PT_tracking_settings_presets"
527
528     preset_defines = [
529         "settings = bpy.context.edit_movieclip.tracking.settings"
530     ]
531
532     preset_values = [
533         "settings.default_correlation_min",
534         "settings.default_pattern_size",
535         "settings.default_search_size",
536         "settings.default_frames_limit",
537         "settings.default_pattern_match",
538         "settings.default_margin",
539         "settings.default_motion_model",
540         "settings.use_default_brute",
541         "settings.use_default_normalization",
542         "settings.use_default_mask",
543         "settings.use_default_red_channel",
544         "settings.use_default_green_channel",
545         "settings.use_default_blue_channel"
546         "settings.default_weight"
547     ]
548
549     preset_subdir = "tracking_settings"
550
551
552 class AddPresetNodeColor(AddPresetBase, Operator):
553     """Add or remove a Node Color Preset"""
554     bl_idname = "node.node_color_preset_add"
555     bl_label = "Add Node Color Preset"
556     preset_menu = "NODE_PT_node_color_presets"
557
558     preset_defines = [
559         "node = bpy.context.active_node"
560     ]
561
562     preset_values = [
563         "node.color",
564         "node.use_custom_color"
565     ]
566
567     preset_subdir = "node_color"
568
569
570 class AddPresetInterfaceTheme(AddPresetBase, Operator):
571     """Add or remove a theme preset"""
572     bl_idname = "wm.interface_theme_preset_add"
573     bl_label = "Add Theme Preset"
574     preset_menu = "USERPREF_MT_interface_theme_presets"
575     preset_subdir = "interface_theme"
576
577
578 class AddPresetKeyconfig(AddPresetBase, Operator):
579     """Add or remove a Key-config Preset"""
580     bl_idname = "wm.keyconfig_preset_add"
581     bl_label = "Add Keyconfig Preset"
582     preset_menu = "USERPREF_MT_keyconfigs"
583     preset_subdir = "keyconfig"
584
585     def add(self, context, filepath):
586         bpy.ops.wm.keyconfig_export(filepath=filepath)
587         bpy.utils.keyconfig_set(filepath)
588
589     def pre_cb(self, context):
590         keyconfigs = bpy.context.window_manager.keyconfigs
591         if self.remove_active:
592             preset_menu_class = getattr(bpy.types, self.preset_menu)
593             preset_menu_class.bl_label = keyconfigs.active.name
594
595     def post_cb(self, context):
596         keyconfigs = bpy.context.window_manager.keyconfigs
597         if self.remove_active:
598             keyconfigs.remove(keyconfigs.active)
599
600
601 class AddPresetOperator(AddPresetBase, Operator):
602     """Add or remove an Operator Preset"""
603     bl_idname = "wm.operator_preset_add"
604     bl_label = "Operator Preset"
605     preset_menu = "WM_MT_operator_presets"
606
607     operator: StringProperty(
608         name="Operator",
609         maxlen=64,
610         options={'HIDDEN', 'SKIP_SAVE'},
611     )
612
613     preset_defines = [
614         "op = bpy.context.active_operator",
615     ]
616
617     @property
618     def preset_subdir(self):
619         return AddPresetOperator.operator_path(self.operator)
620
621     @property
622     def preset_values(self):
623         properties_blacklist = Operator.bl_rna.properties.keys()
624
625         prefix, suffix = self.operator.split("_OT_", 1)
626         op = getattr(getattr(bpy.ops, prefix.lower()), suffix)
627         operator_rna = op.get_rna_type()
628         del op
629
630         ret = []
631         for prop_id, prop in operator_rna.properties.items():
632             if not (prop.is_hidden or prop.is_skip_save):
633                 if prop_id not in properties_blacklist:
634                     ret.append("op.%s" % prop_id)
635
636         return ret
637
638     @staticmethod
639     def operator_path(operator):
640         import os
641         prefix, suffix = operator.split("_OT_", 1)
642         return os.path.join("operator", "%s.%s" % (prefix.lower(), suffix))
643
644
645 class WM_MT_operator_presets(Menu):
646     bl_label = "Operator Presets"
647
648     def draw(self, context):
649         self.operator = context.active_operator.bl_idname
650
651         # dummy 'default' menu item
652         layout = self.layout
653         layout.operator("wm.operator_defaults")
654         layout.separator()
655
656         Menu.draw_preset(self, context)
657
658     @property
659     def preset_subdir(self):
660         return AddPresetOperator.operator_path(self.operator)
661
662     preset_operator = "script.execute_preset"
663
664
665 class AddPresetGpencilBrush(AddPresetBase, Operator):
666     """Add or remove grease pencil brush preset"""
667     bl_idname = "scene.gpencil_brush_preset_add"
668     bl_label = "Add Grease Pencil Brush Preset"
669     preset_menu = "VIEW3D_PT_gpencil_brush_presets"
670
671     preset_defines = [
672         "brush = bpy.context.active_gpencil_brush",
673         "settings = brush.gpencil_settings"
674     ]
675
676     preset_values = [
677         "settings.input_samples",
678         "settings.active_smooth_factor",
679         "settings.angle",
680         "settings.angle_factor",
681         "settings.use_settings_stabilizer",
682         "brush.smooth_stroke_radius",
683         "brush.smooth_stroke_factor",
684         "settings.pen_smooth_factor",
685         "settings.pen_smooth_steps",
686         "settings.pen_thick_smooth_factor",
687         "settings.pen_thick_smooth_steps",
688         "settings.pen_subdivision_steps",
689         "settings.random_subdiv",
690         "settings.use_settings_random",
691         "settings.random_pressure",
692         "settings.random_strength",
693         "settings.uv_random",
694         "settings.pen_jitter",
695         "settings.use_jitter_pressure",
696     ]
697
698     preset_subdir = "gpencil_brush"
699
700
701 class AddPresetGpencilMaterial(AddPresetBase, Operator):
702     """Add or remove grease pencil material preset"""
703     bl_idname = "scene.gpencil_material_preset_add"
704     bl_label = "Add Grease Pencil Material Preset"
705     preset_menu = "MATERIAL_PT_gpencil_material_presets"
706
707     preset_defines = [
708         "material = bpy.context.object.active_material",
709         "gpcolor = material.grease_pencil"
710     ]
711
712     preset_values = [
713         "gpcolor.mode",
714         "gpcolor.stroke_style",
715         "gpcolor.color",
716         "gpcolor.stroke_image",
717         "gpcolor.pixel_size",
718         "gpcolor.use_stroke_pattern",
719         "gpcolor.fill_style",
720         "gpcolor.fill_color",
721         "gpcolor.fill_image",
722         "gpcolor.gradient_type",
723         "gpcolor.mix_color",
724         "gpcolor.mix_factor",
725         "gpcolor.flip",
726         "gpcolor.pattern_shift",
727         "gpcolor.pattern_scale",
728         "gpcolor.pattern_radius",
729         "gpcolor.pattern_angle",
730         "gpcolor.pattern_gridsize",
731         "gpcolor.use_fill_pattern",
732         "gpcolor.texture_offset",
733         "gpcolor.texture_scale",
734         "gpcolor.texture_angle",
735         "gpcolor.texture_opacity",
736         "gpcolor.texture_clamp",
737         "gpcolor.texture_mix",
738         "gpcolor.mix_factor",
739         "gpcolor.show_stroke",
740         "gpcolor.show_fill",
741     ]
742
743     preset_subdir = "gpencil_material"
744
745
746 classes = (
747     AddPresetCamera,
748     AddPresetCloth,
749     AddPresetFluid,
750     AddPresetHairDynamics,
751     AddPresetInteraction,
752     AddPresetInterfaceTheme,
753     AddPresetKeyconfig,
754     AddPresetNodeColor,
755     AddPresetOperator,
756     AddPresetRender,
757     AddPresetSafeAreas,
758     AddPresetTrackingCamera,
759     AddPresetTrackingSettings,
760     AddPresetTrackingTrackColor,
761     AddPresetGpencilBrush,
762     AddPresetGpencilMaterial,
763     ExecutePreset,
764     WM_MT_operator_presets,
765 )