Context: remove active_gpencil_brush
[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 AddPresetTrackingCamera(AddPresetBase, Operator):
445     """Add or remove a Tracking Camera Intrinsics Preset"""
446     bl_idname = "clip.camera_preset_add"
447     bl_label = "Add Camera Preset"
448     preset_menu = "CLIP_PT_camera_presets"
449
450     preset_defines = [
451         "camera = bpy.context.edit_movieclip.tracking.camera"
452     ]
453
454     preset_subdir = "tracking_camera"
455
456     use_focal_length: BoolProperty(
457         name="Include Focal Length",
458         description="Include focal length into the preset",
459         options={'SKIP_SAVE'},
460         default=True
461     )
462
463     @property
464     def preset_values(self):
465         preset_values = [
466             "camera.sensor_width",
467             "camera.pixel_aspect",
468             "camera.k1",
469             "camera.k2",
470             "camera.k3"
471         ]
472         if self.use_focal_length:
473             preset_values.append("camera.units")
474             preset_values.append("camera.focal_length")
475         return preset_values
476
477
478 class AddPresetTrackingTrackColor(AddPresetBase, Operator):
479     """Add or remove a Clip Track Color Preset"""
480     bl_idname = "clip.track_color_preset_add"
481     bl_label = "Add Track Color Preset"
482     preset_menu = "CLIP_PT_track_color_presets"
483
484     preset_defines = [
485         "track = bpy.context.edit_movieclip.tracking.tracks.active"
486     ]
487
488     preset_values = [
489         "track.color",
490         "track.use_custom_color"
491     ]
492
493     preset_subdir = "tracking_track_color"
494
495
496 class AddPresetTrackingSettings(AddPresetBase, Operator):
497     """Add or remove a motion tracking settings preset"""
498     bl_idname = "clip.tracking_settings_preset_add"
499     bl_label = "Add Tracking Settings Preset"
500     preset_menu = "CLIP_PT_tracking_settings_presets"
501
502     preset_defines = [
503         "settings = bpy.context.edit_movieclip.tracking.settings"
504     ]
505
506     preset_values = [
507         "settings.default_correlation_min",
508         "settings.default_pattern_size",
509         "settings.default_search_size",
510         "settings.default_frames_limit",
511         "settings.default_pattern_match",
512         "settings.default_margin",
513         "settings.default_motion_model",
514         "settings.use_default_brute",
515         "settings.use_default_normalization",
516         "settings.use_default_mask",
517         "settings.use_default_red_channel",
518         "settings.use_default_green_channel",
519         "settings.use_default_blue_channel"
520         "settings.default_weight"
521     ]
522
523     preset_subdir = "tracking_settings"
524
525
526 class AddPresetNodeColor(AddPresetBase, Operator):
527     """Add or remove a Node Color Preset"""
528     bl_idname = "node.node_color_preset_add"
529     bl_label = "Add Node Color Preset"
530     preset_menu = "NODE_PT_node_color_presets"
531
532     preset_defines = [
533         "node = bpy.context.active_node"
534     ]
535
536     preset_values = [
537         "node.color",
538         "node.use_custom_color"
539     ]
540
541     preset_subdir = "node_color"
542
543
544 class AddPresetInterfaceTheme(AddPresetBase, Operator):
545     """Add or remove a theme preset"""
546     bl_idname = "wm.interface_theme_preset_add"
547     bl_label = "Add Theme Preset"
548     preset_menu = "USERPREF_MT_interface_theme_presets"
549     preset_subdir = "interface_theme"
550
551
552 class AddPresetKeyconfig(AddPresetBase, Operator):
553     """Add or remove a Key-config Preset"""
554     bl_idname = "wm.keyconfig_preset_add"
555     bl_label = "Add Keyconfig Preset"
556     preset_menu = "USERPREF_MT_keyconfigs"
557     preset_subdir = "keyconfig"
558
559     def add(self, context, filepath):
560         bpy.ops.wm.keyconfig_export(filepath=filepath)
561         bpy.utils.keyconfig_set(filepath)
562
563     def pre_cb(self, context):
564         keyconfigs = bpy.context.window_manager.keyconfigs
565         if self.remove_active:
566             preset_menu_class = getattr(bpy.types, self.preset_menu)
567             preset_menu_class.bl_label = keyconfigs.active.name
568
569     def post_cb(self, context):
570         keyconfigs = bpy.context.window_manager.keyconfigs
571         if self.remove_active:
572             keyconfigs.remove(keyconfigs.active)
573
574
575 class AddPresetOperator(AddPresetBase, Operator):
576     """Add or remove an Operator Preset"""
577     bl_idname = "wm.operator_preset_add"
578     bl_label = "Operator Preset"
579     preset_menu = "WM_MT_operator_presets"
580
581     operator: StringProperty(
582         name="Operator",
583         maxlen=64,
584         options={'HIDDEN', 'SKIP_SAVE'},
585     )
586
587     preset_defines = [
588         "op = bpy.context.active_operator",
589     ]
590
591     @property
592     def preset_subdir(self):
593         return AddPresetOperator.operator_path(self.operator)
594
595     @property
596     def preset_values(self):
597         properties_blacklist = Operator.bl_rna.properties.keys()
598
599         prefix, suffix = self.operator.split("_OT_", 1)
600         op = getattr(getattr(bpy.ops, prefix.lower()), suffix)
601         operator_rna = op.get_rna_type()
602         del op
603
604         ret = []
605         for prop_id, prop in operator_rna.properties.items():
606             if not (prop.is_hidden or prop.is_skip_save):
607                 if prop_id not in properties_blacklist:
608                     ret.append("op.%s" % prop_id)
609
610         return ret
611
612     @staticmethod
613     def operator_path(operator):
614         import os
615         prefix, suffix = operator.split("_OT_", 1)
616         return os.path.join("operator", "%s.%s" % (prefix.lower(), suffix))
617
618
619 class WM_MT_operator_presets(Menu):
620     bl_label = "Operator Presets"
621
622     def draw(self, context):
623         self.operator = context.active_operator.bl_idname
624
625         # dummy 'default' menu item
626         layout = self.layout
627         layout.operator("wm.operator_defaults")
628         layout.separator()
629
630         Menu.draw_preset(self, context)
631
632     @property
633     def preset_subdir(self):
634         return AddPresetOperator.operator_path(self.operator)
635
636     preset_operator = "script.execute_preset"
637
638
639 class AddPresetGpencilBrush(AddPresetBase, Operator):
640     """Add or remove grease pencil brush preset"""
641     bl_idname = "scene.gpencil_brush_preset_add"
642     bl_label = "Add Grease Pencil Brush Preset"
643     preset_menu = "VIEW3D_PT_gpencil_brush_presets"
644
645     preset_defines = [
646         "brush = bpy.context.tool_settings.gpencil_paint.brush",
647         "settings = brush.gpencil_settings"
648     ]
649
650     preset_values = [
651         "settings.input_samples",
652         "settings.active_smooth_factor",
653         "settings.angle",
654         "settings.angle_factor",
655         "settings.use_settings_stabilizer",
656         "brush.smooth_stroke_radius",
657         "brush.smooth_stroke_factor",
658         "settings.pen_smooth_factor",
659         "settings.pen_smooth_steps",
660         "settings.pen_thick_smooth_factor",
661         "settings.pen_thick_smooth_steps",
662         "settings.pen_subdivision_steps",
663         "settings.random_subdiv",
664         "settings.use_settings_random",
665         "settings.random_pressure",
666         "settings.random_strength",
667         "settings.uv_random",
668         "settings.pen_jitter",
669         "settings.use_jitter_pressure",
670     ]
671
672     preset_subdir = "gpencil_brush"
673
674
675 class AddPresetGpencilMaterial(AddPresetBase, Operator):
676     """Add or remove grease pencil material preset"""
677     bl_idname = "scene.gpencil_material_preset_add"
678     bl_label = "Add Grease Pencil Material Preset"
679     preset_menu = "MATERIAL_PT_gpencil_material_presets"
680
681     preset_defines = [
682         "material = bpy.context.object.active_material",
683         "gpcolor = material.grease_pencil"
684     ]
685
686     preset_values = [
687         "gpcolor.mode",
688         "gpcolor.stroke_style",
689         "gpcolor.color",
690         "gpcolor.stroke_image",
691         "gpcolor.pixel_size",
692         "gpcolor.use_stroke_pattern",
693         "gpcolor.fill_style",
694         "gpcolor.fill_color",
695         "gpcolor.fill_image",
696         "gpcolor.gradient_type",
697         "gpcolor.mix_color",
698         "gpcolor.mix_factor",
699         "gpcolor.flip",
700         "gpcolor.pattern_shift",
701         "gpcolor.pattern_scale",
702         "gpcolor.pattern_radius",
703         "gpcolor.pattern_angle",
704         "gpcolor.pattern_gridsize",
705         "gpcolor.use_fill_pattern",
706         "gpcolor.texture_offset",
707         "gpcolor.texture_scale",
708         "gpcolor.texture_angle",
709         "gpcolor.texture_opacity",
710         "gpcolor.texture_clamp",
711         "gpcolor.texture_mix",
712         "gpcolor.mix_factor",
713         "gpcolor.show_stroke",
714         "gpcolor.show_fill",
715     ]
716
717     preset_subdir = "gpencil_material"
718
719
720 classes = (
721     AddPresetCamera,
722     AddPresetCloth,
723     AddPresetFluid,
724     AddPresetHairDynamics,
725     AddPresetInterfaceTheme,
726     AddPresetKeyconfig,
727     AddPresetNodeColor,
728     AddPresetOperator,
729     AddPresetRender,
730     AddPresetSafeAreas,
731     AddPresetTrackingCamera,
732     AddPresetTrackingSettings,
733     AddPresetTrackingTrackColor,
734     AddPresetGpencilBrush,
735     AddPresetGpencilMaterial,
736     ExecutePreset,
737     WM_MT_operator_presets,
738 )