138 lines
4.5 KiB
Python
138 lines
4.5 KiB
Python
"""UI utilities and styling helpers for consistent Avatar Toolkit panel design"""
|
|
|
|
from typing import Callable, Optional
|
|
from bpy.types import UILayout, Context, Operator
|
|
|
|
|
|
class UIStyle:
|
|
"""Centralized UI styling constants for consistent appearance"""
|
|
|
|
SECTION_SEPARATOR_FACTOR: float = 0.5
|
|
SUBSECTION_SEPARATOR_FACTOR: float = 0.3
|
|
PRIMARY_BUTTON_SCALE: float = 1.5
|
|
STANDARD_BUTTON_SCALE: float = 1.0
|
|
COMPACT_BUTTON_SCALE: float = 0.9
|
|
DEFAULT_PADDING: float = 1.0
|
|
COMPACT_PADDING: float = 0.5
|
|
|
|
CATEGORY_ICONS = {
|
|
'optimization': 'MOD_SMOOTH',
|
|
'tools': 'TOOL_SETTINGS',
|
|
'custom': 'TOOL_OPTIONS',
|
|
'eye_tracking': 'OBJECT_CAMERA',
|
|
'settings': 'PREFERENCES',
|
|
'import_export': 'EXPORT',
|
|
'pose': 'POSE_HLT',
|
|
'materials': 'MATERIAL',
|
|
'mesh': 'MESH_DATA',
|
|
'bones': 'BONE_DATA',
|
|
'vfx': 'MOD_DISPLACE'
|
|
}
|
|
|
|
|
|
def draw_section_header(layout: UILayout, title: str, icon: str = 'NONE', separator: bool = True) -> UILayout:
|
|
"""Draw a consistent section header with optional icon and separator"""
|
|
header_box = layout.box()
|
|
col = header_box.column(align=True)
|
|
row = col.row()
|
|
row.scale_y = 1.2
|
|
row.label(text=title, icon=icon)
|
|
|
|
if separator:
|
|
col.separator(factor=UIStyle.SECTION_SEPARATOR_FACTOR)
|
|
|
|
return col
|
|
|
|
|
|
def draw_subsection(layout: UILayout, title: str, icon: str = 'NONE') -> UILayout:
|
|
"""Draw a subsection with reduced visual weight (no box)"""
|
|
col = layout.column(align=True)
|
|
row = col.row()
|
|
row.label(text=title, icon=icon)
|
|
col.separator(factor=UIStyle.SUBSECTION_SEPARATOR_FACTOR)
|
|
return col
|
|
|
|
|
|
def draw_info_text(layout: UILayout, text: str, icon: str = 'INFO') -> None:
|
|
"""Draw informational text that can wrap (replaces multiple labels)"""
|
|
col = layout.column()
|
|
col.alert = False
|
|
# Split long text for wrapping
|
|
row = col.row()
|
|
row.label(text=text, icon=icon)
|
|
|
|
|
|
def draw_warning_text(layout: UILayout, text: str) -> None:
|
|
"""Draw warning-styled text"""
|
|
col = layout.column()
|
|
col.alert = True
|
|
row = col.row()
|
|
row.label(text=text, icon='ERROR')
|
|
|
|
|
|
def draw_primary_button(layout: UILayout, operator_idname: str, text: str = "",
|
|
icon: str = 'NONE', **kwargs) -> None:
|
|
"""Draw a primary action button with standard scaling"""
|
|
row = layout.row(align=True)
|
|
row.scale_y = UIStyle.PRIMARY_BUTTON_SCALE
|
|
row.operator(operator_idname, text=text, icon=icon, **kwargs)
|
|
|
|
|
|
def draw_operator_row(layout: UILayout, operators: list[tuple[str, str, str]],
|
|
scale_y: float = 1.0, equal_width: bool = True) -> None:
|
|
"""Draw multiple operators in a single row with consistent sizing"""
|
|
if not operators:
|
|
return
|
|
|
|
row = layout.row(align=equal_width)
|
|
row.scale_y = scale_y
|
|
|
|
for op_id, text, icon in operators:
|
|
row.operator(op_id, text=text, icon=icon)
|
|
|
|
|
|
def draw_collapsible_section(layout: UILayout, title: str, icon: str,
|
|
draw_func: Callable[[UILayout], None],
|
|
context: Context, storage_attr: str) -> None:
|
|
"""Draw a collapsible section (using context scene properties for state)"""
|
|
col = layout.column(align=True)
|
|
row = col.row()
|
|
|
|
scene = context.scene
|
|
attr_name = f"_ui_expand_{storage_attr}"
|
|
is_expanded = getattr(scene, attr_name, False)
|
|
icon_name = 'DISCLOSURE_TRI_DOWN' if is_expanded else 'DISCLOSURE_TRI_RIGHT'
|
|
row.prop(scene, attr_name, text="", icon=icon_name, emboss=False)
|
|
row.label(text=title, icon=icon)
|
|
|
|
if is_expanded:
|
|
col.separator(factor=UIStyle.SUBSECTION_SEPARATOR_FACTOR)
|
|
draw_func(col)
|
|
|
|
|
|
def apply_operator_disable_feedback(operator: Operator, layout: UILayout,
|
|
is_disabled: bool, reason: str = "") -> UILayout:
|
|
"""Prepare layout for disabled operator with visual feedback"""
|
|
if is_disabled:
|
|
layout.enabled = False
|
|
return layout
|
|
|
|
|
|
def wrap_text_label(layout: UILayout, text: str, max_length: int = 50) -> None:
|
|
"""Draw a label that wraps long text across multiple lines"""
|
|
words = text.split()
|
|
current_line = ""
|
|
|
|
col = layout.column()
|
|
|
|
for word in words:
|
|
test_line = (current_line + " " + word).strip()
|
|
if len(test_line) > max_length and current_line:
|
|
col.label(text=current_line)
|
|
current_line = word
|
|
else:
|
|
current_line = test_line
|
|
|
|
if current_line:
|
|
col.label(text=current_line)
|