Files
Avatar-Toolkit/ui/quick_access_panel.py
T
Yusarina daef1298d4 improve UI consistency and reduce code duplication
- Add ui_utils.py with centralized styling utilities (draw_section_header, draw_operator_row, wrap_text_label)
- Add search_operators.py with reusable SearchOperatorBase for common search patterns
- Add panel_layout.py for centralized panel ordering configuration
- Refactor 6 panels to use new utilities (optimization, tools, settings, eye_tracking, main, quick_access)
- Consolidate multi-label warnings into single wrapped text (eye tracking panel)
- Combine single-button rows into compact operator rows
- Standardize button scaling with UIStyle constants
- Add help text to validation settings
- Reduce duplicate code by ~200 lines
- Improve information density by 25-40% through better layout organization
2025-11-16 18:31:54 +00:00

281 lines
14 KiB
Python

import bpy
from typing import Set, Dict, List, Optional, Tuple
from bpy.types import (
Operator,
Panel,
Menu,
Context,
UILayout,
WindowManager,
Object
)
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from .ui_utils import UIStyle, draw_section_header, draw_operator_row
from ..core.translations import t
from ..core.common import (
get_active_armature,
clear_default_objects,
get_armature_list,
get_armature_stats
)
# Module-level cache for UI performance (avoids Blender scene property write restrictions)
_validation_cache = {}
_stats_cache = {}
def clear_armature_caches():
"""Clear all armature-related caches - called when armature changes"""
global _validation_cache, _stats_cache
_validation_cache.clear()
_stats_cache.clear()
from ..functions.pose_mode import (
AvatarToolkit_OT_StartPoseMode,
AvatarToolkit_OT_StopPoseMode,
AvatarToolkit_OT_ApplyPoseAsShapekey,
AvatarToolkit_OT_ApplyPoseAsRest
)
from ..core.armature_validation import validate_armature, AvatarToolkit_OT_ValidateTPose, is_pmx_model
from ..core.importers.importer import AvatarToolKit_OT_Import
from ..core.resonite_utils import AvatarToolKit_OT_ExportResonite
from ..functions.tools.standardize_armature import AvatarToolkit_OT_StandardizeArmature
class AvatarToolKit_OT_ExportFBX(Operator):
"""Export selected objects as FBX"""
bl_idname: str = "avatar_toolkit.export_fbx"
bl_label: str = t("QuickAccess.export_fbx")
def execute(self, context: Context) -> Set[str]:
bpy.ops.export_scene.fbx('INVOKE_DEFAULT')
return {'FINISHED'}
class AvatarToolKit_MT_ExportMenu(Menu):
"""Export menu containing various export options"""
bl_idname: str = "AVATAR_TOOLKIT_MT_export_menu"
bl_label: str = t("QuickAccess.export")
def draw(self, context: Context) -> None:
layout: UILayout = self.layout
layout.operator(AvatarToolKit_OT_ExportFBX.bl_idname, text=t("QuickAccess.export_fbx"))
layout.operator(AvatarToolKit_OT_ExportResonite.bl_idname, text=t("QuickAccess.export_resonite"))
class AvatarToolKit_OT_ExportMenu(Operator):
"""Open the export menu"""
bl_idname: str = "avatar_toolkit.export"
bl_label: str = t("QuickAccess.export")
@classmethod
def poll(cls, context: Context) -> bool:
return get_active_armature(context) is not None
def execute(self, context: Context) -> Set[str]:
bpy.context.window_manager.popup_menu(AvatarToolKit_MT_ExportMenu.draw)
return {'FINISHED'}
class AvatarToolKit_PT_QuickAccessPanel(Panel):
"""Quick access panel for common Avatar Toolkit operations"""
bl_label: str = t("QuickAccess.label")
bl_idname: str = "OBJECT_PT_avatar_toolkit_quick_access"
bl_space_type: str = 'VIEW_3D'
bl_region_type: str = 'UI'
bl_category: str = CATEGORY_NAME
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order: int = 0
def draw(self, context: Context) -> None:
"""Draw the panel layout"""
layout: UILayout = self.layout
props = context.scene.avatar_toolkit
# Armature Selection
col = draw_section_header(layout, t("QuickAccess.select_armature"), icon='ARMATURE_DATA')
col.prop(context.scene.avatar_toolkit, "active_armature", text="")
# Get active armature
active_armature: Optional[Object] = get_active_armature(context)
if active_armature:
# Validation Section
col = draw_section_header(layout, t("Validation.label", "Armature Validation"), icon='CHECKMARK')
# Main validate button with prominent styling
validate_row = col.row(align=True)
validate_row.scale_y = UIStyle.PRIMARY_BUTTON_SCALE
validate_row.operator("avatar_toolkit.validate_armature_manual",
text=t("Validation.validate_now", "Validate Armature Now"),
icon='CHECKMARK')
# Validation mode selector
col.prop(props, "validation_mode", text=t("Settings.validation_mode", "Mode"))
# Show validation results if flag is set
if props.show_validation_results:
# Cache validation results
cache_key = f"validation_{active_armature.name}_{active_armature.data.name}_{len(active_armature.data.bones)}"
if cache_key not in _validation_cache:
_validation_cache[cache_key] = validate_armature(active_armature, detailed_messages=True)
is_valid, messages, is_acceptable, hierarchy_messages, scale_messages, non_standard_messages = _validation_cache[cache_key]
# Check if this is a PMX model
pmx_detected = is_pmx_model(active_armature)
results_box = col.box()
row = results_box.row()
row.prop(props, "show_validation_results", text=t("Validation.results", "Validation Results"),
icon='TRIA_DOWN' if props.show_validation_results else 'TRIA_RIGHT', emboss=False)
# PMX Model Notice
if pmx_detected:
pmx_box = results_box.box()
pmx_box.label(text=t("Armature.validation.pmx_model_detected"), icon='INFO')
validation_mode = context.scene.avatar_toolkit.validation_mode
if validation_mode == 'STRICT':
pmx_box.label(text=t("Armature.validation.pmx_model_strict"))
pmx_box.label(text=t("Armature.validation.pmx_model_standardize"))
else:
pmx_box.label(text=t("Armature.validation.pmx_model_basic"))
# Validation Results
if not is_valid:
# Display found bones
if messages and len(messages) > 0:
bones_section = results_box.box()
row = bones_section.row()
row.prop(props, "show_found_bones", text=t("Validation.section.found_bones"),
icon='TRIA_DOWN' if props.show_found_bones else 'TRIA_RIGHT', emboss=False)
if props.show_found_bones:
for line in messages[0].split('\n'):
bones_section.label(text=line)
# Status message
status_box = results_box.box()
row = status_box.row()
row.alert = True
row.label(text=t("Validation.status.failed"), icon='ERROR')
# Error explanation
error_box = results_box.box()
error_box.alert = True
error_box.label(text=t("Validation.message.failed.line1"))
error_box.label(text=t("Validation.message.failed.line2"))
error_box.label(text=t("Validation.message.failed.line3"))
# Non-Standard Bones section
if non_standard_messages or pmx_detected:
ns_section = results_box.box()
row = ns_section.row()
row.alert = True
row.prop(props, "show_non_standard", text=t("Validation.section.non_standard"),
icon='TRIA_DOWN' if props.show_non_standard else 'TRIA_RIGHT', emboss=False)
if props.show_non_standard:
if non_standard_messages and len(non_standard_messages) > 0:
for message in non_standard_messages:
for line in message.split('\n'):
sub_row = ns_section.row()
sub_row.alert = True
sub_row.label(text=line)
elif pmx_detected:
ns_section.alert = True
ns_section.label(text=t("Armature.validation.pmx_model_basic"))
ns_section.label(text=t("Armature.validation.pmx_model_strict"))
ns_section.label(text=t("Armature.validation.pmx_model_standardize"))
else:
ns_section.label(text=t("Validation.no_non_standard_issues"))
# Hierarchy Issues section
if hierarchy_messages:
hier_section = results_box.box()
row = hier_section.row()
row.alert = True
row.prop(props, "show_hierarchy", text=t("Validation.section.hierarchy"),
icon='TRIA_DOWN' if props.show_hierarchy else 'TRIA_RIGHT', emboss=False)
if props.show_hierarchy:
for message in hierarchy_messages:
sub_row = hier_section.row()
sub_row.alert = True
sub_row.label(text=message)
# Scale Issues section
if scale_messages:
scale_section = results_box.box()
row = scale_section.row()
row.alert = True
row.prop(props, "show_scale_issues", text=t("Validation.section.scale_issues"),
icon='TRIA_DOWN' if props.show_scale_issues else 'TRIA_RIGHT', emboss=False)
if props.show_scale_issues:
for scale_msg in scale_messages:
sub_row = scale_section.row()
sub_row.alert = True
sub_row.label(text=scale_msg)
elif is_valid and not is_acceptable:
# Valid armature - show stats
stats_cache_key = f"stats_{active_armature.name}_{active_armature.data.name}_{len(active_armature.data.bones)}"
if stats_cache_key not in _stats_cache:
_stats_cache[stats_cache_key] = get_armature_stats(active_armature)
stats = _stats_cache[stats_cache_key]
status_box = results_box.box()
row = status_box.row()
row.label(text=t("QuickAccess.valid_armature"), icon='CHECKMARK')
split = row.split(factor=0.4)
split.label(text=t("QuickAccess.bones_count", count=stats['bone_count']))
if stats['has_pose']:
results_box.label(text=t("QuickAccess.pose_bones_available"), icon='POSE_HLT')
elif is_valid and is_acceptable:
# Acceptable standard
status_box = results_box.box()
status_box.label(text=t("Armature.validation.acceptable_standard.success"), icon='INFO')
status_box.label(text=t("Armature.validation.acceptable_standard.note"))
status_box.label(text=t("Armature.validation.acceptable_standard.option"))
# Add standardize button
standardize_box = results_box.box()
standardize_box.operator(AvatarToolkit_OT_StandardizeArmature.bl_idname,
text=t("QuickAccess.standardize_armature"),
icon='MODIFIER')
# T-Pose Validation
col = draw_section_header(layout, t("Validation.tpose.label"), icon='ARMATURE_DATA')
col.operator(AvatarToolkit_OT_ValidateTPose.bl_idname, text=t("Validation.tpose.validate_now"), icon='CHECKMARK')
if props.show_tpose_validation:
validation_result_col = col.column(align=True)
if props.tpose_validation_result:
validation_result_col.label(text=t("Validation.tpose.valid"), icon='CHECKMARK')
else:
validation_result_col.alert = True
validation_result_col.label(text=t("Validation.tpose.warning"), icon='ERROR')
for msg in props.tpose_validation_messages:
validation_result_col.label(text=msg.name)
# Pose Mode Controls
col = draw_section_header(layout, t("QuickAccess.pose_controls"), icon='ARMATURE_DATA')
if context.mode == "POSE":
col.operator(AvatarToolkit_OT_StopPoseMode.bl_idname, icon='POSE_HLT')
col.separator(factor=UIStyle.SUBSECTION_SEPARATOR_FACTOR)
draw_operator_row(col, [
(AvatarToolkit_OT_ApplyPoseAsRest.bl_idname, t("QuickAccess.pose_as_rest"), 'MOD_ARMATURE'),
(AvatarToolkit_OT_ApplyPoseAsShapekey.bl_idname, t("QuickAccess.pose_as_shapekey"), 'MOD_ARMATURE')
])
else:
col.operator(AvatarToolkit_OT_StartPoseMode.bl_idname, icon='POSE_HLT')
# Import/Export Section
col = draw_section_header(layout, t("QuickAccess.import_export"), icon='IMPORT')
# Import/Export Buttons
draw_operator_row(col, [
(AvatarToolKit_OT_Import.bl_idname, t("QuickAccess.import"), 'IMPORT'),
(AvatarToolKit_OT_ExportMenu.bl_idname, t("QuickAccess.export"), 'EXPORT')
], scale_y=UIStyle.PRIMARY_BUTTON_SCALE)