system_demo_mode: minor usability improvements
[blender-addons.git] / space_view3d_brush_menus / brush_menu.py
blob7f01c65d1f87859ec8fed643b22782ed47caf7a4
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 import bpy
4 from bpy.types import (
5 Operator,
6 Menu,
8 from bpy.props import BoolProperty
9 from . import utils_core
10 from . import brushes
11 from bl_ui.properties_paint_common import UnifiedPaintPanel
13 class BrushOptionsMenu(Menu):
14 bl_label = "Brush Options"
15 bl_idname = "VIEW3D_MT_sv3_brush_options"
17 @classmethod
18 def poll(self, context):
19 return utils_core.get_mode() in (
20 'SCULPT', 'VERTEX_PAINT',
21 'WEIGHT_PAINT', 'TEXTURE_PAINT',
22 'PARTICLE_EDIT'
25 def draw(self, context):
26 mode = utils_core.get_mode()
27 layout = self.layout
29 # add generic menu items
30 layout.operator_context = 'INVOKE_REGION_WIN'
31 layout.operator("wm.search_menu", text="Search", icon='VIEWZOOM')
32 layout.operator("wm.toolbar", text="Tools", icon='TOOL_SETTINGS')
33 layout.menu("SCREEN_MT_user_menu", text="Quick Favorites", icon='HEART')
34 layout.operator_menu_enum("object.mode_set", "mode",
35 text="Interactive Mode", icon='VIEW3D')
36 layout.separator()
39 # add mode specific menu items
40 if mode == 'SCULPT':
41 self.sculpt(mode, layout, context)
43 elif mode in ('VERTEX_PAINT', 'WEIGHT_PAINT'):
44 self.vw_paint(mode, layout, context)
46 elif mode == 'TEXTURE_PAINT':
47 self.texpaint(mode, layout, context)
49 else:
50 self.particle(layout, context)
52 def sculpt(self, mode, layout, context):
53 has_brush = utils_core.get_brush_link(context, types="brush")
54 icons = brushes.get_brush_icon(mode, has_brush.sculpt_tool) if \
55 has_brush else "BRUSH_DATA"
57 layout.row().menu("VIEW3D_MT_sv3_brushes_menu",
58 icon=icons)
60 layout.row().menu(BrushRadiusMenu.bl_idname)
62 if has_brush:
63 # if the active brush is unlinked these menus don't do anything
64 layout.row().menu(BrushStrengthMenu.bl_idname)
65 layout.row().menu(BrushAutosmoothMenu.bl_idname)
66 layout.row().menu(BrushModeMenu.bl_idname)
67 layout.row().menu("VIEW3D_MT_sv3_texture_menu")
68 layout.row().menu("VIEW3D_MT_sv3_stroke_options")
69 layout.row().menu("VIEW3D_MT_sv3_brush_curve_menu")
71 layout.row().menu("VIEW3D_MT_sv3_dyntopo")
72 layout.row().menu("VIEW3D_MT_sv3_master_symmetry_menu")
74 def vw_paint(self, mode, layout, context):
75 has_brush = utils_core.get_brush_link(context, types="brush")
76 icons = brushes.get_brush_icon(mode, has_brush.vertex_tool) if \
77 has_brush else "BRUSH_DATA"
79 if mode == 'VERTEX_PAINT':
80 layout.row().operator(ColorPickerPopup.bl_idname, icon="COLOR")
81 layout.row().separator()
83 layout.row().menu("VIEW3D_MT_sv3_brushes_menu",
84 icon=icons)
86 if mode == 'VERTEX_PAINT':
87 layout.row().menu(BrushRadiusMenu.bl_idname)
89 if has_brush:
90 # if the active brush is unlinked these menus don't do anything
91 layout.row().menu(BrushStrengthMenu.bl_idname)
92 layout.row().menu(BrushModeMenu.bl_idname)
93 layout.row().menu("VIEW3D_MT_sv3_texture_menu")
94 layout.row().menu("VIEW3D_MT_sv3_stroke_options")
95 layout.row().menu("VIEW3D_MT_sv3_brush_curve_menu")
97 if mode == 'WEIGHT_PAINT':
98 layout.row().menu(BrushWeightMenu.bl_idname)
99 layout.row().menu(BrushRadiusMenu.bl_idname)
101 if has_brush:
102 # if the active brush is unlinked these menus don't do anything
103 layout.row().menu(BrushStrengthMenu.bl_idname)
104 layout.row().menu(BrushModeMenu.bl_idname)
105 layout.row().menu("VIEW3D_MT_sv3_stroke_options")
106 layout.row().menu("VIEW3D_MT_sv3_brush_curve_menu")
108 layout.row().menu("VIEW3D_MT_sv3_master_symmetry_menu")
110 def texpaint(self, mode, layout, context):
111 toolsettings = context.tool_settings.image_paint
113 has_brush = utils_core.get_brush_link(context, types="brush")
114 icons = brushes.get_brush_icon(mode, has_brush.image_tool) if \
115 has_brush else "BRUSH_DATA"
117 if context.image_paint_object and not toolsettings.detect_data():
118 if toolsettings.missing_uvs:
119 layout.row().label(text="Missing UVs", icon='ERROR')
120 layout.row().operator("paint.add_simple_uvs")
122 return
124 elif toolsettings.missing_materials or toolsettings.missing_texture:
125 layout.row().label(text="Missing Data", icon='ERROR')
126 layout.row().operator_menu_enum("paint.add_texture_paint_slot", \
127 "type", \
128 icon='ADD', \
129 text="Add Texture Paint Slot")
131 return
133 elif toolsettings.missing_stencil:
134 layout.row().label(text="Missing Data", icon='ERROR')
135 layout.row().label(text="See Mask Properties", icon='FORWARD')
136 layout.row().separator()
137 layout.row().menu("VIEW3D_MT_sv3_brushes_menu",
138 icon=icons)
140 return
142 else:
143 layout.row().label(text="Missing Data", icon="INFO")
145 else:
146 if has_brush and has_brush.image_tool in {'DRAW', 'FILL'} and \
147 has_brush.blend not in {'ERASE_ALPHA', 'ADD_ALPHA'}:
148 layout.row().operator(ColorPickerPopup.bl_idname, icon="COLOR")
149 layout.row().separator()
151 layout.row().menu("VIEW3D_MT_sv3_brushes_menu",
152 icon=icons)
154 if has_brush:
155 # if the active brush is unlinked these menus don't do anything
156 if has_brush and has_brush.image_tool in {'MASK'}:
157 layout.row().menu(BrushWeightMenu.bl_idname, text="Mask Value")
159 if has_brush and has_brush.image_tool not in {'FILL'}:
160 layout.row().menu(BrushRadiusMenu.bl_idname)
162 layout.row().menu(BrushStrengthMenu.bl_idname)
164 if has_brush and has_brush.image_tool in {'DRAW'}:
165 layout.row().menu(BrushModeMenu.bl_idname)
167 layout.row().menu("VIEW3D_MT_sv3_texture_menu")
168 layout.row().menu("VIEW3D_MT_sv3_stroke_options")
169 layout.row().menu("VIEW3D_MT_sv3_brush_curve_menu")
171 layout.row().menu("VIEW3D_MT_sv3_master_symmetry_menu")
173 def particle(self, layout, context):
174 particle_edit = context.tool_settings.particle_edit
176 layout.row().menu("VIEW3D_MT_sv3_brushes_menu",
177 icon="BRUSH_DATA")
178 layout.row().menu(BrushRadiusMenu.bl_idname)
180 if particle_edit.tool != 'ADD':
181 layout.row().menu(BrushStrengthMenu.bl_idname)
182 else:
183 layout.row().menu(ParticleCountMenu.bl_idname)
184 layout.row().separator()
185 layout.row().prop(particle_edit, "use_default_interpolate", toggle=True)
187 layout.row().prop(particle_edit.brush, "steps", slider=True)
188 layout.row().prop(particle_edit, "default_key_count", slider=True)
190 if particle_edit.tool == 'LENGTH':
191 layout.row().separator()
192 layout.row().menu(ParticleLengthMenu.bl_idname)
194 if particle_edit.tool == 'PUFF':
195 layout.row().separator()
196 layout.row().menu(ParticlePuffMenu.bl_idname)
197 layout.row().prop(particle_edit.brush, "use_puff_volume", toggle=True)
199 layout.row().menu("VIEW3D_MT_sv3_master_symmetry_menu")
202 class BrushRadiusMenu(Menu):
203 bl_label = "Radius"
204 bl_idname = "VIEW3D_MT_sv3_brush_radius_menu"
205 bl_description = "Change the size of the brushes"
207 def init(self):
208 if utils_core.get_mode() == 'PARTICLE_EDIT':
209 settings = (("100", 100),
210 ("70", 70),
211 ("50", 50),
212 ("30", 30),
213 ("20", 20),
214 ("10", 10))
216 datapath = "tool_settings.particle_edit.brush.size"
217 proppath = bpy.context.tool_settings.particle_edit.brush
219 else:
220 settings = (("200", 200),
221 ("150", 150),
222 ("100", 100),
223 ("50", 50),
224 ("35", 35),
225 ("10", 10))
227 datapath = "tool_settings.unified_paint_settings.size"
228 proppath = bpy.context.tool_settings.unified_paint_settings
230 return settings, datapath, proppath
232 def draw(self, context):
233 settings, datapath, proppath = self.init()
234 layout = self.layout
236 # add the top slider
237 layout.row().prop(proppath, "size", slider=True)
238 layout.row().separator()
240 # add the rest of the menu items
241 for i in range(len(settings)):
242 utils_core.menuprop(
243 layout.row(), settings[i][0], settings[i][1],
244 datapath, icon='RADIOBUT_OFF', disable=True,
245 disable_icon='RADIOBUT_ON'
249 class BrushStrengthMenu(Menu):
250 bl_label = "Strength"
251 bl_idname = "VIEW3D_MT_sv3_brush_strength_menu"
253 def init(self):
254 mode = utils_core.get_mode()
255 settings = (("1.0", 1.0),
256 ("0.7", 0.7),
257 ("0.5", 0.5),
258 ("0.3", 0.3),
259 ("0.2", 0.2),
260 ("0.1", 0.1))
262 proppath = utils_core.get_brush_link(bpy.context, types="brush")
264 if mode == 'SCULPT':
265 datapath = "tool_settings.sculpt.brush.strength"
267 elif mode == 'VERTEX_PAINT':
268 datapath = "tool_settings.vertex_paint.brush.strength"
270 elif mode == 'WEIGHT_PAINT':
271 datapath = "tool_settings.weight_paint.brush.strength"
273 elif mode == 'TEXTURE_PAINT':
274 datapath = "tool_settings.image_paint.brush.strength"
276 else:
277 datapath = "tool_settings.particle_edit.brush.strength"
278 proppath = bpy.context.tool_settings.particle_edit.brush
280 return settings, datapath, proppath
282 def draw(self, context):
283 settings, datapath, proppath = self.init()
284 layout = self.layout
286 # add the top slider
287 if proppath:
288 layout.row().prop(proppath, "strength", slider=True)
289 layout.row().separator()
291 # add the rest of the menu items
292 for i in range(len(settings)):
293 utils_core.menuprop(
294 layout.row(), settings[i][0], settings[i][1],
295 datapath, icon='RADIOBUT_OFF', disable=True,
296 disable_icon='RADIOBUT_ON'
298 else:
299 layout.row().label(text="No brushes available", icon="INFO")
302 class BrushModeMenu(Menu):
303 bl_label = "Brush Mode"
304 bl_idname = "VIEW3D_MT_sv3_brush_mode_menu"
306 def init(self):
307 mode = utils_core.get_mode()
308 has_brush = utils_core.get_brush_link(bpy.context, types="brush")
310 if mode == 'SCULPT':
311 enum = has_brush.bl_rna.properties['sculpt_plane'].enum_items if \
312 has_brush else None
313 path = "tool_settings.sculpt.brush.sculpt_plane"
315 elif mode == 'VERTEX_PAINT':
316 enum = has_brush.bl_rna.properties['blend'].enum_items if \
317 has_brush else None
318 path = "tool_settings.vertex_paint.brush.blend"
320 elif mode == 'WEIGHT_PAINT':
321 enum = has_brush.bl_rna.properties['blend'].enum_items if \
322 has_brush else None
323 path = "tool_settings.weight_paint.brush.blend"
325 elif mode == 'TEXTURE_PAINT':
326 enum = has_brush.bl_rna.properties['blend'].enum_items if \
327 has_brush else None
328 path = "tool_settings.image_paint.brush.blend"
330 else:
331 enum = None
332 path = ""
334 return enum, path
336 def draw(self, context):
337 enum, path = self.init()
338 layout = self.layout
339 colum_n = utils_core.addon_settings()
341 layout.row().label(text="Brush Mode")
342 layout.row().separator()
344 if enum:
345 if utils_core.get_mode() != 'SCULPT':
346 column_flow = layout.column_flow(columns=colum_n)
348 # add all the brush modes to the menu
349 for brush in enum:
350 utils_core.menuprop(
351 column_flow.row(), brush.name,
352 brush.identifier, path, icon='RADIOBUT_OFF',
353 disable=True, disable_icon='RADIOBUT_ON'
355 else:
356 # add all the brush modes to the menu
357 for brush in enum:
358 utils_core.menuprop(
359 layout.row(), brush.name,
360 brush.identifier, path, icon='RADIOBUT_OFF',
361 disable=True, disable_icon='RADIOBUT_ON'
363 else:
364 layout.row().label(text="No brushes available", icon="INFO")
367 class BrushAutosmoothMenu(Menu):
368 bl_label = "Autosmooth"
369 bl_idname = "VIEW3D_MT_sv3_brush_autosmooth_menu"
371 def init(self):
372 settings = (("1.0", 1.0),
373 ("0.7", 0.7),
374 ("0.5", 0.5),
375 ("0.3", 0.3),
376 ("0.2", 0.2),
377 ("0.1", 0.1))
379 return settings
381 def draw(self, context):
382 settings = self.init()
383 layout = self.layout
384 has_brush = utils_core.get_brush_link(context, types="brush")
386 if has_brush:
387 # add the top slider
388 layout.row().prop(has_brush, "auto_smooth_factor", slider=True)
389 layout.row().separator()
391 # add the rest of the menu items
392 for i in range(len(settings)):
393 utils_core.menuprop(
394 layout.row(), settings[i][0], settings[i][1],
395 "tool_settings.sculpt.brush.auto_smooth_factor",
396 icon='RADIOBUT_OFF', disable=True,
397 disable_icon='RADIOBUT_ON'
399 else:
400 layout.row().label(text="No Smooth options available", icon="INFO")
403 class BrushWeightMenu(Menu):
404 bl_label = "Weight"
405 bl_idname = "VIEW3D_MT_sv3_brush_weight_menu"
407 def init(self):
408 settings = (("1.0", 1.0),
409 ("0.7", 0.7),
410 ("0.5", 0.5),
411 ("0.3", 0.3),
412 ("0.2", 0.2),
413 ("0.1", 0.1))
415 if utils_core.get_mode() == 'WEIGHT_PAINT':
416 brush = bpy.context.tool_settings.unified_paint_settings
417 brushstr = "tool_settings.unified_paint_settings.weight"
418 name = "Weight"
420 else:
421 brush = bpy.context.tool_settings.image_paint.brush
422 brushstr = "tool_settings.image_paint.brush.weight"
423 name = "Mask Value"
425 return settings, brush, brushstr, name
427 def draw(self, context):
428 settings, brush, brushstr, name = self.init()
429 layout = self.layout
431 if brush:
432 # add the top slider
433 layout.row().prop(brush, "weight", text=name, slider=True)
434 layout.row().separator()
436 # add the rest of the menu items
437 for i in range(len(settings)):
438 utils_core.menuprop(
439 layout.row(), settings[i][0], settings[i][1],
440 brushstr,
441 icon='RADIOBUT_OFF', disable=True,
442 disable_icon='RADIOBUT_ON'
444 else:
445 layout.row().label(text="No brush available", icon="INFO")
448 class ParticleCountMenu(Menu):
449 bl_label = "Count"
450 bl_idname = "VIEW3D_MT_sv3_particle_count_menu"
452 def init(self):
453 settings = (("50", 50),
454 ("25", 25),
455 ("10", 10),
456 ("5", 5),
457 ("3", 3),
458 ("1", 1))
460 return settings
462 def draw(self, context):
463 settings = self.init()
464 layout = self.layout
466 # add the top slider
467 layout.row().prop(context.tool_settings.particle_edit.brush,
468 "count", slider=True)
469 layout.row().separator()
471 # add the rest of the menu items
472 for i in range(len(settings)):
473 utils_core.menuprop(
474 layout.row(), settings[i][0], settings[i][1],
475 "tool_settings.particle_edit.brush.count",
476 icon='RADIOBUT_OFF', disable=True,
477 disable_icon='RADIOBUT_ON'
481 class ParticleLengthMenu(Menu):
482 bl_label = "Length Mode"
483 bl_idname = "VIEW3D_MT_sv3_particle_length_menu"
485 def draw(self, context):
486 layout = self.layout
487 path = "tool_settings.particle_edit.brush.length_mode"
489 # add the menu items
490 for item in context.tool_settings.particle_edit.brush. \
491 bl_rna.properties['length_mode'].enum_items:
492 utils_core.menuprop(
493 layout.row(), item.name, item.identifier, path,
494 icon='RADIOBUT_OFF',
495 disable=True,
496 disable_icon='RADIOBUT_ON'
500 class ParticlePuffMenu(Menu):
501 bl_label = "Puff Mode"
502 bl_idname = "VIEW3D_MT_sv3_particle_puff_menu"
504 def draw(self, context):
505 layout = self.layout
506 path = "tool_settings.particle_edit.brush.puff_mode"
508 # add the menu items
509 for item in context.tool_settings.particle_edit.brush. \
510 bl_rna.properties['puff_mode'].enum_items:
511 utils_core.menuprop(
512 layout.row(), item.name, item.identifier, path,
513 icon='RADIOBUT_OFF',
514 disable=True,
515 disable_icon='RADIOBUT_ON'
519 class FlipColorsAll(Operator):
520 """Switch between Foreground and Background colors"""
521 bl_label = "Flip Colors"
522 bl_idname = "view3d.sv3_flip_colors_all"
523 bl_description = "Switch between Foreground and Background colors"
525 is_tex: BoolProperty(
526 default=False,
527 options={'HIDDEN'}
530 def execute(self, context):
531 try:
532 if self.is_tex is False:
533 color = context.tool_settings.vertex_paint.brush.color
534 secondary_color = context.tool_settings.vertex_paint.brush.secondary_color
536 orig_prim = color.hsv
537 orig_sec = secondary_color.hsv
539 color.hsv = orig_sec
540 secondary_color.hsv = orig_prim
541 else:
542 color = context.tool_settings.image_paint.brush.color
543 secondary_color = context.tool_settings.image_paint.brush.secondary_color
545 orig_prim = color.hsv
546 orig_sec = secondary_color.hsv
548 color.hsv = orig_sec
549 secondary_color.hsv = orig_prim
551 return {'FINISHED'}
553 except Exception as e:
554 utils_core.error_handlers(self, "view3d.sv3_flip_colors_all", e,
555 "Flip Colors could not be completed")
557 return {'CANCELLED'}
560 class ColorPickerPopup(Operator):
561 """Open Color Picker"""
562 bl_label = "Color"
563 bl_idname = "view3d.sv3_color_picker_popup"
564 bl_description = "Open Color Picker"
565 bl_options = {'REGISTER'}
567 @classmethod
568 def poll(self, context):
569 return utils_core.get_mode() in (
570 'VERTEX_PAINT',
571 'TEXTURE_PAINT'
574 def check(self, context):
575 return True
577 def init(self):
578 if utils_core.get_mode() == 'TEXTURE_PAINT':
579 settings = bpy.context.tool_settings.image_paint
580 brush = getattr(settings, "brush", None)
581 else:
582 settings = bpy.context.tool_settings.vertex_paint
583 brush = settings.brush
584 brush = getattr(settings, "brush", None)
586 return settings, brush
588 def draw(self, context):
589 layout = self.layout
590 settings, brush = self.init()
593 if brush:
594 layout.row().template_color_picker(brush, "color", value_slider=True)
595 prim_sec_row = layout.row(align=True)
596 prim_sec_row.prop(brush, "color", text="")
597 prim_sec_row.prop(brush, "secondary_color", text="")
599 if utils_core.get_mode() == 'VERTEX_PAINT':
600 prim_sec_row.operator(
601 FlipColorsAll.bl_idname,
602 icon='FILE_REFRESH', text=""
603 ).is_tex = False
604 else:
605 prim_sec_row.operator(
606 FlipColorsAll.bl_idname,
607 icon='FILE_REFRESH', text=""
608 ).is_tex = True
610 if settings.palette:
611 layout.column().template_palette(settings, "palette", color=True)
613 layout.row().template_ID(settings, "palette", new="palette.new")
614 else:
615 layout.row().label(text="No brushes currently available", icon="INFO")
617 return
619 def execute(self, context):
620 return context.window_manager.invoke_popup(self, width=180)
623 classes = (
624 BrushOptionsMenu,
625 BrushRadiusMenu,
626 BrushStrengthMenu,
627 BrushModeMenu,
628 BrushAutosmoothMenu,
629 BrushWeightMenu,
630 ParticleCountMenu,
631 ParticleLengthMenu,
632 ParticlePuffMenu,
633 FlipColorsAll,
634 ColorPickerPopup
637 def register():
638 for cls in classes:
639 bpy.utils.register_class(cls)
641 def unregister():
642 for cls in classes:
643 bpy.utils.unregister_class(cls)