# SPDX-License-Identifier: MIT OR GPL-3.0-or-later from collections.abc import Set as AbstractSet from typing import Callable, Optional, Protocol, TypeVar, Union, runtime_checkable from bpy.app.translations import pgettext from bpy.types import ( AnyType, Armature, Context, Menu, Operator, Panel, UILayout, UIList, ) from ..common import version from ..common.preferences import get_preferences from . import make_armature, search, validation from .extension import get_armature_extension from .ops import layout_operator __AddOperator = TypeVar("__AddOperator", bound=Operator) __RemoveOperator = TypeVar("__RemoveOperator", bound=Operator) __MoveUpOperator = TypeVar("__MoveUpOperator", bound=Operator) __MoveDownOperator = TypeVar("__MoveDownOperator", bound=Operator) @runtime_checkable class TemplateListCollectionProtocol(Protocol): def __len__(self) -> int: ... def __getitem__(self, index: int) -> object: ... def draw_template_list( layout: UILayout, template_list: type[UIList], base_object: AnyType, collection_attribue_name: str, active_index_attribute_name: str, add_operator_type: type[__AddOperator], remove_operator_type: type[__RemoveOperator], move_up_operator_type: type[__MoveUpOperator], move_down_operator_type: type[__MoveDownOperator], *, can_remove: Callable[[int], bool] = lambda _: True, can_move: Callable[[int], bool] = lambda _: True, compact: bool = False, menu: Optional[type[Menu]] = None, ) -> tuple[ list[Union[__AddOperator, __RemoveOperator, __MoveUpOperator, __MoveDownOperator]], list[Union[__RemoveOperator, __MoveUpOperator, __MoveDownOperator]], int, object, tuple[ __AddOperator, __RemoveOperator, Optional[__MoveUpOperator], Optional[__MoveDownOperator], ], ]: collection = getattr(base_object, collection_attribue_name, None) if not isinstance(collection, TemplateListCollectionProtocol): message = ( f"{collection}.{collection_attribue_name}" + " is not a Template List Collection Protocol." ) raise TypeError(message) active_index = getattr(base_object, active_index_attribute_name, None) if not isinstance(active_index, int): message = f"{base_object}.{active_index_attribute_name} is not an int." raise TypeError(message) if 0 <= active_index < len(collection): active_object = collection[active_index] else: active_object = None length = len(collection) list_row_len = 4 if length > int(compact) else 2 list_row = layout.row() list_row.template_list( template_list.bl_idname, "", base_object, collection_attribue_name, base_object, active_index_attribute_name, rows=list_row_len, ) list_side_column = list_row.column(align=True) add_operator = layout_operator( list_side_column, add_operator_type, icon="ADD", text="", translate=False ) if length >= 1 and 0 <= active_index < length and can_remove(active_index): remove_operator_parent = list_side_column else: remove_operator_parent = list_side_column.column(align=True) remove_operator_parent.enabled = False remove_operator = layout_operator( remove_operator_parent, remove_operator_type, icon="REMOVE", text="", translate=False, ) if menu is not None: list_side_column.separator() list_side_column.menu(menu=menu.bl_idname, text="", icon="DOWNARROW_HLT") move_operator_parent = list_side_column.column(align=True) # Without separator, the button width changes mysteriously when list elements are 0 move_operator_parent.separator() if length <= 1 or not (0 <= active_index < length) or not can_move(active_index): move_operator_parent.enabled = False collection_ops: list[ Union[__AddOperator, __RemoveOperator, __MoveUpOperator, __MoveDownOperator] ] = [add_operator] collection_item_ops: list[ Union[__RemoveOperator, __MoveUpOperator, __MoveDownOperator] ] = [remove_operator] if length > int(compact): move_up_operator = layout_operator( move_operator_parent, move_up_operator_type, icon="TRIA_UP", text="", translate=False, ) collection_item_ops.append(move_up_operator) move_down_operator = layout_operator( move_operator_parent, move_down_operator_type, icon="TRIA_DOWN", text="", translate=False, ) collection_item_ops.append(move_down_operator) else: move_up_operator = None move_down_operator = None collection_ops.extend(collection_item_ops) return ( collection_ops, collection_item_ops, active_index, active_object, (add_operator, remove_operator, move_up_operator, move_down_operator), ) class VRM_PT_vrm_armature_object_property(Panel): bl_idname = "VRM_PT_vrm_armature_object_property" bl_label = "VRM" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "object" @classmethod def poll(cls, context: Context) -> bool: active_object = context.active_object if not active_object: return False armature_data = active_object.data return isinstance(armature_data, Armature) def draw(self, _context: Context) -> None: warning_message = version.panel_warning_message() if not warning_message: return box = self.layout.box() warning_column = box.column() for index, warning_line in enumerate(warning_message.splitlines()): warning_column.label( text=warning_line, translate=False, icon="NONE" if index else "ERROR", ) def add_armature(add_armature_op: Operator, _context: Context) -> None: layout_operator( add_armature_op.layout, make_armature.ICYP_OT_make_armature, text="VRM Humanoid", icon="OUTLINER_OB_ARMATURE", ).skip_heavy_armature_setup = True class VRM_PT_current_selected_armature(Panel): bl_idname = "VRM_PT_current_selected_armature" bl_label = "Current selected armature" bl_space_type = "VIEW_3D" bl_region_type = "UI" bl_category = "VRM" bl_options: AbstractSet[str] = {"HIDE_HEADER"} @classmethod def poll(cls, context: Context) -> bool: return search.multiple_armatures_exist(context) def draw(self, context: Context) -> None: armature = search.current_armature(context) if not armature: return layout = self.layout layout.label(text=armature.name, icon="ARMATURE_DATA", translate=False) class VRM_PT_controller(Panel): bl_idname = "ICYP_PT_ui_controller" bl_label = "Operator" bl_space_type = "VIEW_3D" bl_region_type = "UI" bl_category = "VRM" def draw_header(self, _context: Context) -> None: self.layout.label(icon="TOOL_SETTINGS") def draw(self, context: Context) -> None: layout = self.layout preferences = get_preferences(context) # draw_main layout_operator( layout, make_armature.ICYP_OT_make_armature, text=pgettext("Create VRM Model"), icon="OUTLINER_OB_ARMATURE", ).skip_heavy_armature_setup = True vrm_validator_op = layout_operator( layout, validation.WM_OT_vrm_validator, text=pgettext("Check VRM Model"), icon="VIEWZOOM", ) vrm_validator_op.show_successful_message = True layout.prop(preferences, "export_invisibles") layout.prop(preferences, "export_only_selections") armature = search.current_armature(context) if armature: vrm_validator_op.armature_object_name = armature.name armature_data = armature.data if isinstance(armature_data, Armature): layout.prop( get_armature_extension(armature_data), "spec_version", text="", translate=False, ) class VRM_PT_controller_unsupported_blender_version_warning(Panel): bl_idname = "VRM_PT_controller_unsupported_blender_version_warning" bl_label = "Unsupported Blender Version Warning" bl_space_type = "VIEW_3D" bl_region_type = "UI" bl_category = "VRM" bl_options: AbstractSet[str] = {"HIDE_HEADER"} @classmethod def poll(cls, _context: Context) -> bool: return bool(version.panel_warning_message()) def draw(self, _context: Context) -> None: warning_message = version.panel_warning_message() if warning_message is None: return box = self.layout.box() warning_column = box.column() for index, warning_line in enumerate(warning_message.splitlines()): warning_column.label( text=warning_line, translate=False, icon="NONE" if index else "ERROR", )