MetaFusion/dna_viewer/ui/app.py
2025-01-06 23:33:41 +08:00

1071 lines
34 KiB
Python

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
from PySide2.QtWidgets import (
QApplication,
QCheckBox,
QGridLayout,
QHBoxLayout,
QLabel,
QMainWindow,
QMessageBox,
QProgressBar,
QPushButton,
QTabWidget,
QTreeWidget,
QTreeWidgetItem,
QTreeWidgetItemIterator,
QVBoxLayout,
QWidget,
)
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 MeshTreeList(QWidget):
"""
A custom widget that lists out meshes with checkboxes next to them, so these meshes can be selected to be processed. The meshes are grouped by LOD
@type mesh_tree: QWidget
@param mesh_tree: The widget that contains the meshes to be selected in a tree list
"""
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,
)
self.btn_select_all = QPushButton("Select all meshes")
self.btn_select_all.setEnabled(False)
self.btn_select_all.clicked.connect(self.select_all)
layout_holder.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)
layout_holder.addWidget(self.btn_deselect_all)
self.setLayout(layout_holder)
def create_mesh_tree(self) -> QWidget:
"""
Creates the mesh tree list widget
@rtype: QWidget
@returns: The created widget
"""
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: QWidget = None) -> None:
super().__init__(parent)
self.body: QVBoxLayout = None
self.header: QHBoxLayout = None
self.build_options: QWidget = None
self.extra_build_options: QWidget = None
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:
"""Start the build process of creation of scene from provided configuration from the UI"""
process = True
if cmds.file(q=True, modified=True):
process = self.show_message_dialog()
if process:
self.set_progress(text="Processing in progress...", value=0)
config = RigConfig(
meshes=self.mesh_tree_list.get_selected_meshes(),
gui_path=self.select_gui_path.get_file_path(),
analog_gui_path=self.select_analog_gui_path.get_file_path(),
aas_path=self.select_aas_path.get_file_path(),
add_rig_logic=self.add_rig_logic(),
add_joints=self.add_joints(),
add_blend_shapes=self.add_blend_shapes(),
add_skin_cluster=self.add_skin_cluster(),
add_ctrl_attributes_on_root_joint=self.add_ctrl_attributes_on_root_joint(),
add_animated_map_attributes_on_root_joint=self.add_animated_map_attributes_on_root_joint(),
add_mesh_name_to_blend_shape_channel_name=self.add_mesh_name_to_blend_shape_channel_name(),
add_key_frames=self.add_key_frames(),
)
self.main_widget.setEnabled(False)
try:
self.set_progress(value=33)
self.dna = DNA(self.select_dna_path.get_file_path())
self.set_progress(value=66)
build_rig(dna=self.dna, config=config)
self.set_progress(text="Processing completed", value=100)
except Exception as e:
self.set_progress(text="Processing failed", value=100)
logging.error(e)
confirmDialog(message=e, button=["ok"], icon="critical")
self.main_widget.setEnabled(True)
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_attributes_on_root_joint_cb)
def add_animated_map_attributes_on_root_joint(self) -> bool:
return self.is_checked(self.animated_map_attributes_on_root_joint_cb)
def add_mesh_name_to_blend_shape_channel_name(self) -> bool:
return self.is_checked(self.mesh_name_to_blend_shape_channel_name_cb)
def add_key_frames(self) -> bool:
return self.is_checked(self.key_frames_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_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.select_gui_path = self.create_gui_selector()
self.select_analog_gui_path = self.create_analog_gui_selector()
self.select_aas_path = self.create_aas_selector()
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:
"""
Creates and adds the DNA selector widget
@rtype: QWidget
@returns: The created DNA selector widget
"""
widget = QWidget()
self.select_dna_path = self.create_dna_chooser()
self.load_dna_btn = self.create_load_dna_button(self.select_dna_path)
self.select_dna_path.fc_text_field.textChanged.connect(
lambda: self.on_dna_selected(self.select_dna_path)
)
layout = QVBoxLayout()
layout.addWidget(self.select_dna_path)
layout.addWidget(self.load_dna_btn)
layout.setContentsMargins(
MARGIN_HEADER_LEFT,
MARGIN_HEADER_TOP,
MARGIN_HEADER_RIGHT,
MARGIN_HEADER_BOTTOM,
)
widget.setLayout(layout)
self.body.addWidget(widget)
return widget
def on_dna_selected(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
"""
enabled = input.get_file_path() is not None
self.load_dna_btn.setEnabled(enabled)
self.process_btn.setEnabled(False)
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. Requires: DNA to be loaded",
layout,
self.on_joints_changed,
)
self.blend_shapes_cb = self.create_checkbox(
"blend shapes",
"Add blend shapes to rig. Requires: DNA to be loaded and at least one mesh to be check",
layout,
self.on_generic_changed,
)
self.skin_cb = self.create_checkbox(
"skin cluster",
"Add skin cluster to rig. Requires: DNA to be loaded and at least one mesh and joints to be checked",
layout,
self.on_generic_changed,
)
self.rig_logic_cb = self.create_checkbox(
"rig logic",
"Add RigLogic to rig. Requires: DNA to be loaded, all meshes to be checked, joints, skin, blend shapes to be checked, also gui, analog gui and additional assemble script must be set",
layout,
)
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_attributes_on_root_joint_cb = self.create_checkbox(
"ctrl attributes on root joint",
"ctrl attributes on root joint",
layout,
enabled=True,
checked=True,
)
self.animated_map_attributes_on_root_joint_cb = self.create_checkbox(
"animated map attributes on root joint",
"animated map attributes on root joint",
layout,
enabled=True,
checked=True,
)
self.mesh_name_to_blend_shape_channel_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_frames_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_attributes_on_root_joint_cb.setEnabled(enable)
self.animated_map_attributes_on_root_joint_cb.setEnabled(enable)
self.mesh_name_to_blend_shape_channel_name_cb.setEnabled(enable)
self.key_frames_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)