Files
Dynamic-Link-Manager/ui/panels.py
T
2026-03-10 10:21:13 -06:00

178 lines
9.3 KiB
Python

# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
import bpy
from bpy.types import Panel, UIList
from . import properties
class DLM_UL_library_list(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
if self.layout_type in {"DEFAULT", "COMPACT"}:
layout.scale_x = 0.4
layout.label(text=item.name)
layout.scale_x = 0.3
if item.is_missing:
layout.label(text="MISSING", icon="ERROR")
elif item.is_indirect:
layout.label(text="INDIRECT", icon="INFO")
else:
layout.label(text="OK", icon="FILE_BLEND")
layout.scale_x = 0.3
path_text = item.filepath or ""
if path_text.startswith("\\\\"):
parts = path_text.split("\\")
short_path = f"\\\\{parts[2]}\\" if len(parts) >= 3 else "\\\\ (network)"
elif len(path_text) >= 2 and path_text[1] == ":":
short_path = f"{path_text[:2]}\\"
elif path_text.startswith("//"):
short_path = "// (relative)"
else:
short_path = path_text[:15] + "..." if len(path_text) > 15 else path_text
layout.label(text=short_path, icon="FILE_FOLDER")
elif self.layout_type == "GRID":
layout.alignment = "CENTER"
layout.label(text="", icon="FILE_BLEND")
def _get_short_path(filepath):
if not filepath:
return "Unknown"
if filepath.startswith("//"):
return "// (relative)"
if filepath.startswith("\\\\"):
parts = filepath.split("\\")
return f"\\\\{parts[2]}\\" if len(parts) >= 3 else "\\\\ (network)"
if len(filepath) >= 2 and filepath[1] == ":":
return f"{filepath[:2]}\\"
if filepath.startswith("/"):
parts = filepath.split("/")
return f"/{parts[1]}/" if len(parts) >= 2 else "/ (root)"
return "Unknown"
class DLM_PT_main_panel(Panel):
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Dynamic Link Manager"
bl_label = "Dynamic Link Manager"
def draw_header(self, context):
layout = self.layout
layout.operator("preferences.addon_show", text="", icon="PREFERENCES").module = __package__.rsplit(".", 1)[0]
def draw(self, context):
layout = self.layout
props = context.scene.dynamic_link_manager
# Path management
row = layout.row()
row.operator("dlm.make_paths_relative", text="Make Paths Relative", icon="FILE_PARENT")
row.operator("dlm.make_paths_absolute", text="Make Paths Absolute", icon="FILE_FOLDER")
# CharMig section (placeholder; full UI in implementation step 5)
box = layout.box()
box.label(text="Character Migrator")
row = box.row()
row.prop(props, "migrator_mode", text="Automatic pair discovery")
row = box.row()
row.prop(props, "original_character", text="Original")
row.operator("dlm.picker_original_character", text="", icon="EYEDROPPER")
row = box.row()
row.prop(props, "replacement_character", text="Replacement")
row.operator("dlm.picker_replacement_character", text="", icon="EYEDROPPER")
row = box.row()
row.operator("dlm.migrator_remove_original", text="Remove Original", icon="TRASH")
row = box.row(align=True)
row.operator("dlm.migrator_copy_attributes", text="CopyAttr", icon="COPY_ID")
row.operator("dlm.migrator_migrate_nla", text="MigNLA", icon="NLA")
row.operator("dlm.migrator_custom_properties", text="MigCustProps", icon="PROPERTIES")
row = box.row(align=True)
row.operator("dlm.migrator_bone_constraints", text="MigBoneConst", icon="CONSTRAINT_BONE")
row.operator("dlm.migrator_retarget_relations", text="RetargRelatives", icon="ORIENTATION_PARENT")
# Situational
situational_box = layout.box()
situational_box.label(text="Situational Fixes", icon="QUESTION")
row = situational_box.row(align=True)
row.operator("dlm.migrator_basebody_shapekeys", text="MigBBodyShapeKeys", icon="SHAPEKEY_DATA")
row = situational_box.row(align=True)
row.operator("dlm.migrator_fk_rotations", text="MigFKRot", icon="BONE_DATA")
row.operator("dlm.migrator_fk_rotations_remove", text="Remove", icon="X")
row.operator("dlm.migrator_fk_rotations_bake", text="Bake", icon="KEYFRAME")
# Tweak Tools: header row (always), main box only when expanded
section_icon = "DISCLOSURE_TRI_DOWN" if props.tweak_tools_section_expanded else "DISCLOSURE_TRI_RIGHT"
row = layout.row(align=True)
row.prop(props, "tweak_tools_section_expanded", text="", icon=section_icon, icon_only=True)
row.label(text="Tweak Tools", icon="CONSTRAINT")
if props.tweak_tools_section_expanded:
tweak_box = layout.box()
row = tweak_box.row(align=True)
row.operator("dlm.tweak_add_arm", text="Add Arm", icon="CONSTRAINT_BONE")
row.operator("dlm.tweak_remove_arm", text="Remove Arm", icon="X")
row.operator("dlm.tweak_bake_arm", text="Bake Arm", icon="KEYFRAME")
row = tweak_box.row(align=True)
row.operator("dlm.tweak_add_leg", text="Add Leg", icon="CONSTRAINT_BONE")
row.operator("dlm.tweak_remove_leg", text="Remove Leg", icon="X")
row.operator("dlm.tweak_bake_leg", text="Bake Leg", icon="KEYFRAME")
row = tweak_box.row(align=True)
row.operator("dlm.tweak_add_both", text="Add Both", icon="CONSTRAINT_BONE")
row.operator("dlm.tweak_remove_both", text="Remove Both", icon="X")
row.operator("dlm.tweak_bake_both", text="Bake Both", icon="KEYFRAME")
row = tweak_box.row()
row.prop(props, "tweak_nla_track_name", text="NLA track")
row = tweak_box.row()
row.prop(props, "tweak_bake_post_clean", text="Post-clean after bake")
# Linked Libraries: header row (always), main box only when expanded
missing_count = sum(1 for lib in props.linked_libraries if lib.is_missing)
section_icon = "DISCLOSURE_TRI_DOWN" if props.linked_libraries_section_expanded else "DISCLOSURE_TRI_RIGHT"
row = layout.row(align=True)
row.prop(props, "linked_libraries_section_expanded", text="", icon=section_icon, icon_only=True)
row.label(text="Linked Libraries Analysis")
if missing_count > 0:
row.label(text=f"({props.linked_assets_count} libs, {missing_count} missing)", icon="ERROR")
else:
row.label(text=f"({props.linked_assets_count} libraries)")
if props.linked_libraries_section_expanded:
box = layout.box()
row = box.row()
row.operator("dlm.scan_linked_assets", text="Scan Linked Assets", icon="FILE_REFRESH")
if props.linked_assets_count > 0:
row = box.row()
row.template_list("DLM_UL_library_list", "", props, "linked_libraries", props, "linked_libraries_index")
row = box.row()
row.operator("dlm.reload_libraries", text="Reload Libraries", icon="FILE_REFRESH")
prefs = context.preferences.addons.get(__package__.rsplit(".", 1)[0])
if prefs and prefs.preferences.search_paths:
for i, path_item in enumerate(prefs.preferences.search_paths):
row = box.row()
row.prop(path_item, "path", text=f"Search path {i+1}")
row.operator("dlm.browse_search_path", text="", icon="FILE_FOLDER").index = i
row.operator("dlm.remove_search_path", text="", icon="REMOVE").index = i
row = box.row()
row.alignment = "RIGHT"
row.operator("dlm.add_search_path", text="Add search path", icon="ADD")
if missing_count > 0:
row = box.row()
row.operator("dlm.find_libraries_in_folders", text="Find libraries in these folders", icon="VIEWZOOM")
if props.linked_libraries and 0 <= props.linked_libraries_index < len(props.linked_libraries):
selected_lib = props.linked_libraries[props.linked_libraries_index]
info_box = box.box()
if selected_lib.is_missing:
info_box.alert = True
info_box.label(text=f"Selected: {selected_lib.name}")
if selected_lib.is_missing:
info_box.label(text="Status: MISSING", icon="ERROR")
elif selected_lib.is_indirect:
info_box.label(text="Status: INDIRECT", icon="INFO")
else:
info_box.label(text="Status: OK", icon="FILE_BLEND")
info_box.label(text=f"Path: {selected_lib.filepath}", icon="FILE_FOLDER")
info_box.operator("dlm.open_linked_file", text="Open Blend", icon="FILE_BLEND").filepath = selected_lib.filepath
op = info_box.operator("dlm.relocate_single_library", text="Relocate Library", icon="FILE_FOLDER")
op.target_filepath = selected_lib.filepath