Fix add-ons with Python 3.12 by replacing "imp" with "importlib"
[blender-addons.git] / space_view3d_brush_menus / brush_menu.py
blobe7625b6eb19ab66071857fd481daf5f0d7dcde8e
1 # SPDX-FileCopyrightText: 2017-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 import bpy
6 from bpy.types import (
7 Operator,
8 Menu,
10 from bpy.props import BoolProperty
11 from . import utils_core
12 from . import brushes
13 from bl_ui.properties_paint_common import UnifiedPaintPanel
15 class BrushOptionsMenu(Menu):
16 bl_label = "Brush Options"
17 bl_idname = "VIEW3D_MT_sv3_brush_options"
19 @classmethod
20 def poll(self, context):
21 return utils_core.get_mode() in (
22 'SCULPT', 'VERTEX_PAINT',
23 'WEIGHT_PAINT', 'TEXTURE_PAINT',
24 'PARTICLE_EDIT'
27 def draw(self, context):
28 mode = utils_core.get_mode()
29 layout = self.layout
31 # add generic menu items
32 layout.operator_context = 'INVOKE_REGION_WIN'
33 layout.operator("wm.search_menu", text="Search", icon='VIEWZOOM')
34 layout.operator("wm.toolbar", text="Tools", icon='TOOL_SETTINGS')
35 layout.menu("SCREEN_MT_user_menu", text="Quick Favorites", icon='HEART')
36 layout.operator_menu_enum("object.mode_set", "mode",
37 text="Interactive Mode", icon='VIEW3D')
38 layout.separator()
41 # add mode specific menu items
42 if mode == 'SCULPT':
43 self.sculpt(mode, layout, context)
45 elif mode in ('VERTEX_PAINT', 'WEIGHT_PAINT'):
46 self.vw_paint(mode, layout, context)
48 elif mode == 'TEXTURE_PAINT':
49 self.texpaint(mode, layout, context)
51 else:
52 self.particle(layout, context)
54 def sculpt(self, mode, layout, context):
55 has_brush = utils_core.get_brush_link(context, types="brush")
56 icons = brushes.get_brush_icon(mode, has_brush.sculpt_tool) if \
57 has_brush else "BRUSH_DATA"
59 layout.row().menu("VIEW3D_MT_sv3_brushes_menu",
60 icon=icons)
62 layout.row().menu(BrushRadiusMenu.bl_idname)
64 if has_brush:
65 # if the active brush is unlinked these menus don't do anything
66 layout.row().menu(BrushStrengthMenu.bl_idname)
67 layout.row().menu(BrushAutosmoothMenu.bl_idname)
68 layout.row().menu(BrushModeMenu.bl_idname)
69 layout.row().menu("VIEW3D_MT_sv3_texture_menu")
70 layout.row().menu("VIEW3D_MT_sv3_stroke_options")
71 layout.row().menu("VIEW3D_MT_sv3_brush_curve_menu")
73 layout.row().menu("VIEW3D_MT_sv3_dyntopo")
74 layout.row().menu("VIEW3D_MT_sv3_master_symmetry_menu")
76 def vw_paint(self, mode, layout, context):
77 has_brush = utils_core.get_brush_link(context, types="brush")
78 icons = brushes.get_brush_icon(mode, has_brush.vertex_tool) if \
79 has_brush else "BRUSH_DATA"
81 if mode == 'VERTEX_PAINT':
82 layout.row().operator(ColorPickerPopup.bl_idname, icon="COLOR")
83 layout.row().separator()
85 layout.row().menu("VIEW3D_MT_sv3_brushes_menu",
86 icon=icons)
88 if mode == 'VERTEX_PAINT':
89 layout.row().menu(BrushRadiusMenu.bl_idname)
91 if has_brush:
92 # if the active brush is unlinked these menus don't do anything
93 layout.row().menu(BrushStrengthMenu.bl_idname)
94 layout.row().menu(BrushModeMenu.bl_idname)
95 layout.row().menu("VIEW3D_MT_sv3_texture_menu")
96 layout.row().menu("VIEW3D_MT_sv3_stroke_options")
97 layout.row().menu("VIEW3D_MT_sv3_brush_curve_menu")
99 if mode == 'WEIGHT_PAINT':
100 layout.row().menu(BrushWeightMenu.bl_idname)
101 layout.row().menu(BrushRadiusMenu.bl_idname)
103 if has_brush:
104 # if the active brush is unlinked these menus don't do anything
105 layout.row().menu(BrushStrengthMenu.bl_idname)
106 layout.row().menu(BrushModeMenu.bl_idname)
107 layout.row().menu("VIEW3D_MT_sv3_stroke_options")
108 layout.row().menu("VIEW3D_MT_sv3_brush_curve_menu")
110 layout.row().menu("VIEW3D_MT_sv3_master_symmetry_menu")
112 def texpaint(self, mode, layout, context):
113 toolsettings = context.tool_settings.image_paint
115 has_brush = utils_core.get_brush_link(context, types="brush")
116 icons = brushes.get_brush_icon(mode, has_brush.image_tool) if \
117 has_brush else "BRUSH_DATA"
119 if context.image_paint_object and not toolsettings.detect_data():
120 if toolsettings.missing_uvs:
121 layout.row().label(text="Missing UVs", icon='ERROR')
122 layout.row().operator("paint.add_simple_uvs")
124 return
126 elif toolsettings.missing_materials or toolsettings.missing_texture:
127 layout.row().label(text="Missing Data", icon='ERROR')
128 layout.row().operator_menu_enum("paint.add_texture_paint_slot", \
129 "type", \
130 icon='ADD', \
131 text="Add Texture Paint Slot")
133 return
135 elif toolsettings.missing_stencil:
136 layout.row().label(text="Missing Data", icon='ERROR')
137 layout.row().label(text="See Mask Properties", icon='FORWARD')
138 layout.row().separator()
139 layout.row().menu("VIEW3D_MT_sv3_brushes_menu",
140 icon=icons)
142 return
144 else:
145 layout.row().label(text="Missing Data", icon="INFO")
147 else:
148 if has_brush and has_brush.image_tool in {'DRAW', 'FILL'} and \
149 has_brush.blend not in {'ERASE_ALPHA', 'ADD_ALPHA'}:
150 layout.row().operator(ColorPickerPopup.bl_idname, icon="COLOR")
151 layout.row().separator()
153 layout.row().menu("VIEW3D_MT_sv3_brushes_menu",
154 icon=icons)
156 if has_brush:
157 # if the active brush is unlinked these menus don't do anything
158 if has_brush and has_brush.image_tool in {'MASK'}:
159 layout.row().menu(BrushWeightMenu.bl_idname, text="Mask Value")
161 if has_brush and has_brush.image_tool not in {'FILL'}:
162 layout.row().menu(BrushRadiusMenu.bl_idname)
164 layout.row().menu(BrushStrengthMenu.bl_idname)
166 if has_brush and has_brush.image_tool in {'DRAW'}:
167 layout.row().menu(BrushModeMenu.bl_idname)
169 layout.row().menu("VIEW3D_MT_sv3_texture_menu")
170 layout.row().menu("VIEW3D_MT_sv3_stroke_options")
171 layout.row().menu("VIEW3D_MT_sv3_brush_curve_menu")
173 layout.row().menu("VIEW3D_MT_sv3_master_symmetry_menu")
175 def particle(self, layout, context):
176 particle_edit = context.tool_settings.particle_edit
178 layout.row().menu("VIEW3D_MT_sv3_brushes_menu",
179 icon="BRUSH_DATA")
180 layout.row().menu(BrushRadiusMenu.bl_idname)
182 if particle_edit.tool != 'ADD':
183 layout.row().menu(BrushStrengthMenu.bl_idname)
184 else:
185 layout.row().menu(ParticleCountMenu.bl_idname)
186 layout.row().separator()
187 layout.row().prop(particle_edit, "use_default_interpolate", toggle=True)
189 layout.row().prop(particle_edit.brush, "steps", slider=True)
190 layout.row().prop(particle_edit, "default_key_count", slider=True)
192 if particle_edit.tool == 'LENGTH':
193 layout.row().separator()
194 layout.row().menu(ParticleLengthMenu.bl_idname)
196 if particle_edit.tool == 'PUFF':
197 layout.row().separator()
198 layout.row().menu(ParticlePuffMenu.bl_idname)
199 layout.row().prop(particle_edit.brush, "use_puff_volume", toggle=True)
201 layout.row().menu("VIEW3D_MT_sv3_master_symmetry_menu")
204 class BrushRadiusMenu(Menu):
205 bl_label = "Radius"
206 bl_idname = "VIEW3D_MT_sv3_brush_radius_menu"
207 bl_description = "Change the size of the brushes"
209 def init(self):
210 if utils_core.get_mode() == 'PARTICLE_EDIT':
211 settings = (("100", 100),
212 ("70", 70),
213 ("50", 50),
214 ("30", 30),
215 ("20", 20),
216 ("10", 10))
218 datapath = "tool_settings.particle_edit.brush.size"
219 proppath = bpy.context.tool_settings.particle_edit.brush
221 else:
222 settings = (("200", 200),
223 ("150", 150),
224 ("100", 100),
225 ("50", 50),
226 ("35", 35),
227 ("10", 10))
229 datapath = "tool_settings.unified_paint_settings.size"
230 proppath = bpy.context.tool_settings.unified_paint_settings
232 return settings, datapath, proppath
234 def draw(self, context):
235 settings, datapath, proppath = self.init()
236 layout = self.layout
238 # add the top slider
239 layout.row().prop(proppath, "size", slider=True)
240 layout.row().separator()
242 # add the rest of the menu items
243 for i in range(len(settings)):
244 utils_core.menuprop(
245 layout.row(), settings[i][0], settings[i][1],
246 datapath, icon='RADIOBUT_OFF', disable=True,
247 disable_icon='RADIOBUT_ON'
251 class BrushStrengthMenu(Menu):
252 bl_label = "Strength"
253 bl_idname = "VIEW3D_MT_sv3_brush_strength_menu"
255 def init(self):
256 mode = utils_core.get_mode()
257 settings = (("1.0", 1.0),
258 ("0.7", 0.7),
259 ("0.5", 0.5),
260 ("0.3", 0.3),
261 ("0.2", 0.2),
262 ("0.1", 0.1))
264 proppath = utils_core.get_brush_link(bpy.context, types="brush")
266 if mode == 'SCULPT':
267 datapath = "tool_settings.sculpt.brush.strength"
269 elif mode == 'VERTEX_PAINT':
270 datapath = "tool_settings.vertex_paint.brush.strength"
272 elif mode == 'WEIGHT_PAINT':
273 datapath = "tool_settings.weight_paint.brush.strength"
275 elif mode == 'TEXTURE_PAINT':
276 datapath = "tool_settings.image_paint.brush.strength"
278 else:
279 datapath = "tool_settings.particle_edit.brush.strength"
280 proppath = bpy.context.tool_settings.particle_edit.brush
282 return settings, datapath, proppath
284 def draw(self, context):
285 settings, datapath, proppath = self.init()
286 layout = self.layout
288 # add the top slider
289 if proppath:
290 layout.row().prop(proppath, "strength", slider=True)
291 layout.row().separator()
293 # add the rest of the menu items
294 for i in range(len(settings)):
295 utils_core.menuprop(
296 layout.row(), settings[i][0], settings[i][1],
297 datapath, icon='RADIOBUT_OFF', disable=True,
298 disable_icon='RADIOBUT_ON'
300 else:
301 layout.row().label(text="No brushes available", icon="INFO")
304 class BrushModeMenu(Menu):
305 bl_label = "Brush Mode"
306 bl_idname = "VIEW3D_MT_sv3_brush_mode_menu"
308 def init(self):
309 mode = utils_core.get_mode()
310 has_brush = utils_core.get_brush_link(bpy.context, types="brush")
312 if mode == 'SCULPT':
313 enum = has_brush.bl_rna.properties['sculpt_plane'].enum_items if \
314 has_brush else None
315 path = "tool_settings.sculpt.brush.sculpt_plane"
317 elif mode == 'VERTEX_PAINT':
318 enum = has_brush.bl_rna.properties['blend'].enum_items if \
319 has_brush else None
320 path = "tool_settings.vertex_paint.brush.blend"
322 elif mode == 'WEIGHT_PAINT':
323 enum = has_brush.bl_rna.properties['blend'].enum_items if \
324 has_brush else None
325 path = "tool_settings.weight_paint.brush.blend"
327 elif mode == 'TEXTURE_PAINT':
328 enum = has_brush.bl_rna.properties['blend'].enum_items if \
329 has_brush else None
330 path = "tool_settings.image_paint.brush.blend"
332 else:
333 enum = None
334 path = ""
336 return enum, path
338 def draw(self, context):
339 enum, path = self.init()
340 layout = self.layout
341 colum_n = utils_core.addon_settings()
343 layout.row().label(text="Brush Mode")
344 layout.row().separator()
346 if enum:
347 if utils_core.get_mode() != 'SCULPT':
348 column_flow = layout.column_flow(columns=colum_n)
350 # add all the brush modes to the menu
351 for brush in enum:
352 utils_core.menuprop(
353 column_flow.row(), brush.name,
354 brush.identifier, path, icon='RADIOBUT_OFF',
355 disable=True, disable_icon='RADIOBUT_ON'
357 else:
358 # add all the brush modes to the menu
359 for brush in enum:
360 utils_core.menuprop(
361 layout.row(), brush.name,
362 brush.identifier, path, icon='RADIOBUT_OFF',
363 disable=True, disable_icon='RADIOBUT_ON'
365 else:
366 layout.row().label(text="No brushes available", icon="INFO")
369 class BrushAutosmoothMenu(Menu):
370 bl_label = "Autosmooth"
371 bl_idname = "VIEW3D_MT_sv3_brush_autosmooth_menu"
373 def init(self):
374 settings = (("1.0", 1.0),
375 ("0.7", 0.7),
376 ("0.5", 0.5),
377 ("0.3", 0.3),
378 ("0.2", 0.2),
379 ("0.1", 0.1))
381 return settings
383 def draw(self, context):
384 settings = self.init()
385 layout = self.layout
386 has_brush = utils_core.get_brush_link(context, types="brush")
388 if has_brush:
389 # add the top slider
390 layout.row().prop(has_brush, "auto_smooth_factor", slider=True)
391 layout.row().separator()
393 # add the rest of the menu items
394 for i in range(len(settings)):
395 utils_core.menuprop(
396 layout.row(), settings[i][0], settings[i][1],
397 "tool_settings.sculpt.brush.auto_smooth_factor",
398 icon='RADIOBUT_OFF', disable=True,
399 disable_icon='RADIOBUT_ON'
401 else:
402 layout.row().label(text="No Smooth options available", icon="INFO")
405 class BrushWeightMenu(Menu):
406 bl_label = "Weight"
407 bl_idname = "VIEW3D_MT_sv3_brush_weight_menu"
409 def init(self):
410 settings = (("1.0", 1.0),
411 ("0.7", 0.7),
412 ("0.5", 0.5),
413 ("0.3", 0.3),
414 ("0.2", 0.2),
415 ("0.1", 0.1))
417 if utils_core.get_mode() == 'WEIGHT_PAINT':
418 brush = bpy.context.tool_settings.unified_paint_settings
419 brushstr = "tool_settings.unified_paint_settings.weight"
420 name = "Weight"
422 else:
423 brush = bpy.context.tool_settings.image_paint.brush
424 brushstr = "tool_settings.image_paint.brush.weight"
425 name = "Mask Value"
427 return settings, brush, brushstr, name
429 def draw(self, context):
430 settings, brush, brushstr, name = self.init()
431 layout = self.layout
433 if brush:
434 # add the top slider
435 layout.row().prop(brush, "weight", text=name, slider=True)
436 layout.row().separator()
438 # add the rest of the menu items
439 for i in range(len(settings)):
440 utils_core.menuprop(
441 layout.row(), settings[i][0], settings[i][1],
442 brushstr,
443 icon='RADIOBUT_OFF', disable=True,
444 disable_icon='RADIOBUT_ON'
446 else:
447 layout.row().label(text="No brush available", icon="INFO")
450 class ParticleCountMenu(Menu):
451 bl_label = "Count"
452 bl_idname = "VIEW3D_MT_sv3_particle_count_menu"
454 def init(self):
455 settings = (("50", 50),
456 ("25", 25),
457 ("10", 10),
458 ("5", 5),
459 ("3", 3),
460 ("1", 1))
462 return settings
464 def draw(self, context):
465 settings = self.init()
466 layout = self.layout
468 # add the top slider
469 layout.row().prop(context.tool_settings.particle_edit.brush,
470 "count", slider=True)
471 layout.row().separator()
473 # add the rest of the menu items
474 for i in range(len(settings)):
475 utils_core.menuprop(
476 layout.row(), settings[i][0], settings[i][1],
477 "tool_settings.particle_edit.brush.count",
478 icon='RADIOBUT_OFF', disable=True,
479 disable_icon='RADIOBUT_ON'
483 class ParticleLengthMenu(Menu):
484 bl_label = "Length Mode"
485 bl_idname = "VIEW3D_MT_sv3_particle_length_menu"
487 def draw(self, context):
488 layout = self.layout
489 path = "tool_settings.particle_edit.brush.length_mode"
491 # add the menu items
492 for item in context.tool_settings.particle_edit.brush. \
493 bl_rna.properties['length_mode'].enum_items:
494 utils_core.menuprop(
495 layout.row(), item.name, item.identifier, path,
496 icon='RADIOBUT_OFF',
497 disable=True,
498 disable_icon='RADIOBUT_ON'
502 class ParticlePuffMenu(Menu):
503 bl_label = "Puff Mode"
504 bl_idname = "VIEW3D_MT_sv3_particle_puff_menu"
506 def draw(self, context):
507 layout = self.layout
508 path = "tool_settings.particle_edit.brush.puff_mode"
510 # add the menu items
511 for item in context.tool_settings.particle_edit.brush. \
512 bl_rna.properties['puff_mode'].enum_items:
513 utils_core.menuprop(
514 layout.row(), item.name, item.identifier, path,
515 icon='RADIOBUT_OFF',
516 disable=True,
517 disable_icon='RADIOBUT_ON'
521 class FlipColorsAll(Operator):
522 """Switch between Foreground and Background colors"""
523 bl_label = "Flip Colors"
524 bl_idname = "view3d.sv3_flip_colors_all"
525 bl_description = "Switch between Foreground and Background colors"
527 is_tex: BoolProperty(
528 default=False,
529 options={'HIDDEN'}
532 def execute(self, context):
533 try:
534 if self.is_tex is False:
535 color = context.tool_settings.vertex_paint.brush.color
536 secondary_color = context.tool_settings.vertex_paint.brush.secondary_color
538 orig_prim = color.hsv
539 orig_sec = secondary_color.hsv
541 color.hsv = orig_sec
542 secondary_color.hsv = orig_prim
543 else:
544 color = context.tool_settings.image_paint.brush.color
545 secondary_color = context.tool_settings.image_paint.brush.secondary_color
547 orig_prim = color.hsv
548 orig_sec = secondary_color.hsv
550 color.hsv = orig_sec
551 secondary_color.hsv = orig_prim
553 return {'FINISHED'}
555 except Exception as e:
556 utils_core.error_handlers(self, "view3d.sv3_flip_colors_all", e,
557 "Flip Colors could not be completed")
559 return {'CANCELLED'}
562 class ColorPickerPopup(Operator):
563 """Open Color Picker"""
564 bl_label = "Color"
565 bl_idname = "view3d.sv3_color_picker_popup"
566 bl_description = "Open Color Picker"
567 bl_options = {'REGISTER'}
569 @classmethod
570 def poll(self, context):
571 return utils_core.get_mode() in (
572 'VERTEX_PAINT',
573 'TEXTURE_PAINT'
576 def check(self, context):
577 return True
579 def init(self):
580 if utils_core.get_mode() == 'TEXTURE_PAINT':
581 settings = bpy.context.tool_settings.image_paint
582 brush = getattr(settings, "brush", None)
583 else:
584 settings = bpy.context.tool_settings.vertex_paint
585 brush = settings.brush
586 brush = getattr(settings, "brush", None)
588 return settings, brush
590 def draw(self, context):
591 layout = self.layout
592 settings, brush = self.init()
595 if brush:
596 layout.row().template_color_picker(brush, "color", value_slider=True)
597 prim_sec_row = layout.row(align=True)
598 prim_sec_row.prop(brush, "color", text="")
599 prim_sec_row.prop(brush, "secondary_color", text="")
601 if utils_core.get_mode() == 'VERTEX_PAINT':
602 prim_sec_row.operator(
603 FlipColorsAll.bl_idname,
604 icon='FILE_REFRESH', text=""
605 ).is_tex = False
606 else:
607 prim_sec_row.operator(
608 FlipColorsAll.bl_idname,
609 icon='FILE_REFRESH', text=""
610 ).is_tex = True
612 if settings.palette:
613 layout.column().template_palette(settings, "palette", color=True)
615 layout.row().template_ID(settings, "palette", new="palette.new")
616 else:
617 layout.row().label(text="No brushes currently available", icon="INFO")
619 return
621 def execute(self, context):
622 return context.window_manager.invoke_popup(self, width=180)
625 classes = (
626 BrushOptionsMenu,
627 BrushRadiusMenu,
628 BrushStrengthMenu,
629 BrushModeMenu,
630 BrushAutosmoothMenu,
631 BrushWeightMenu,
632 ParticleCountMenu,
633 ParticleLengthMenu,
634 ParticlePuffMenu,
635 FlipColorsAll,
636 ColorPickerPopup
639 def register():
640 for cls in classes:
641 bpy.utils.register_class(cls)
643 def unregister():
644 for cls in classes:
645 bpy.utils.unregister_class(cls)