#!/usr/bin/env python # -*- coding: utf-8 -*- import logging import os import webbrowser from typing import Callable, List from maya import cmds from maya.cmds import confirmDialog from PySide2.QtCore import ( QCoreApplication, Qt, Signal, QRect, QPoint, QSize, ) from PySide2.QtWidgets import ( QApplication, QCheckBox, QGridLayout, QHBoxLayout, QLabel, QMainWindow, QMessageBox, QProgressBar, QPushButton, QTabWidget, QTreeWidget, QTreeWidgetItem, QTreeWidgetItemIterator, QVBoxLayout, QWidget, QLayout, QScrollArea, QGroupBox, ) from PySide2 import QtGui from .. import DNA, build_rig from ..builder.config import RigConfig from ..dnalib.layer import Layer from ..version import __version__ from .widgets import FileChooser, QHLine def show() -> None: DnaViewerWindow.show_window() WINDOW_OBJECT = "dnaviewer" WINDOW_TITLE = "DNA Viewer" HELP_URL = "https://epicgames.github.io/MetaHuman-DNA-Calibration/" SPACING = 6 WINDOW_SIZE_WIDTH_MIN = 800 WINDOW_SIZE_WIDTH_MAX = 1200 WINDOW_SIZE_HEIGHT_MIN = 800 WINDOW_SIZE_HEIGHT_MAX = 1000 MARGIN_LEFT = 8 MARGIN_TOP = 8 MARGIN_RIGHT = 8 MARGIN_BOTTOM = 8 MARGIN_HEADER_LEFT = 0 MARGIN_HEADER_TOP = 0 MARGIN_HEADER_RIGHT = 0 MARGIN_HEADER_BOTTOM = 0 MARGIN_BODY_LEFT = 0 MARGIN_BODY_TOP = 0 MARGIN_BODY_RIGHT = 0 class FlowLayout(QLayout): def __init__(self, parent=None): super().__init__(parent) self.itemList = [] self.spacing_x = 5 self.spacing_y = 5 def addItem(self, item): self.itemList.append(item) def count(self): return len(self.itemList) def itemAt(self, index): if 0 <= index < len(self.itemList): return self.itemList[index] return None def takeAt(self, index): if 0 <= index < len(self.itemList): return self.itemList.pop(index) return None def expandingDirections(self): return Qt.Orientations(Qt.Orientation(0)) def hasHeightForWidth(self): return True def heightForWidth(self, width): height = self.doLayout(QRect(0, 0, width, 0), True) return height def setGeometry(self, rect): super().setGeometry(rect) self.doLayout(rect, False) def sizeHint(self): return self.minimumSize() def minimumSize(self): size = QSize() for item in self.itemList: size = size.expandedTo(item.minimumSize()) return size def doLayout(self, rect, testOnly): x = rect.x() y = rect.y() lineHeight = 0 for item in self.itemList: widget = item.widget() spaceX = self.spacing_x spaceY = self.spacing_y nextX = x + item.sizeHint().width() + spaceX if nextX - spaceX > rect.right() and lineHeight > 0: x = rect.x() y = y + lineHeight + spaceY nextX = x + item.sizeHint().width() + spaceX lineHeight = 0 if not testOnly: item.setGeometry(QRect(QPoint(x, y), item.sizeHint())) x = nextX lineHeight = max(lineHeight, item.sizeHint().height()) return y + lineHeight - rect.y() class DNABrowserWidget(QWidget): dna_selected = Signal(str) def __init__(self, dna_path, img_path, parent=None): super().__init__(parent) self.dna_path = dna_path self.img_path = img_path self.setup_ui() self.scan_dna_files() self.update_grid() self.calculate_and_set_height() def calculate_and_set_height(self): container_width = self.flow_widget.width() or 800 button_width = (container_width - 40) // 3 button_height = button_width self.setFixedHeight(button_height * 3 + 10 * 4) def setup_ui(self): self.main_layout = QVBoxLayout(self) self.main_layout.setContentsMargins(0, 0, 0, 0) self.flow_widget = QWidget() self.flow_layout = FlowLayout(self.flow_widget) self.flow_layout.setSpacing(5) self.scroll_area = QScrollArea() self.scroll_area.setWidgetResizable(True) self.scroll_area.setWidget(self.flow_widget) self.setup_scroll_area_style() self.main_layout.addWidget(self.scroll_area) def setup_scroll_area_style(self): self.scroll_area.setStyleSheet(""" QScrollArea { border: none; background-color: transparent; } QScrollBar:vertical { border: none; background: #F0F0F0; width: 8px; margin: 0px; } QScrollBar::handle:vertical { background: #CCCCCC; border-radius: 4px; min-height: 20px; } QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0px; } """) def scan_dna_files(self): self.dna_files = {} if not os.path.exists(self.dna_path): cmds.warning(f"DNA path not found: {self.dna_path}") return if not os.path.exists(self.img_path): cmds.warning(f"Image path not found: {self.img_path}") return for file in os.listdir(self.dna_path): if file.endswith('.dna'): name = os.path.splitext(file)[0] dna_file = os.path.join(self.dna_path, file).replace("\\", "/") img_file = None for ext in ['.jpg', '.png', '.jpeg']: img_path = os.path.join(self.img_path, f"{name}{ext}").replace("\\", "/") if os.path.exists(img_path): img_file = img_path break self.dna_files[name] = { 'dna_path': dna_file, 'img_path': img_file } def update_grid(self): for i in reversed(range(self.flow_layout.count())): item = self.flow_layout.takeAt(i) if item.widget(): item.widget().deleteLater() container_width = self.flow_widget.width() or 800 button_width = (container_width - 40) // 3 button_height = button_width for name, info in sorted(self.dna_files.items()): self.flow_layout.addWidget(self.create_dna_button(name, info, button_width, button_height)) def create_dna_button(self, name, info, width, height): btn = QPushButton() btn.setFixedSize(width, height) layout = QVBoxLayout(btn) layout.setContentsMargins(10, 10, 10, 10) layout.setSpacing(5) icon_label = QLabel() icon_label.setAlignment(Qt.AlignCenter) icon_size = height - 40 if info['img_path']: icon_label.setPixmap( QtGui.QPixmap(info['img_path']).scaled( icon_size, icon_size, Qt.KeepAspectRatio, Qt.SmoothTransformation ) ) else: icon_label.setText("No Image") icon_label.setStyleSheet("color: #FFFFFF; font-size: 12px;") text_label = QLabel(name) text_label.setAlignment(Qt.AlignCenter) text_label.setStyleSheet(""" color: #FFFFFF; font-size: 12px; font-weight: bold; """) layout.addWidget(icon_label, 1) layout.addWidget(text_label) btn.setStyleSheet(""" QPushButton { background-color: #303030; border: 2px solid #202020; border-radius: 10px; padding: 8px; color: #FFFFFF; } QPushButton:hover { background-color: #404040; border: 2px solid #505050; } QPushButton:pressed { background-color: #202020; border: 2px solid #606060; } """) btn.setProperty('dna_path', info['dna_path']) btn.clicked.connect(lambda: self.on_dna_selected(info['dna_path'])) return btn def on_dna_selected(self, dna_path): self.dna_selected.emit(dna_path) class MeshTreeList(QWidget): def __init__(self, main_window: "DnaViewerWindow") -> None: super().__init__() self.main_window = main_window label = QLabel("Meshes:") self.mesh_tree = self.create_mesh_tree() layout = QGridLayout() layout.addWidget(self.mesh_tree, 0, 0, 4, 1) layout.setContentsMargins( MARGIN_BODY_LEFT, MARGIN_BODY_TOP, MARGIN_BODY_RIGHT, MARGIN_BOTTOM, ) layout_holder = QVBoxLayout() layout_holder.addWidget(label) layout_holder.addLayout(layout) layout_holder.setContentsMargins( MARGIN_BODY_LEFT, MARGIN_BODY_TOP, MARGIN_BODY_RIGHT, MARGIN_BOTTOM, ) buttons_layout = QHBoxLayout() self.btn_select_all = QPushButton("Select all meshes") self.btn_select_all.setEnabled(False) self.btn_select_all.clicked.connect(self.select_all) buttons_layout.addWidget(self.btn_select_all) self.btn_deselect_all = QPushButton("Deselect all meshes") self.btn_deselect_all.setEnabled(False) self.btn_deselect_all.clicked.connect(self.deselect_all) buttons_layout.addWidget(self.btn_deselect_all) layout_holder.addLayout(buttons_layout) self.setLayout(layout_holder) def create_mesh_tree(self) -> QWidget: mesh_tree = QTreeWidget() mesh_tree.setHeaderHidden(True) mesh_tree.itemChanged.connect(self.tree_item_changed) mesh_tree.setStyleSheet("background-color: #505050") mesh_tree.setToolTip("Select mesh or meshes to add to rig") return mesh_tree def fill_mesh_list( self, lod_count: int, names: List[str], indices_names: List[List[int]] ) -> None: """ Fills the mesh list with the meshes, and groups them by lods @type lod_count: int @param lod_count: The LOD count @type names: List[str] @param names: The names and indices of all the meshes @type indices_names: List[List[int] @param indices_names: The names and indices of all the meshes """ self.mesh_tree.clear() for i in range(lod_count): parent = QTreeWidgetItem(self.mesh_tree) parent.setText(0, f"LOD {i}") parent.setFlags(parent.flags() | Qt.ItemIsTristate | Qt.ItemIsUserCheckable) meshes_in_lod = indices_names[i] for mesh_index in meshes_in_lod: child = QTreeWidgetItem(parent) child.setFlags(child.flags() | Qt.ItemIsUserCheckable) child.setText(0, f"{names[mesh_index]}") child.setCheckState(0, Qt.Unchecked) self.mesh_tree.setItemExpanded(parent, True) def get_selected_meshes(self) -> List[int]: """ Gets the selected meshes from the tree widget @rtype: List[int] @returns: The list of mesh indices that are selected """ meshes = [] iterator = QTreeWidgetItemIterator( self.mesh_tree, QTreeWidgetItemIterator.Checked ) while iterator.value(): item = iterator.value() mesh_name = item.text(0) mesh_index = self.main_window.dna.get_mesh_id_from_mesh_name(mesh_name) if mesh_index is not None: meshes.append(mesh_index) iterator += 1 return meshes def select_all(self) -> None: """ Selects all meshes in the tree widget """ self.iterate_over_items(Qt.Checked) def deselect_all(self) -> None: """ Deselects all meshes in the tree widget """ self.iterate_over_items(Qt.Unchecked) def iterate_over_items(self, state: Qt.CheckState) -> None: """ Deselects all meshes in the tree widget """ item = self.mesh_tree.invisibleRootItem() for index in range(item.childCount()): child = item.child(index) child.setCheckState(0, state) def tree_item_changed(self) -> None: """The method that gets called when a tree item gets its value changed""" meshes = self.get_selected_meshes() if meshes: self.main_window.skin_cb.setEnabled(self.main_window.joints_cb.checkState()) self.main_window.blend_shapes_cb.setEnabled(True) self.main_window.process_btn.setEnabled(True) self.main_window.rig_logic_cb.setEnabled(False) if len(meshes) == self.main_window.dna.get_mesh_count(): self.main_window.rig_logic_cb.setEnabled( self.main_window.joints_cb.checkState() and self.main_window.blend_shapes_cb.checkState() and self.main_window.skin_cb.checkState() and self.main_window.select_gui_path.get_file_path() is not None and self.main_window.select_analog_gui_path.get_file_path() is not None and self.main_window.select_aas_path.get_file_path() is not None ) else: self.main_window.skin_cb.setEnabled(False) self.main_window.blend_shapes_cb.setEnabled(False) self.main_window.process_btn.setEnabled( self.main_window.joints_cb.checkState() ) class DnaViewerWindow(QMainWindow): """ UI Window Attributes ---------- @type select_dna_path: FileChooser @param select_dna_path: The FileChooser widget for getting the DNA path @type load_dna_btn: QPushButton @param load_dna_btn: The button that starts loading in the DNA @type mesh_tree_list: QWidget @param mesh_tree_list: The widget that contains the meshes to be selected in a tree list @type joints_cb: QCheckBox @param joints_cb: The checkbox that represents if joints should be added @type blend_shapes_cb: QCheckBox @param blend_shapes_cb: The checkbox that represents if blend shapes should be added @type skin_cb: QCheckBox @param skin_cb: The checkbox that represents if skin should be added @type rig_logic_cb: QCheckBox @param rig_logic_cb: The checkbox that represents if rig logic should be added @type ctrl_attributes_on_root_joint_cb: QCheckBox @param ctrl_attributes_on_root_joint_cb: The checkbox that represents if control attributes on joint should be added @type animated_map_attributes_on_root_joint_cb: QCheckBox @param animated_map_attributes_on_root_joint_cb: The checkbox that represents if animated maps attributes on root joint should be added @type mesh_name_to_blend_shape_channel_name_cb: QCheckBox @param mesh_name_to_blend_shape_channel_name_cb: The checkbox that represents if mesh names to blend shapes channel name should be added @type key_frames_cb: QCheckBox @param key_frames_cb: The checkbox that represents if key frames should be added @type select_gui_path: FileChooser @param select_gui_path: The FileChooser widget for getting the gui path @type select_analog_gui_path: FileChooser @param select_analog_gui_path: The FileChooser widget for getting the analog gui path @type select_aas_path: FileChooser @param select_aas_path: The FileChooser widget for getting the additional assemble script path @type process_btn: QPushButton @param process_btn: The button that starts creating the scene and character @type progress_bar: QProgressBar @param progress_bar: The progress bar that shows the building progress """ _instance: "DnaViewerWindow" = None main_widget: QWidget = None select_dna_path: FileChooser = None load_dna_btn: QPushButton = None mesh_tree_list: QWidget = None joints_cb: QCheckBox = None blend_shapes_cb: QCheckBox = None skin_cb: QCheckBox = None rig_logic_cb: QCheckBox = None ctrl_attributes_on_root_joint_cb: QCheckBox = None animated_map_attributes_on_root_joint_cb: QCheckBox = None mesh_name_to_blend_shape_channel_name_cb: QCheckBox = None key_frames_cb: QCheckBox = None select_gui_path: FileChooser = None select_analog_gui_path: FileChooser = None select_aas_path: FileChooser = None process_btn: QPushButton = None progress_bar: QProgressBar = None dna: DNA = None def __init__(self, parent=None) -> None: super().__init__(parent) # 设置默认路径 self.default_paths = { 'gui_path': os.path.normpath("data/gui.ma"), 'analog_gui_path': os.path.normpath("data/analog_gui.ma"), 'additional_script_path': os.path.normpath("data/additional_assemble_script.py") } self.setup_window() self.create_ui() def setup_window(self) -> None: """A method for setting up the window""" self.setWindowFlags( self.windowFlags() | Qt.WindowTitleHint | Qt.WindowMaximizeButtonHint | Qt.WindowMinimizeButtonHint | Qt.WindowCloseButtonHint ) self.setAttribute(Qt.WA_DeleteOnClose) self.setObjectName(WINDOW_OBJECT) self.setWindowTitle(WINDOW_TITLE) self.setWindowFlags(Qt.Window) self.setFocusPolicy(Qt.StrongFocus) def create_ui(self) -> None: """Fills the window with UI elements""" self.main_widget = self.create_main_widget() self.setCentralWidget(self.main_widget) self.set_size() self.setStyleSheet(self.load_css()) def load_css(self) -> str: css = os.path.join(os.path.dirname(__file__), "app.css") with open(css, encoding="utf-8") as file: return file.read() def create_main_widget(self) -> QWidget: """ Creates the widget containing the UI elements @rtype: QtWidgets.QWidget @returns: the main widget """ header = self.create_header() body = self.create_body() widget = QWidget() layout = QVBoxLayout(widget) layout.addLayout(header) layout.addWidget(QHLine()) layout.addLayout(body) layout.setContentsMargins(MARGIN_LEFT, MARGIN_TOP, MARGIN_RIGHT, MARGIN_BOTTOM) layout.setSpacing(SPACING) return widget def set_size(self) -> None: """Sets the window size""" self.setMaximumSize(WINDOW_SIZE_WIDTH_MAX, WINDOW_SIZE_HEIGHT_MAX) self.setMinimumSize(WINDOW_SIZE_WIDTH_MIN, WINDOW_SIZE_HEIGHT_MIN) self.resize(WINDOW_SIZE_WIDTH_MIN, WINDOW_SIZE_HEIGHT_MIN) def show_message_dialog(self) -> bool: dlg = QMessageBox() dlg.setIcon(QMessageBox.Warning) dlg.setWindowTitle("Warning") dlg.setText( "Unsaved changes exists.\nSave changes and create new scene, discard changes, and create new scene or cancel procesing." ) dlg.setStandardButtons( QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel ) button = dlg.exec_() if button == QMessageBox.Save: cmds.SaveScene() return not cmds.file(q=True, modified=True) if button == QMessageBox.Cancel: return False return True def process(self) -> None: """处理DNA构建时使用默认路径""" options = { "joints": self.joints_cb.isChecked(), "blendShapes": self.blend_shapes_cb.isChecked(), "skinCluster": self.skin_cb.isChecked(), "rigLogic": self.rig_logic_cb.isChecked(), "ctrlAttributes": self.ctrl_attrs_cb.isChecked(), "animatedMapAttributes": self.anim_attrs_cb.isChecked(), "meshNameToBlendShape": self.mesh_name_cb.isChecked(), "keyFrame": self.key_frame_cb.isChecked(), "guiPath": self.default_paths['gui_path'], "analogGuiPath": self.default_paths['analog_gui_path'], "analogAssembleScript": self.default_paths['additional_script_path'] } # ... 其余处理代码保持不变 def set_progress(self, text: str = None, value: int = None) -> None: """Setting text and/or value to progress bar""" if text is not None: self.progress_bar.setFormat(text) if value is not None: self.progress_bar.setValue(value) @staticmethod def show_window() -> None: if DnaViewerWindow._instance is None: DnaViewerWindow._instance = DnaViewerWindow( parent=DnaViewerWindow.maya_main_window() ) DnaViewerWindow.activate_window() @staticmethod def maya_main_window() -> QWidget: """ Gets the MayaWindow instance @throws RuntimeError @rtype: QtWidgets.QWidget @returns: main window instance """ for obj in QApplication.topLevelWidgets(): if obj.objectName() == "MayaWindow": return obj raise RuntimeError("Could not find MayaWindow instance") @staticmethod def activate_window() -> None: """Shows window if minimized""" try: DnaViewerWindow._instance.show() if DnaViewerWindow._instance.windowState() & Qt.WindowMinimized: DnaViewerWindow._instance.setWindowState(Qt.WindowActive) DnaViewerWindow._instance.raise_() DnaViewerWindow._instance.activateWindow() except RuntimeError as e: logging.info(e) if str(e).rstrip().endswith("already deleted."): DnaViewerWindow._instance = None DnaViewerWindow.show_window() def add_joints(self) -> bool: return self.is_checked(self.joints_cb) def add_blend_shapes(self) -> bool: return self.is_checked(self.blend_shapes_cb) def add_skin_cluster(self) -> bool: return self.is_checked(self.skin_cb) def add_rig_logic(self) -> bool: return self.is_checked(self.rig_logic_cb) def add_ctrl_attributes_on_root_joint(self) -> bool: return self.is_checked(self.ctrl_attrs_cb) def add_animated_map_attributes_on_root_joint(self) -> bool: return self.is_checked(self.anim_attrs_cb) def add_mesh_name_to_blend_shape_channel_name(self) -> bool: return self.is_checked(self.mesh_name_cb) def add_key_frames(self) -> bool: return self.is_checked(self.key_frame_cb) def is_checked(self, checkbox: QCheckBox) -> bool: """ Returns if the provided checkbox is checked and enabled @type checkbox: QCheckBox @param checkbox: The checkbox thats value needs to be checked and enabled @rtype: bool @returns: The flag representing if the checkbox is checked and enabled """ return ( checkbox is not None and bool(checkbox.isEnabled()) and checkbox.checkState() == Qt.CheckState.Checked ) def create_body(self) -> QVBoxLayout: """ Creates the main body layout and adds needed widgets @rtype: QVBoxLayout @returns: The created vertical box layout with the widgets added """ self.body = QVBoxLayout() self.body.setContentsMargins( MARGIN_BODY_LEFT, MARGIN_BODY_TOP, MARGIN_BODY_RIGHT, MARGIN_BOTTOM, ) self.body.setSpacing(SPACING) # 创建但隐藏文件输入控件 self.create_file_inputs() self.create_dna_selector() self.mesh_tree_list = self.create_mesh_selector() self.build_options = self.create_build_options() self.extra_build_options = self.create_extra_build_options() tab = QTabWidget(self) tab.addTab(self.build_options, "Build options") tab.addTab(self.extra_build_options, "Extra options") widget = QWidget() layout = QHBoxLayout(widget) layout.addWidget(tab) self.body.addWidget(widget) self.process_btn = self.create_process_btn() self.progress_bar = self.create_progress_bar() return self.body def create_header(self) -> QHBoxLayout: """ Creates and adds to the header widget @rtype: QHBoxLayout @returns: The created horizontal box layout with the widgets added """ self.header = QHBoxLayout() label = QLabel("v" + __version__) btn = self.create_help_btn() self.header.addWidget(label) self.header.addStretch(1) self.header.addWidget(btn) self.header.setContentsMargins( MARGIN_HEADER_LEFT, MARGIN_HEADER_TOP, MARGIN_HEADER_RIGHT, MARGIN_HEADER_BOTTOM, ) self.header.setSpacing(SPACING) return self.header def create_help_btn(self) -> QWidget: """ Creates the help button widget @rtype: QHBoxLayout @returns: The created horizontal box layout with the widgets added """ btn = QPushButton(self) btn.setText(" ? ") btn.setToolTip("Help") btn.clicked.connect(self.on_help) return btn def on_help(self) -> None: """The method that gets called when the help button is clicked""" if HELP_URL: webbrowser.open(HELP_URL) else: QMessageBox.about( self, "About", "Sorry, this application does not have documentation yet.", ) def create_dna_selector(self) -> QWidget: widget = QWidget() layout = QVBoxLayout() layout.setSpacing(5) dna_group = QGroupBox("DNA") dna_layout = QVBoxLayout(dna_group) dna_layout.setContentsMargins(5, 5, 5, 5) dna_layout.setSpacing(10) self.dna_browser = DNABrowserWidget( os.path.join(os.path.dirname(__file__), "..", "..", "..", "data", "dna"), os.path.join(os.path.dirname(__file__), "..", "..", "..", "data", "img") ) self.dna_browser.dna_selected.connect(self.on_dna_browser_selected) dna_layout.addWidget(self.dna_browser) self.select_dna_path = self.create_dna_chooser() dna_layout.addWidget(self.select_dna_path) self.load_dna_btn = self.create_load_dna_button(self.select_dna_path) dna_layout.addWidget(self.load_dna_btn) self.select_dna_path.fc_text_field.textChanged.connect( lambda: self.on_dna_selected(self.select_dna_path) ) layout.addWidget(dna_group) layout.setContentsMargins( MARGIN_HEADER_LEFT, MARGIN_HEADER_TOP, MARGIN_HEADER_RIGHT, MARGIN_HEADER_BOTTOM, ) self.setMinimumWidth(900) widget.setLayout(layout) self.body.addWidget(widget) return widget def on_dna_browser_selected(self, dna_path: str) -> None: """ Handle DNA selection from DNA Browser """ if dna_path and os.path.exists(dna_path): self.select_dna_path.fc_text_field.setText(dna_path) self.on_dna_selected(self.select_dna_path) def on_dna_selected(self, input: FileChooser) -> None: """ Handle DNA selection from file input """ dna_path = input.get_file_path() enabled = dna_path is not None and os.path.exists(dna_path) self.load_dna_btn.setEnabled(enabled) self.process_btn.setEnabled(False) # Update DNA path variable if enabled: global DNA_File DNA_File = dna_path def create_dna_chooser(self) -> FileChooser: """ Creates and adds the DNA chooser widget @rtype: FileChooser @returns: Dna chooser widget """ return self.create_file_chooser( "Path:", "DNA file to load. Required by all gui elements", "Select a DNA file", "DNA files (*.dna)", self.on_dna_changed, ) def on_dna_changed(self, state: int) -> None: # pylint: disable=unused-argument """ Method that gets called when the checkbox is changed @type state: int @param state: The changed state of the checkbox """ enabled = False if self.dna: if self.dna.path == self.select_dna_path.get_file_path(): enabled = True self.load_dna_btn.setEnabled(enabled) self.mesh_tree_list.btn_select_all.setEnabled(enabled) self.mesh_tree_list.btn_deselect_all.setEnabled(enabled) self.process_btn.setEnabled(enabled) def create_load_dna_button(self, dna_input: FileChooser) -> QWidget: """ Creates and adds the load DNA button widget @type input: FileChooser @param input: The file chooser object corresponding to the DNA selector widget @rtype: QWidget @returns: The created load DNA button widget """ btn = QPushButton("Load DNA") btn.setEnabled(False) btn.clicked.connect(lambda: self.on_load_dna_clicked(dna_input)) return btn def on_load_dna_clicked(self, input: FileChooser) -> None: """ The method that gets called when a DNA file gets selected @type input: FileChooser @param input: The file chooser object corresponding to the DNA selector widget """ self.main_widget.setEnabled(False) QCoreApplication.processEvents() try: dna_file_path = input.get_file_path() if dna_file_path: self.dna = DNA(dna_file_path, [Layer.definition]) lod_count = self.dna.get_lod_count() names = self.get_mesh_names() indices_names = self.get_lod_indices_names() self.mesh_tree_list.fill_mesh_list(lod_count, names, indices_names) self.joints_cb.setEnabled(True) self.enable_additional_build_options(True) self.process_btn.setEnabled(False) self.mesh_tree_list.btn_select_all.setEnabled(True) self.mesh_tree_list.btn_deselect_all.setEnabled(True) except Exception as e: dlg = QMessageBox() dlg.setIcon(QMessageBox.Warning) dlg.setWindowTitle("Error") dlg.setText(str(e)) dlg.setStandardButtons(QMessageBox.Ok) dlg.exec_() self.main_widget.setEnabled(True) def get_mesh_names(self) -> List[str]: """Reads in the meshes of the definition""" names: List[str] = [] for index in range(self.dna.get_mesh_count()): names.append(self.dna.get_mesh_name(index)) return names def get_lod_indices_names(self) -> List[List[int]]: """Reads in the meshes of the definition""" lod_indices: List[List[int]] = [] for index in range(self.dna.get_lod_count()): lod_indices.append(self.dna.get_mesh_indices_for_lod(index)) return lod_indices def create_mesh_selector(self) -> MeshTreeList: """ Creates and adds a mesh tree list where the entries are grouped by lods, this is used for selecting the meses that need to be processed @rtype: MeshTreeList @returns: The created mesh tree list widget """ widget = MeshTreeList(self) self.body.addWidget(widget) return widget def create_file_chooser( self, label: str, hint: str, caption: str, filter: str, on_changed: Callable[[int], None] = None, ) -> FileChooser: """ Creates a file chooser widget that is used for selecting file paths @type label: str @param label: The label in the FileDialog that pops up @type hint: str @param hint: The label in the FileDialog that pops up @type caption: str @param caption: The caption in the FileDialog that pops up @type filter: str @param filter: The file filter that is used in the FileDialog @rtype: FileChooser @returns: The created file chooser object """ widget = FileChooser( label, hint, self, dialog_caption=caption, dialog_filter=filter, on_changed=on_changed or self.on_generic_changed, ) self.body.addWidget(widget) return widget def create_gui_selector(self) -> FileChooser: """ Creates the gui selector widget @rtype: FileChooser @returns: Gui selector widget """ return self.create_file_chooser( "Gui path:", "GUI file to load. Required by RigLogic", "Select the gui file", "gui files (*.ma)", ) def create_aas_selector(self) -> FileChooser: """ Creates and adds the additional assemble script selector widget @rtype: FileChooser @returns: Additional assemble script selector widget """ return self.create_file_chooser( "Additional assemble script path:", "Additional assemble script to use. Required by RigLogic", "Select the aas file", "python script (*.py)", ) def create_analog_gui_selector(self) -> FileChooser: """ Creates and adds the analog gui selector widget @rtype: FileChooser @returns: Analog gui selector widget """ return self.create_file_chooser( "Analog gui path:", "Analog GUI file to load. Required by RigLogic", "Select the analog gui file", "analog gui files (*.ma)", ) def create_build_options(self) -> QWidget: """Creates and adds the widget containing the build options checkboxes""" widget = QWidget() layout = QVBoxLayout(widget) layout.setContentsMargins( MARGIN_BODY_LEFT, MARGIN_BODY_TOP, MARGIN_BODY_RIGHT, MARGIN_BOTTOM, ) self.joints_cb = self.create_checkbox( "joints", "Add joints to rig", layout, self.on_joints_changed, enabled=True, ) self.blend_shapes_cb = self.create_checkbox( "blend shapes", "Add blend shapes to rig", layout, self.on_generic_changed, enabled=True, ) self.skin_cb = self.create_checkbox( "skin cluster", "Add skin cluster to rig", layout, self.on_generic_changed, ) self.rig_logic_cb = self.create_checkbox( "rig logic", "Add rig logic to rig", layout, self.on_generic_changed, ) layout.addStretch() return widget def create_extra_build_options(self) -> QWidget: """Creates and adds the widget containing the extra build options checkboxes""" widget = QWidget() layout = QVBoxLayout(widget) layout.setContentsMargins( MARGIN_BODY_LEFT, MARGIN_BODY_TOP, MARGIN_BODY_RIGHT, MARGIN_BOTTOM, ) self.ctrl_attrs_cb = self.create_checkbox( "ctrl attributes on root joint", "ctrl attributes on root joint", layout, enabled=True, checked=True, ) self.anim_attrs_cb = self.create_checkbox( "animated map attributes on root joint", "animated map attributes on root joint", layout, enabled=True, checked=True, ) self.mesh_name_cb = self.create_checkbox( "mesh name to blend shape channel name", "mesh name to blend shape channel name", layout, enabled=True, checked=True, ) self.key_frame_cb = self.create_checkbox( "key frames", "Add keyframes to rig", layout, enabled=True, checked=True, ) layout.addStretch() return widget def enable_additional_build_options(self, enable: bool) -> None: self.ctrl_attrs_cb.setEnabled(enable) self.anim_attrs_cb.setEnabled(enable) self.mesh_name_cb.setEnabled(enable) self.key_frame_cb.setEnabled(enable) def create_checkbox( self, label: str, hint: str, layout: QHBoxLayout, on_changed: Callable[[int], None] = None, checked: bool = False, enabled: bool = False, ) -> QCheckBox: """ Adds a checkbox with given parameters and connects them to the on_changed method @type label: str @param label: The label of the checkbox @type hint: str @param hint: The hint of the checkbox @type on_changed: Callable[[int], None] @param on_changed: The method that will get called when the checkbox changes states @type checked: bool @param checked: The value representing if the checkbox is checked after creation @type enabled: bool @param enabled: The value representing if the checkbox is enabled after creation @rtype: QCheckBox @returns: the created checkbox object """ checkbox = QCheckBox(label, self) checkbox.setChecked(checked) checkbox.setEnabled(enabled) checkbox.setToolTip(hint) if on_changed: checkbox.stateChanged.connect(on_changed) layout.addWidget(checkbox) return checkbox def on_joints_changed(self, state: int) -> None: """ Method that gets called when the joints checkbox is changed @type state: int @param state: The changed state of the checkbox """ if self.joints_cb.isChecked(): self.process_btn.setEnabled(True) if self.mesh_tree_list.get_selected_meshes(): self.skin_cb.setEnabled(True) else: self.skin_cb.setEnabled(False) if not self.mesh_tree_list.get_selected_meshes(): self.process_btn.setEnabled(False) self.on_generic_changed(state) def create_process_btn(self) -> QPushButton: """ Creates and adds a process button @type window: QMainWindow @param window: The instance of the window object @rtype: QPushButton @returns: The created process button """ btn = QPushButton("Process") btn.setEnabled(False) btn.clicked.connect(self.process) self.body.addWidget(btn) return btn def create_progress_bar(self) -> QProgressBar: """ Creates and adds progress bar @type window: QMainWindow @param window: The instance of the window object @rtype: QProgressBar @returns: The created progress bar """ progress = QProgressBar(self) progress.setRange(0, 100) progress.setValue(0) progress.setTextVisible(True) progress.setFormat("") self.body.addWidget(progress) return progress def on_generic_changed(self, state: int) -> None: # pylint: disable=unused-argument """ Method that gets called when the checkbox is changed @type state: int @param state: The changed state of the checkbox """ self.set_riglogic_cb_enabled() def is_enabled_and_checked(self, check_box: QCheckBox) -> bool: """ Method that checks if check box is enabled in same time @type check_box: QCheckBox @param check_box: The checkbox instance to check """ return ( check_box is not None and bool(check_box.isEnabled()) and bool(check_box.isChecked()) ) def set_riglogic_cb_enabled(self) -> None: """Method that sets enable state of riglogic check box""" all_total_meshes = False if self.dna and self.is_enabled_and_checked(self.blend_shapes_cb): if ( len(self.mesh_tree_list.get_selected_meshes()) == self.dna.get_mesh_count() ): all_total_meshes = True enabled = ( self.is_enabled_and_checked(self.joints_cb) and self.is_enabled_and_checked(self.blend_shapes_cb) and all_total_meshes and self.is_enabled_and_checked(self.skin_cb) and self.select_gui_path.get_file_path() is not None and self.select_analog_gui_path.get_file_path() is not None and self.select_aas_path.get_file_path() is not None ) self.rig_logic_cb.setEnabled(enabled) def create_file_inputs(self) -> None: """Creates the file input widgets but keeps them hidden""" # GUI Path - 隐藏但保持功能 self.select_gui_path = self.create_file_chooser( "GUI Path:", "GUI file to load", "Select a GUI file", "Maya ASCII (*.ma)", self.on_generic_changed, ) self.select_gui_path.hide() # 隐藏控件 self.select_gui_path.fc_text_field.setText(self.default_paths['gui_path']) # Analog GUI Path - 隐藏但保持功能 self.select_analog_gui_path = self.create_file_chooser( "Analog GUI Path:", "Analog GUI file to load", "Select an analog GUI file", "Maya ASCII (*.ma)", self.on_generic_changed, ) self.select_analog_gui_path.hide() # 隐藏控件 self.select_analog_gui_path.fc_text_field.setText(self.default_paths['analog_gui_path']) # Additional Script Path - 隐藏但保持功能 self.select_aas_path = self.create_file_chooser( "Additional Script Path:", "Additional assembly script to load", "Select a Python file", "Python files (*.py)", self.on_generic_changed, ) self.select_aas_path.hide() # 隐藏控件 self.select_aas_path.fc_text_field.setText(self.default_paths['additional_script_path'])