Created
May 2, 2025 17:40
-
-
Save Pakmanv/5a4663f3209db206a9074ec9cfab83a4 to your computer and use it in GitHub Desktop.
Massive Reference VV 1.19
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # -*- coding: utf-8 -*- | |
| import os | |
| import glob | |
| import json | |
| import getpass | |
| import fnmatch | |
| import re | |
| import subprocess | |
| from PySide2 import QtWidgets, QtCore, QtGui | |
| import maya.cmds as cmds | |
| import maya.mel as mel | |
| import time | |
| import sys | |
| # Script version | |
| SCRIPT_VERSION = "1.19" | |
| class MassiveReferenceVV(QtWidgets.QDialog): | |
| def __init__(self, project_dir): | |
| super(MassiveReferenceVV, self).__init__() | |
| self.project_dir = project_dir | |
| self.asset_cache = self.cache_assets() | |
| self.ref_items = {} | |
| self.show_full_path = False | |
| self.show_namespace = False | |
| self.scene_sort_mode = "asset" # Sort mode for Current Scene References | |
| self.ref_sort_mode = "asset" # Sort mode for Assets to Reference | |
| self.ref_timestamps = {} # To track addition order | |
| self.group_colors = [ | |
| "#FF9999", # Pastel red | |
| "#CCFFCC", # Pastel green | |
| "#ADD8E6", # Pastel blue | |
| "#D9B3FF", # Pastel purple | |
| "#FFFF99", # Pastel yellow | |
| "#FFB3E6", # Pastel pink | |
| ] | |
| self.scene_ref_list_height = 200 | |
| self.ref_list_height = 200 | |
| self.browser_list_height = 200 | |
| self.project_history = self.load_project_history() | |
| self.load_window_state() | |
| self.setup_ui() | |
| self.initialize_ref_list() | |
| def clean_file_path(self, file_path): | |
| """Remove {N} suffix from file path.""" | |
| return re.sub(r'\{[0-9]+\}', '', file_path) | |
| def create_colored_icon(self, color): | |
| """Create a QIcon with a solid colored square.""" | |
| pixmap = QtGui.QPixmap(16, 16) | |
| pixmap.fill(QtGui.QColor(color)) | |
| return QtGui.QIcon(pixmap) | |
| def setup_ui(self): | |
| self.setWindowTitle("Massive Reference VV 1.19") | |
| self.resize(self.window_width, self.window_height) | |
| layout = QtWidgets.QVBoxLayout() | |
| menu_bar = QtWidgets.QMenuBar(self) | |
| menu_bar.setContextMenuPolicy(QtCore.Qt.NoContextMenu) # Disable default context menu | |
| tools_menu = menu_bar.addMenu("Tools") | |
| tools_menu.setContextMenuPolicy(QtCore.Qt.NoContextMenu) # Disable for Tools menu | |
| ref_editor_action = QtWidgets.QAction("Open Reference Editor", self) | |
| ref_editor_action.triggered.connect(self.open_reference_editor) | |
| namespace_editor_action = QtWidgets.QAction("Open Namespace Editor", self) | |
| namespace_editor_action.triggered.connect(self.open_namespace_editor) | |
| refresh_ui_action = QtWidgets.QAction("Refresh UI", self) | |
| refresh_ui_action.triggered.connect(self.refresh_ui) | |
| save_state_action = QtWidgets.QAction("Save Window State", self) | |
| save_state_action.triggered.connect(self.save_window_state) | |
| tools_menu.addAction(ref_editor_action) | |
| tools_menu.addAction(namespace_editor_action) | |
| tools_menu.addAction(refresh_ui_action) | |
| tools_menu.addAction(save_state_action) | |
| layout.addWidget(menu_bar) | |
| # Current Scene References header | |
| scene_ref_header_layout = QtWidgets.QHBoxLayout() | |
| scene_ref_header_layout.addWidget(QtWidgets.QLabel("Current Scene References:")) | |
| scene_ref_header_layout.addStretch() | |
| sort_label = QtWidgets.QLabel("Sort by:") | |
| self.scene_sort_dropdown = QtWidgets.QComboBox() | |
| self.scene_sort_dropdown.addItems(["Alphabetical A-Z", "Alphabetical Z-A", "Version", "Asset", "Time"]) | |
| self.scene_sort_dropdown.setCurrentText(self.scene_sort_dropdown_text) | |
| self.scene_sort_dropdown.currentTextChanged.connect(self.on_scene_sort_changed) | |
| self.scene_sort_dropdown.setMinimumWidth(150) | |
| scene_ref_header_layout.addWidget(sort_label) | |
| scene_ref_header_layout.addWidget(self.scene_sort_dropdown) | |
| self.scene_ref_count_label = QtWidgets.QLabel("Reference(s) in Scene: 0") | |
| self.scene_ref_count_label.setStyleSheet("font-size: 10px; color: gray;") | |
| scene_ref_header_layout.addWidget(self.scene_ref_count_label) | |
| layout.addLayout(scene_ref_header_layout) | |
| self.scene_ref_list = QtWidgets.QListWidget() | |
| self.scene_ref_list.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) | |
| self.scene_ref_list.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) | |
| self.scene_ref_list.customContextMenuRequested.connect(self.show_scene_ref_context_menu) | |
| self.scene_ref_list.itemSelectionChanged.connect(self.select_viewport_objects) | |
| self.scene_ref_list.setFixedHeight(self.scene_ref_list_height) | |
| layout.addWidget(self.scene_ref_list) | |
| checkbox_layout = QtWidgets.QHBoxLayout() | |
| self.namespace_checkbox = QtWidgets.QCheckBox("Show Namespace") | |
| self.namespace_checkbox.setChecked(self.show_namespace) | |
| self.namespace_checkbox.stateChanged.connect(self.toggle_namespace_display) | |
| checkbox_layout.addWidget(self.namespace_checkbox) | |
| self.append_preset_to_scene_checkbox = QtWidgets.QCheckBox("Append Preset to Scene") | |
| self.append_preset_to_scene_checkbox.setChecked(self.append_preset_to_scene) | |
| self.append_preset_to_scene_checkbox.stateChanged.connect(self.save_window_state) | |
| checkbox_layout.addWidget(self.append_preset_to_scene_checkbox) | |
| self.scene_selection_checkbox = QtWidgets.QCheckBox("Auto-Select in Viewport") | |
| self.scene_selection_checkbox.setChecked(self.scene_selection_enabled) | |
| self.scene_selection_checkbox.stateChanged.connect(self.save_window_state) | |
| checkbox_layout.addWidget(self.scene_selection_checkbox) | |
| checkbox_layout.addStretch() | |
| layout.addLayout(checkbox_layout) | |
| note_label = QtWidgets.QLabel("Right-click to swap/import/duplicate/load/unload/remove selected references, open source/location, or resize list") | |
| note_label.setStyleSheet("font-size: 10px; color: gray;") | |
| layout.addWidget(note_label) | |
| self.update_scene_ref_list() | |
| preset_layout = QtWidgets.QHBoxLayout() | |
| self.save_btn = QtWidgets.QPushButton("Save Preset") | |
| self.save_btn.clicked.connect(self.save_preset) | |
| self.load_btn = QtWidgets.QPushButton("Load Preset") | |
| self.load_btn.clicked.connect(self.load_preset) | |
| self.append_btn = QtWidgets.QPushButton("Append Preset") | |
| self.append_btn.clicked.connect(self.append_preset) | |
| frost_blue = "QPushButton { background-color: #B3D9FF; color: black; }" | |
| darker_blue = "QPushButton { background-color: #99C2FF; color: black; }" | |
| self.save_btn.setStyleSheet(frost_blue) | |
| self.load_btn.setStyleSheet(frost_blue) | |
| self.append_btn.setStyleSheet(darker_blue) | |
| preset_layout.addWidget(self.save_btn) | |
| preset_layout.addWidget(self.load_btn) | |
| preset_layout.addWidget(self.append_btn) | |
| layout.addLayout(preset_layout) | |
| # Assets to Reference header | |
| ref_header_layout = QtWidgets.QHBoxLayout() | |
| ref_header_layout.addWidget(QtWidgets.QLabel("Assets to Reference:")) | |
| self.ref_count_label = QtWidgets.QLabel("Assets Registered: 0") | |
| self.ref_count_label.setStyleSheet("font-size: 10px; color: gray;") | |
| ref_header_layout.addStretch() | |
| ref_sort_label = QtWidgets.QLabel("Sort by:") | |
| self.ref_sort_dropdown = QtWidgets.QComboBox() | |
| self.ref_sort_dropdown.addItems(["Alphabetical A-Z", "Alphabetical Z-A", "Version", "Asset", "Time"]) | |
| self.ref_sort_dropdown.setCurrentText(self.ref_sort_dropdown_text) | |
| self.ref_sort_dropdown.currentTextChanged.connect(self.on_ref_sort_changed) | |
| self.ref_sort_dropdown.setMinimumWidth(150) | |
| ref_header_layout.addWidget(ref_sort_label) | |
| ref_header_layout.addWidget(self.ref_sort_dropdown) | |
| ref_header_layout.addWidget(self.ref_count_label) | |
| layout.addLayout(ref_header_layout) | |
| self.ref_list = QtWidgets.QListWidget() | |
| self.ref_list.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) | |
| self.ref_list.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) | |
| self.ref_list.customContextMenuRequested.connect(self.show_ref_list_context_menu) | |
| self.ref_list.setFixedHeight(self.ref_list_height) | |
| self.ref_list.itemSelectionChanged.connect(self.update_remove_button_state) | |
| layout.addWidget(self.ref_list) | |
| self.clear_ref_list_checkbox = QtWidgets.QCheckBox("Clear Reference List After Referencing") | |
| self.clear_ref_list_checkbox.setChecked(self.clear_ref_list) | |
| layout.addWidget(self.clear_ref_list_checkbox) | |
| ref_action_layout = QtWidgets.QHBoxLayout() | |
| self.add_to_scene_btn = QtWidgets.QPushButton("Add to Scene (Right click to overwrite)") | |
| self.add_to_scene_btn.setStyleSheet("QPushButton { background-color: #FFCC99; color: black; }") | |
| self.add_to_scene_btn.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) | |
| self.add_to_scene_btn.clicked.connect(self.add_to_scene) | |
| self.add_to_scene_btn.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) | |
| self.add_to_scene_btn.customContextMenuRequested.connect(self.show_add_to_scene_context_menu) | |
| ref_action_layout.addWidget(self.add_to_scene_btn) | |
| self.remove_from_ref_btn = QtWidgets.QPushButton("Remove Selection (Right click to clear all)") | |
| self.remove_from_ref_btn.setStyleSheet("QPushButton { background-color: #FF9999; color: black; }") | |
| self.remove_from_ref_btn.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) | |
| self.remove_from_ref_btn.clicked.connect(self.remove_from_ref_list) | |
| self.remove_from_ref_btn.setEnabled(True) | |
| self.remove_from_ref_btn.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) | |
| self.remove_from_ref_btn.customContextMenuRequested.connect(self.show_remove_from_ref_context_menu) | |
| ref_action_layout.addWidget(self.remove_from_ref_btn) | |
| layout.addLayout(ref_action_layout) | |
| dir_layout = QtWidgets.QHBoxLayout() | |
| dir_layout.addWidget(QtWidgets.QLabel("Project Directory:")) | |
| self.dir_field = QtWidgets.QLineEdit() | |
| self.dir_field.setReadOnly(True) | |
| self.dir_field.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) | |
| self.dir_field.customContextMenuRequested.connect(self.show_dir_field_context_menu) | |
| self.update_dir_field() | |
| dir_layout.addWidget(self.dir_field) | |
| self.dir_btn = QtWidgets.QPushButton("Change") | |
| self.dir_btn.clicked.connect(self.change_project_dir) | |
| dir_layout.addWidget(self.dir_btn) | |
| layout.addLayout(dir_layout) | |
| browser_header_layout = QtWidgets.QHBoxLayout() | |
| browser_header_layout.addWidget(QtWidgets.QLabel("Asset Browser:")) | |
| self.browser_count_label = QtWidgets.QLabel("Assets Found: 0") | |
| self.browser_count_label.setStyleSheet("font-size: 10px; color: gray;") | |
| browser_header_layout.addStretch() | |
| browser_header_layout.addWidget(self.browser_count_label) | |
| layout.addLayout(browser_header_layout) | |
| self.search_bar = QtWidgets.QLineEdit() | |
| self.search_bar.setPlaceholderText("e.g., sp v003, tv, *doom*") | |
| self.search_bar.textChanged.connect(self.update_browser) | |
| layout.addWidget(self.search_bar) | |
| self.path_checkbox = QtWidgets.QCheckBox("Show Full Path") | |
| self.path_checkbox.setChecked(self.show_full_path) | |
| self.path_checkbox.stateChanged.connect(self.toggle_path_display) | |
| layout.addWidget(self.path_checkbox) | |
| self.browser_list = QtWidgets.QListWidget() | |
| self.browser_list.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) | |
| self.browser_list.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) | |
| self.browser_list.customContextMenuRequested.connect(self.show_browser_context_menu) | |
| self.browser_list.setFixedHeight(self.browser_list_height) | |
| layout.addWidget(self.browser_list) | |
| btn_layout = QtWidgets.QHBoxLayout() | |
| self.add_btn = QtWidgets.QPushButton("Add to Reference List") | |
| self.add_btn.setStyleSheet("QPushButton { background-color: #CCFFCC; color: black; }") | |
| self.add_btn.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) | |
| self.add_btn.clicked.connect(self.add_to_ref_list) | |
| btn_layout.addWidget(self.add_btn) | |
| layout.addLayout(btn_layout) | |
| self.status_bar = QtWidgets.QLabel("Ready") | |
| layout.addWidget(self.status_bar) | |
| disclaimer = QtWidgets.QLabel("Tool made by Vypac Voeur and should not be distributed.") | |
| disclaimer.setStyleSheet("font-size: 10px; color: gray;") | |
| layout.addWidget(disclaimer) | |
| self.setLayout(layout) | |
| self.update_browser() | |
| def refresh_ui(self): | |
| """Refresh all UI lists to reflect current state.""" | |
| self.update_scene_ref_list() | |
| self.update_ref_list_display() | |
| self.update_browser() | |
| self.status_bar.setText("UI refreshed.") | |
| def show_remove_from_ref_context_menu(self, pos): | |
| menu = QtWidgets.QMenu(self) | |
| clear_all_action = menu.addAction("Clear All Assets") | |
| action = menu.exec_(self.remove_from_ref_btn.mapToGlobal(pos)) | |
| if action == clear_all_action: | |
| self.clear_all_ref_list() | |
| def clear_all_ref_list(self): | |
| if not self.ref_items: | |
| self.status_bar.setText("Reference list is already empty.") | |
| return | |
| self.ref_list.clearSelection() | |
| self.ref_list.clear() | |
| self.ref_items.clear() | |
| self.update_ref_list_display() | |
| self.status_bar.setText("Cleared all assets from reference list.") | |
| def show_add_to_scene_context_menu(self, pos): | |
| self.ref_list.clearSelection() | |
| self.add_to_scene_btn.setFocus() | |
| menu = QtWidgets.QMenu(self) | |
| overwrite_action = menu.addAction("Overwrite Scene") | |
| overwrite_action.setIcon(self.create_colored_icon("#FF0000")) # Bright red icon | |
| action = menu.exec_(self.add_to_scene_btn.mapToGlobal(pos)) | |
| if action == overwrite_action: | |
| self.overwrite_scene() | |
| def update_dir_field(self): | |
| self.dir_field.setText("Please select project directory" if not self.asset_cache else self.project_dir) | |
| def update_ref_list_display(self): | |
| self.ref_list.clear() | |
| ref_items = [] | |
| for file_path in self.ref_items.keys(): | |
| file_name = os.path.basename(file_path) | |
| name_match = re.match(r'^(.*?)(?:_v\d+)?\.(ma|mb|fbx)$', file_name) | |
| name = name_match.group(1) if name_match else file_name | |
| version_match = re.search(r'_v(\d+)', file_name) | |
| version = version_match.group(1) if version_match else "0" | |
| asset = file_name | |
| timestamp = self.ref_timestamps.get(file_path, time.time()) | |
| display_text = file_path if self.show_full_path else file_name | |
| ref_items.append({ | |
| "text": display_text, | |
| "file_path": file_path, | |
| "name": name, | |
| "sort_name": file_name if not self.show_full_path else file_path, | |
| "version": version, | |
| "asset": asset, | |
| "timestamp": timestamp | |
| }) | |
| # Sort ref_items based on ref_sort_mode | |
| if self.ref_sort_mode == "alphabetical_asc": | |
| ref_items.sort(key=lambda x: (x["sort_name"].lower(), x["version"], x["asset"])) | |
| elif self.ref_sort_mode == "alphabetical_desc": | |
| ref_items.sort(key=lambda x: (x["sort_name"].lower(), x["version"], x["asset"]), reverse=True) | |
| elif self.ref_sort_mode == "version": | |
| ref_items.sort(key=lambda x: (x["version"], x["name"], x["asset"])) | |
| elif self.ref_sort_mode == "time": | |
| ref_items.sort(key=lambda x: x["timestamp"]) | |
| else: # "asset" | |
| ref_items.sort(key=lambda x: (x["asset"], x["name"], x["version"])) | |
| for item_data in ref_items: | |
| list_item = QtWidgets.QListWidgetItem(item_data["text"]) | |
| list_item.setForeground(QtGui.QColor("white")) | |
| self.ref_list.addItem(list_item) | |
| self.ref_items[item_data["file_path"]]["item"] = list_item | |
| self.ref_count_label.setText(f"Assets Registered: {len(self.ref_items)}") | |
| self.update_remove_button_state() | |
| def on_ref_sort_changed(self, text): | |
| sort_mapping = { | |
| "Alphabetical A-Z": "alphabetical_asc", | |
| "Alphabetical Z-A": "alphabetical_desc", | |
| "Version": "version", | |
| "Asset": "asset", | |
| "Time": "time" | |
| } | |
| self.ref_sort_mode = sort_mapping.get(text, "asset") | |
| self.update_ref_list_display() | |
| self.save_window_state() | |
| def remove_from_ref_list(self): | |
| selected = self.ref_list.selectedItems() | |
| if not selected: | |
| self.status_bar.setText("Please make at least one selection.") | |
| return | |
| for item in selected: | |
| file_path = next((path for path, info in self.ref_items.items() if info["item"] == item), None) | |
| if file_path: | |
| self.ref_list.takeItem(self.ref_list.row(item)) | |
| del self.ref_items[file_path] | |
| if file_path in self.ref_timestamps: | |
| del self.ref_timestamps[file_path] | |
| self.update_ref_list_display() | |
| self.status_bar.setText(f"Removed {len(selected)} assets from reference list.") | |
| def add_to_ref_list(self): | |
| selected = self.browser_list.selectedItems() | |
| if not selected: | |
| self.status_bar.setText("No items selected to add.") | |
| return | |
| added_count = 0 | |
| for item in selected: | |
| file_path = next((asset for asset in self.asset_cache if os.path.basename(asset) == item.text() or asset == item.text()), None) | |
| if file_path and file_path not in self.ref_items: | |
| display_text = file_path if self.show_full_path else os.path.basename(file_path) | |
| list_item = QtWidgets.QListWidgetItem(display_text) | |
| list_item.setForeground(QtGui.QColor("white")) | |
| self.ref_list.addItem(list_item) | |
| self.ref_items[file_path] = {"item": list_item} | |
| self.ref_timestamps[file_path] = time.time() | |
| added_count += 1 | |
| self.update_ref_list_display() | |
| self.status_bar.setText(f"Added {added_count} assets to reference list.") | |
| def initialize_ref_list(self): | |
| # Initialize Assets to Reference list as empty | |
| self.ref_items.clear() | |
| self.ref_list.clear() | |
| self.ref_timestamps.clear() | |
| self.ref_count_label.setText("Assets Registered: 0") | |
| def update_browser(self): | |
| self.browser_list.clear() | |
| search_input = self.search_bar.text().strip() | |
| displayed_assets = [] | |
| if not search_input: | |
| displayed_assets = self.asset_cache | |
| for asset in displayed_assets: | |
| display_text = asset if self.show_full_path else os.path.basename(asset) | |
| self.browser_list.addItem(display_text) | |
| self.status_bar.setText(f"Showing all {len(self.asset_cache)} assets.") | |
| else: | |
| search_input = search_input.replace(",", " ") | |
| tokens = [token.strip() for token in search_input.split() if token.strip()] | |
| matches = 0 | |
| for asset in self.asset_cache: | |
| asset_name = os.path.basename(asset).lower() | |
| if all(fnmatch.fnmatch(asset_name, f"*{token}*") for token in tokens): | |
| display_text = asset if self.show_full_path else os.path.basename(asset) | |
| self.browser_list.addItem(display_text) | |
| displayed_assets.append(asset) | |
| matches += 1 | |
| self.status_bar.setText(f"Found {matches} matches for '{search_input}'.") | |
| self.update_dir_field() | |
| self.browser_count_label.setText(f"Assets Found: {len(displayed_assets)}") | |
| def toggle_path_display(self): | |
| self.show_full_path = self.path_checkbox.isChecked() | |
| self.update_browser() | |
| self.update_ref_list_display() | |
| self.update_scene_ref_list() | |
| def toggle_namespace_display(self): | |
| self.show_namespace = self.namespace_checkbox.isChecked() | |
| self.update_scene_ref_list() | |
| def select_viewport_objects(self): | |
| """Select objects in the viewport corresponding to selected references.""" | |
| if not self.scene_selection_checkbox.isChecked(): | |
| return # Skip selection if checkbox is unchecked | |
| selected_items = self.scene_ref_list.selectedItems() | |
| objects_to_select = [] | |
| for item in selected_items: | |
| file_path = item.data(QtCore.Qt.UserRole) | |
| try: | |
| ref_node = cmds.referenceQuery(file_path, referenceNode=True) | |
| namespace = cmds.referenceQuery(ref_node, namespace=True).lstrip(":") | |
| # Get top-level nodes under the namespace | |
| nodes = cmds.ls(f"{namespace}:*", assemblies=True, long=True) or [] | |
| objects_to_select.extend(nodes) | |
| except RuntimeError as e: | |
| self.status_bar.setText(f"Error selecting objects for {os.path.basename(file_path)}: {str(e)}") | |
| if objects_to_select: | |
| cmds.select(objects_to_select, replace=True) | |
| self.status_bar.setText(f"Selected {len(objects_to_select)} object(s) in viewport") | |
| else: | |
| cmds.select(clear=True) | |
| self.status_bar.setText("No objects found to select in viewport") | |
| def update_scene_ref_list(self): | |
| self.scene_ref_list.clear() | |
| current_refs = cmds.file(query=True, reference=True) or [] | |
| ref_groups = {} | |
| for ref in current_refs: | |
| file_name = os.path.basename(ref) | |
| group_name = re.match(r'^([a-zA-Z_]+)', file_name.split('_v')[0]).group(1) if '_v' in file_name else re.match(r'^([a-zA-Z_]+)', file_name).group(1) | |
| ref_groups[ref] = group_name | |
| unique_groups = list(set(ref_groups.values())) | |
| group_color_map = {group: self.group_colors[i % len(self.group_colors)] for i, group in enumerate(unique_groups)} | |
| ref_items = [] | |
| for ref in current_refs: | |
| try: | |
| ref_node = cmds.referenceQuery(ref, referenceNode=True) | |
| namespace = cmds.referenceQuery(ref_node, namespace=True).lstrip(":") | |
| # Clean only {N} suffixes, preserving _N suffixes | |
| clean_namespace = re.sub(r'\{[0-9]+\}', '', namespace) | |
| is_loaded = cmds.referenceQuery(ref_node, isLoaded=True) | |
| # Debug: Print namespace and file details | |
| print(f"Ref: {ref}") | |
| print(f"Raw namespace: {namespace}") | |
| print(f"Clean namespace: {clean_namespace}") | |
| print(f"File name: {os.path.basename(ref)}") | |
| # Debug: Verify file_path | |
| clean_ref = self.clean_file_path(ref) | |
| if not os.path.exists(clean_ref): | |
| print(f"Warning: Reference path not found: {clean_ref}") | |
| except RuntimeError as e: | |
| self.status_bar.setText(f"Error querying {os.path.basename(ref)}: {str(e)}") | |
| continue | |
| # Set display text: mimic Reference Editor | |
| if self.show_namespace: | |
| base_text = f"{clean_namespace}RN {os.path.basename(ref)}" | |
| else: | |
| base_text = ref if self.show_full_path else os.path.basename(ref) | |
| display_text = base_text if is_loaded else f"[UR] {base_text}" | |
| group_name = ref_groups[ref] | |
| file_name = os.path.basename(ref) | |
| name_match = re.match(r'^(.*?)(?:_v\d+)?\.(ma|mb|fbx)$', file_name) | |
| name = name_match.group(1) if name_match else file_name | |
| version_match = re.search(r'_v(\d+)', file_name) | |
| version = version_match.group(1) if version_match else "0" | |
| asset = file_name | |
| timestamp = self.ref_timestamps.get(ref, time.time()) | |
| ref_items.append({ | |
| "text": display_text, | |
| "ref": ref, | |
| "is_loaded": is_loaded, | |
| "group_name": group_name, | |
| "name": name, | |
| "sort_name": os.path.basename(ref) if not self.show_full_path else ref, | |
| "version": version, | |
| "asset": asset, | |
| "timestamp": timestamp | |
| }) | |
| if self.scene_sort_mode == "alphabetical_asc": | |
| ref_items.sort(key=lambda x: (x["sort_name"].lower(), x["version"], x["asset"])) | |
| elif self.scene_sort_mode == "alphabetical_desc": | |
| ref_items.sort(key=lambda x: (x["sort_name"].lower(), x["version"], x["asset"]), reverse=True) | |
| elif self.scene_sort_mode == "version": | |
| ref_items.sort(key=lambda x: (x["version"], x["name"], x["asset"])) | |
| elif self.scene_sort_mode == "time": | |
| ref_items.sort(key=lambda x: x["timestamp"]) | |
| else: # "asset" | |
| ref_items.sort(key=lambda x: (x["asset"], x["name"], x["version"])) | |
| for item_data in ref_items: | |
| item = QtWidgets.QListWidgetItem(item_data["text"]) | |
| group_color = group_color_map[item_data["group_name"]] | |
| group_count = sum(1 for r in ref_items if r["group_name"] == item_data["group_name"]) | |
| if not item_data["is_loaded"]: | |
| item.setForeground(QtGui.QColor("grey")) | |
| elif group_count > 1: | |
| item.setForeground(QtGui.QColor(group_color)) | |
| else: | |
| item.setForeground(QtGui.QColor("white")) | |
| item.setData(QtCore.Qt.UserRole, item_data["ref"]) | |
| self.scene_ref_list.addItem(item) | |
| self.scene_ref_count_label.setText(f"Reference(s) in Scene: {len(current_refs)}") | |
| def on_scene_sort_changed(self, text): | |
| sort_mapping = { | |
| "Alphabetical A-Z": "alphabetical_asc", | |
| "Alphabetical Z-A": "alphabetical_desc", | |
| "Version": "version", | |
| "Asset": "asset", | |
| "Time": "time" | |
| } | |
| self.scene_sort_mode = sort_mapping.get(text, "asset") | |
| self.update_scene_ref_list() | |
| self.save_window_state() | |
| def remove_from_scene_ref_list(self): | |
| selected = self.scene_ref_list.selectedItems() | |
| if not selected: | |
| self.status_bar.setText("No references selected to remove.") | |
| return | |
| removed_count = 0 | |
| for item in list(selected): | |
| file_path = item.data(QtCore.Qt.UserRole) | |
| try: | |
| cmds.file(file_path, removeReference=True) | |
| self.scene_ref_list.takeItem(self.scene_ref_list.row(item)) | |
| if file_path in self.ref_timestamps: | |
| del self.ref_timestamps[file_path] | |
| removed_count += 1 | |
| except RuntimeError as e: | |
| self.status_bar.setText(f"Error removing {os.path.basename(file_path)}: {str(e)}") | |
| if removed_count > 0: | |
| self.status_bar.setText(f"Removed {removed_count} reference(s) from scene.") | |
| self.update_scene_ref_list() | |
| self.update_ref_list_display() | |
| def swap_reference(self, old_file_path): | |
| """Swap the selected reference with a new file.""" | |
| if not old_file_path: | |
| self.status_bar.setText("No reference selected to swap.") | |
| return | |
| # Get the current namespace | |
| try: | |
| ref_node = cmds.referenceQuery(old_file_path, referenceNode=True) | |
| namespace = cmds.referenceQuery(ref_node, namespace=True).lstrip(":") | |
| except RuntimeError as e: | |
| self.status_bar.setText(f"Error querying {os.path.basename(old_file_path)}: {str(e)}") | |
| return | |
| # Open file dialog to select new reference | |
| new_file, _ = QtWidgets.QFileDialog.getOpenFileName( | |
| self, | |
| "Select New Reference File", | |
| self.project_dir, | |
| "Maya Files (*.ma *.mb);;FBX Files (*.fbx);;All Files (*.*)" | |
| ) | |
| if not new_file: | |
| self.status_bar.setText("No file selected for swap.") | |
| return | |
| # Verify the new file exists | |
| clean_new_file = self.clean_file_path(new_file) | |
| if not os.path.exists(clean_new_file): | |
| self.status_bar.setText(f"Error: {os.path.basename(clean_new_file)} not found.") | |
| return | |
| # Determine file type | |
| file_type = "mayaAscii" if clean_new_file.endswith(".ma") else "mayaBinary" if clean_new_file.endswith(".mb") else "FBX" | |
| # Remove the old reference | |
| try: | |
| cmds.file(old_file_path, removeReference=True) | |
| if old_file_path in self.ref_timestamps: | |
| del self.ref_timestamps[old_file_path] | |
| except RuntimeError as e: | |
| self.status_bar.setText(f"Error removing {os.path.basename(old_file_path)}: {str(e)}") | |
| return | |
| # Add the new reference with the same namespace | |
| try: | |
| cmds.file(clean_new_file, reference=True, namespace=namespace, type=file_type) | |
| self.ref_timestamps[clean_new_file] = time.time() | |
| self.status_bar.setText(f"Swapped {os.path.basename(old_file_path)} with {os.path.basename(clean_new_file)}") | |
| except RuntimeError as e: | |
| self.status_bar.setText(f"Error referencing {os.path.basename(clean_new_file)}: {str(e)}") | |
| return | |
| self.update_scene_ref_list() | |
| def duplicate_reference(self): | |
| """Duplicate selected references with incremented namespaces.""" | |
| selected = self.scene_ref_list.selectedItems() | |
| if not selected: | |
| self.status_bar.setText("No references selected to duplicate.") | |
| return | |
| duplicated_count = 0 | |
| for item in selected: | |
| file_path = item.data(QtCore.Qt.UserRole) | |
| clean_file_path = self.clean_file_path(file_path) | |
| try: | |
| ref_node = cmds.referenceQuery(file_path, referenceNode=True) | |
| namespace = cmds.referenceQuery(ref_node, namespace=True).lstrip(":") | |
| file_type = "mayaAscii" if clean_file_path.endswith(".ma") else "mayaBinary" if clean_file_path.endswith(".mb") else "FBX" | |
| # Generate incremented namespace | |
| base_namespace = re.sub(r'_\d+$', '', namespace) # Remove existing numeric suffix | |
| suffix = 1 | |
| while cmds.namespace(exists=f"{base_namespace}_{suffix}"): | |
| suffix += 1 | |
| new_namespace = f"{base_namespace}_{suffix}" | |
| # Create new reference | |
| cmds.file(clean_file_path, reference=True, namespace=new_namespace, type=file_type) | |
| self.ref_timestamps[file_path] = time.time() | |
| duplicated_count += 1 | |
| except RuntimeError as e: | |
| self.status_bar.setText(f"Error duplicating {os.path.basename(clean_file_path)}: {str(e)}") | |
| if duplicated_count > 0: | |
| self.status_bar.setText(f"Duplicated {duplicated_count} reference(s)") | |
| self.update_scene_ref_list() | |
| def import_reference(self): | |
| """Import selected references into the scene, embedding their contents.""" | |
| selected = self.scene_ref_list.selectedItems() | |
| if not selected: | |
| self.status_bar.setText("No references selected to import.") | |
| return | |
| imported_count = 0 | |
| # Create a copy of selected items to avoid modification during iteration | |
| selected_items = list(selected) | |
| for index, item in enumerate(selected_items): | |
| file_path = item.data(QtCore.Qt.UserRole) | |
| # Debug: Print what the method is looking for | |
| print(f"Processing item: {item.text()}") | |
| print(f"Original file path: {file_path}") | |
| # Clean {N} suffix from file_path | |
| clean_file_path = self.clean_file_path(file_path) | |
| print(f"Cleaned file path: {clean_file_path}") | |
| print(f"File exists: {os.path.exists(clean_file_path) if clean_file_path else False}") | |
| if not clean_file_path or not os.path.exists(clean_file_path): | |
| self.status_bar.setText(f"Error: {clean_file_path if clean_file_path else 'Unknown file'} not found.") | |
| print(f"Error: Skipping item due to invalid or missing file path.") | |
| continue | |
| try: | |
| # Get reference node | |
| ref_node = cmds.referenceQuery(file_path, referenceNode=True) | |
| # Get namespace and clean only {N} suffixes | |
| namespace = cmds.referenceQuery(ref_node, namespace=True).lstrip(":") | |
| clean_namespace = re.sub(r'\{[0-9]+\}', '', namespace) # Remove {1}, {2}, etc. | |
| # Debug: Print namespace details | |
| print(f"Raw namespace: {namespace}") | |
| print(f"Clean namespace: {clean_namespace}") | |
| # Sanitize namespace: replace invalid characters, preserving _N | |
| clean_namespace = re.sub(r'[^a-zA-Z0-9_]', '_', clean_namespace) | |
| # Validate namespace; fallback to file-based namespace if invalid | |
| if not clean_namespace or not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', clean_namespace): | |
| clean_namespace = re.sub(r'[^a-zA-Z0-9_]', '_', os.path.basename(clean_file_path).split(".")[0]) | |
| print(f"Fallback namespace (from file): {clean_namespace}") | |
| # Ensure unique namespace by appending numeric suffix if needed | |
| base_namespace = clean_namespace | |
| suffix = 0 | |
| while cmds.namespace(exists=clean_namespace): | |
| suffix += 1 | |
| clean_namespace = f"{base_namespace}_{suffix}" | |
| print(f"Final namespace: {clean_namespace}") | |
| # Determine file type | |
| file_type = "mayaAscii" if clean_file_path.endswith(".ma") else "mayaBinary" if clean_file_path.endswith(".mb") else "FBX" | |
| print(f"File type: {file_type}") | |
| # Import with a temporary namespace | |
| temp_namespace = f"temp_import_{index}" | |
| print(f"Importing with temporary namespace: {temp_namespace}") | |
| cmds.file(clean_file_path, i=True, type=file_type, namespace=temp_namespace, preserveReferences=False) | |
| # Rename to the final cleaned, unique namespace | |
| print(f"Renaming namespace from {temp_namespace} to {clean_namespace}") | |
| cmds.namespace(rename=[temp_namespace, clean_namespace], force=True) | |
| # Remove the reference from the scene | |
| print(f"Removing reference: {file_path}") | |
| cmds.file(file_path, removeReference=True) | |
| if file_path in self.ref_timestamps: | |
| del self.ref_timestamps[file_path] | |
| # Remove the item from scene_ref_list | |
| print(f"Removing item from scene_ref_list: {item.text()}") | |
| self.scene_ref_list.takeItem(self.scene_ref_list.row(item)) | |
| imported_count += 1 | |
| except RuntimeError as e: | |
| self.status_bar.setText(f"Error importing {os.path.basename(clean_file_path)}: {str(e)}") | |
| print(f"Error importing {clean_file_path}: {str(e)}") | |
| continue | |
| if imported_count > 0: | |
| self.status_bar.setText(f"Imported {imported_count} asset(s) into scene") | |
| print(f"Successfully imported {imported_count} asset(s)") | |
| else: | |
| self.status_bar.setText("No assets imported due to errors.") | |
| print("No assets imported due to errors in all selected items.") | |
| # Update the reference count label | |
| current_refs = cmds.file(query=True, reference=True) or [] | |
| self.scene_ref_count_label.setText(f"Reference(s) in Scene: {len(current_refs)}") | |
| print(f"Updated scene reference count: {len(current_refs)}") | |
| def open_source_location(self, file_path): | |
| try: | |
| clean_file_path = self.clean_file_path(file_path) | |
| directory = os.path.dirname(clean_file_path) | |
| if not os.path.exists(directory): | |
| self.status_bar.setText(f"Directory not found for {os.path.basename(clean_file_path)}.") | |
| return | |
| os.startfile(directory) | |
| self.status_bar.setText(f"Opened source location for {os.path.basename(clean_file_path)}.") | |
| except Exception as e: | |
| self.status_bar.setText(f"Error opening location for {os.path.basename(clean_file_path)}: {str(e)}") | |
| def clear_reference_list(self): | |
| if self.clear_ref_list_checkbox.isChecked(): | |
| self.ref_items.clear() | |
| self.ref_list.clear() | |
| self.ref_timestamps.clear() | |
| self.update_ref_list_display() | |
| def overwrite_scene(self): | |
| desired_refs = [next((path for path, info in self.ref_items.items() if info["item"] == self.ref_list.item(i)), None) for i in range(self.ref_list.count())] | |
| current_refs = cmds.file(query=True, reference=True) or [] | |
| for ref in current_refs: | |
| if ref not in desired_refs: | |
| try: | |
| cmds.file(ref, removeReference=True) | |
| self.status_bar.setText(f"Removed reference: {os.path.basename(ref)}") | |
| if ref in self.ref_timestamps: | |
| del self.ref_timestamps[ref] | |
| except RuntimeError as e: | |
| self.status_bar.setText(f"Error removing {os.path.basename(ref)}: {str(e)}") | |
| for file_path in desired_refs: | |
| clean_file_path = self.clean_file_path(file_path) | |
| if not file_path or not os.path.exists(clean_file_path): | |
| self.status_bar.setText(f"Error: {os.path.basename(clean_file_path)} not found.") | |
| continue | |
| namespace = re.sub(r'[^a-zA-Z0-9_]', '_', os.path.basename(clean_file_path).split(".")[0]) | |
| file_type = "mayaAscii" if clean_file_path.endswith(".ma") else "mayaBinary" if clean_file_path.endswith(".mb") else "FBX" | |
| try: | |
| if file_path not in current_refs: | |
| cmds.file(clean_file_path, reference=True, namespace=namespace, type=file_type) | |
| self.ref_timestamps[file_path] = time.time() | |
| self.status_bar.setText(f"Referenced: {os.path.basename(clean_file_path)}") | |
| else: | |
| self.status_bar.setText(f"Reference already exists: {os.path.basename(clean_file_path)}") | |
| except RuntimeError as e: | |
| self.status_bar.setText(f"Error referencing {os.path.basename(clean_file_path)}: {str(e)}") | |
| self.status_bar.setText(f"Scene overwritten with {len(desired_refs)} references.") | |
| self.clear_reference_list() | |
| self.update_ref_list_display() | |
| self.update_scene_ref_list() | |
| def add_to_scene(self): | |
| desired_refs = [next((path for path, info in self.ref_items.items() if info["item"] == self.ref_list.item(i)), None) for i in range(self.ref_list.count())] | |
| if not desired_refs: | |
| QtWidgets.QMessageBox.warning(self, "Empty Reference List", "Please add assets to the reference list.") | |
| self.status_bar.setText("Please add assets to reference list.") | |
| return | |
| added_count = 0 | |
| for file_path in desired_refs: | |
| clean_file_path = self.clean_file_path(file_path) | |
| if not file_path or not os.path.exists(clean_file_path): | |
| self.status_bar.setText(f"Error: {os.path.basename(clean_file_path)} not found.") | |
| continue | |
| base_namespace = re.sub(r'[^a-zA-Z0-9_]', '_', os.path.basename(clean_file_path).split(".")[0]) | |
| if clean_file_path.endswith(".ma"): | |
| file_type = "mayaAscii" | |
| elif clean_file_path.endswith(".mb"): | |
| file_type = "mayaBinary" | |
| else: | |
| file_type = "FBX" | |
| try: | |
| # Generate unique namespace | |
| suffix = added_count | |
| namespace = f"{base_namespace}_{suffix}" | |
| while cmds.namespace(exists=namespace): | |
| suffix += 1 | |
| namespace = f"{base_namespace}_{suffix}" | |
| cmds.file(clean_file_path, reference=True, namespace=namespace, type=file_type) | |
| self.ref_timestamps[file_path] = time.time() | |
| self.status_bar.setText(f"Added to scene: {os.path.basename(clean_file_path)}") | |
| added_count += 1 | |
| except RuntimeError as e: | |
| self.status_bar.setText(f"Error adding {os.path.basename(clean_file_path)}: {str(e)}") | |
| self.update_scene_ref_list() | |
| self.clear_reference_list() | |
| self.update_ref_list_display() | |
| self.status_bar.setText(f"Added {added_count} assets to scene.") | |
| def show_dir_field_context_menu(self, pos): | |
| menu = QtWidgets.QMenu(self) | |
| if not self.project_history: | |
| menu.addAction("No previous directories").setEnabled(False) | |
| else: | |
| for entry in sorted(self.project_history, key=lambda x: x["timestamp"], reverse=True): | |
| path = entry["path"] | |
| action = menu.addAction(path) | |
| action.setData(path) | |
| action = menu.exec_(self.dir_field.mapToGlobal(pos)) | |
| if action and action.data(): | |
| self.change_project_dir(action.data()) | |
| def show_ref_list_context_menu(self, pos): | |
| item_at_pos = self.ref_list.itemAt(pos) | |
| if item_at_pos and item_at_pos not in self.ref_list.selectedItems(): | |
| self.ref_list.clearSelection() | |
| item_at_pos.setSelected(True) | |
| selected_items = self.ref_list.selectedItems() | |
| menu = QtWidgets.QMenu(self) | |
| remove_action = menu.addAction("Remove from Reference List") | |
| remove_action.setEnabled(len(selected_items) > 0) | |
| menu.addSeparator() | |
| taller_action = menu.addAction("Make Taller") | |
| shorter_action = menu.addAction("Make Shorter") | |
| taller_action.setEnabled(self.ref_list_height < 400) | |
| shorter_action.setEnabled(self.ref_list_height > 100) | |
| action = menu.exec_(self.ref_list.mapToGlobal(pos)) | |
| if action == remove_action: | |
| self.remove_from_ref_list() | |
| elif action == taller_action: | |
| self.ref_list_height = min(self.ref_list_height + 50, 400) | |
| self.ref_list.setFixedHeight(self.ref_list_height) | |
| self.ref_list.updateGeometry() | |
| self.status_bar.setText(f"Assets to Reference list height set to {self.ref_list_height}px") | |
| elif action == shorter_action: | |
| self.ref_list_height = max(self.ref_list_height - 50, 100) | |
| self.ref_list.setFixedHeight(self.ref_list_height) | |
| self.ref_list.updateGeometry() | |
| self.status_bar.setText(f"Assets to Reference list height set to {self.ref_list_height}px") | |
| def show_scene_ref_context_menu(self, pos): | |
| item_at_pos = self.scene_ref_list.itemAt(pos) | |
| if item_at_pos and item_at_pos not in self.scene_ref_list.selectedItems(): | |
| self.scene_ref_list.clearSelection() | |
| item_at_pos.setSelected(True) | |
| selected_items = self.scene_ref_list.selectedItems() | |
| menu = QtWidgets.QMenu(self) | |
| swap_action = menu.addAction("Swap Reference") | |
| import_action = menu.addAction("Import Reference") | |
| duplicate_action = menu.addAction("Duplicate Reference") | |
| menu.addSeparator() | |
| load_action = menu.addAction("Load Reference") | |
| reload_action = menu.addAction("Reload Reference") | |
| unload_action = menu.addAction("Unload Reference") | |
| remove_action = menu.addAction("Remove Reference") | |
| load_all_action = menu.addAction("Load All References") | |
| unload_all_action = menu.addAction("Unload All References") | |
| reload_all_action = menu.addAction("Reload All References") | |
| remove_all_action = menu.addAction("Remove All References") | |
| menu.addSeparator() | |
| open_source_action = menu.addAction("Open Source") | |
| open_location_action = menu.addAction("Open Source Location") | |
| refresh_action = menu.addAction("Refresh List") | |
| menu.addSeparator() | |
| taller_action = menu.addAction("Make Taller") | |
| shorter_action = menu.addAction("Make Shorter") | |
| # Add dark grey icons for "all" actions | |
| grey_icon = self.create_colored_icon("#333333") | |
| for action in [load_all_action, unload_all_action, reload_all_action, remove_all_action]: | |
| action.setIcon(grey_icon) | |
| any_loaded = False | |
| any_unloaded = False | |
| if selected_items: | |
| for item in selected_items: | |
| file_path = item.data(QtCore.Qt.UserRole) | |
| try: | |
| ref_node = cmds.referenceQuery(file_path, referenceNode=True) | |
| is_loaded = cmds.referenceQuery(ref_node, isLoaded=True) | |
| any_loaded = any_loaded or is_loaded | |
| any_unloaded = any_unloaded or not is_loaded | |
| except RuntimeError as e: | |
| self.status_bar.setText(f"Error querying {os.path.basename(file_path)}: {str(e)}") | |
| swap_action.setEnabled(len(selected_items) == 1) | |
| import_action.setEnabled(len(selected_items) > 0) | |
| duplicate_action.setEnabled(len(selected_items) > 0) | |
| load_action.setEnabled(any_unloaded) | |
| reload_action.setEnabled(any_loaded) | |
| unload_action.setEnabled(any_loaded) | |
| remove_action.setEnabled(len(selected_items) > 0) | |
| load_all_action.setEnabled(True) | |
| unload_all_action.setEnabled(True) | |
| reload_all_action.setEnabled(True) | |
| remove_all_action.setEnabled(True) | |
| open_source_action.setEnabled(len(selected_items) == 1) | |
| open_location_action.setEnabled(len(selected_items) == 1) | |
| refresh_action.setEnabled(True) | |
| taller_action.setEnabled(self.scene_ref_list_height < 400) | |
| shorter_action.setEnabled(self.scene_ref_list_height > 100) | |
| action = menu.exec_(self.scene_ref_list.mapToGlobal(pos)) | |
| if action == swap_action and len(selected_items) == 1: | |
| file_path = selected_items[0].data(QtCore.Qt.UserRole) | |
| self.swap_reference(file_path) | |
| elif action == import_action: | |
| self.import_reference() | |
| elif action == duplicate_action: | |
| self.duplicate_reference() | |
| elif action == load_action: | |
| loaded_count = 0 | |
| for item in selected_items: | |
| file_path = item.data(QtCore.Qt.UserRole) | |
| clean_file_path = self.clean_file_path(file_path) | |
| try: | |
| ref_node = cmds.referenceQuery(file_path, referenceNode=True) | |
| if not cmds.referenceQuery(ref_node, isLoaded=True): | |
| cmds.file(clean_file_path, loadReference=True) | |
| item.setForeground(QtGui.QColor("white")) | |
| loaded_count += 1 | |
| except RuntimeError as e: | |
| self.status_bar.setText(f"Error loading {os.path.basename(clean_file_path)}: {str(e)}") | |
| if loaded_count > 0: | |
| self.status_bar.setText(f"Loaded {loaded_count} reference(s)") | |
| self.update_ref_list_display() | |
| self.update_scene_ref_list() | |
| elif action == reload_action: | |
| reloaded_count = 0 | |
| for item in selected_items: | |
| file_path = item.data(QtCore.Qt.UserRole) | |
| clean_file_path = self.clean_file_path(file_path) | |
| try: | |
| ref_node = cmds.referenceQuery(file_path, referenceNode=True) | |
| if cmds.referenceQuery(ref_node, isLoaded=True): | |
| cmds.file(clean_file_path, loadReference=True) | |
| reloaded_count += 1 | |
| except RuntimeError as e: | |
| self.status_bar.setText(f"Error reloading {os.path.basename(clean_file_path)}: {str(e)}") | |
| if reloaded_count > 0: | |
| self.status_bar.setText(f"Reloaded {reloaded_count} reference(s)") | |
| self.update_ref_list_display() | |
| self.update_scene_ref_list() | |
| elif action == unload_action: | |
| unloaded_count = 0 | |
| for item in selected_items: | |
| file_path = item.data(QtCore.Qt.UserRole) | |
| try: | |
| ref_node = cmds.referenceQuery(file_path, referenceNode=True) | |
| if cmds.referenceQuery(ref_node, isLoaded=True): | |
| cmds.file(file_path, unloadReference=True) | |
| item.setForeground(QtGui.QColor("grey")) | |
| unloaded_count += 1 | |
| except RuntimeError as e: | |
| self.status_bar.setText(f"Error unloading {os.path.basename(file_path)}: {str(e)}") | |
| if unloaded_count > 0: | |
| self.status_bar.setText(f"Unloaded {unloaded_count} reference(s)") | |
| self.update_ref_list_display() | |
| self.update_scene_ref_list() | |
| elif action == remove_action: | |
| self.remove_from_scene_ref_list() | |
| elif action == load_all_action: | |
| current_refs = cmds.file(query=True, reference=True) or [] | |
| loaded_count = 0 | |
| for file_path in current_refs: | |
| clean_file_path = self.clean_file_path(file_path) | |
| try: | |
| ref_node = cmds.referenceQuery(file_path, referenceNode=True) | |
| if not cmds.referenceQuery(ref_node, isLoaded=True): | |
| cmds.file(clean_file_path, loadReference=True) | |
| loaded_count += 1 | |
| except RuntimeError as e: | |
| self.status_bar.setText(f"Error loading {os.path.basename(clean_file_path)}: {str(e)}") | |
| if loaded_count > 0: | |
| self.status_bar.setText(f"Loaded all {loaded_count} reference(s)") | |
| else: | |
| self.status_bar.setText("No references were unloaded to load") | |
| self.update_ref_list_display() | |
| self.update_scene_ref_list() | |
| elif action == unload_all_action: | |
| current_refs = cmds.file(query=True, reference=True) or [] | |
| unloaded_count = 0 | |
| for file_path in current_refs: | |
| try: | |
| ref_node = cmds.referenceQuery(file_path, referenceNode=True) | |
| if cmds.referenceQuery(ref_node, isLoaded=True): | |
| cmds.file(file_path, unloadReference=True) | |
| unloaded_count += 1 | |
| except RuntimeError as e: | |
| self.status_bar.setText(f"Error unloading {os.path.basename(file_path)}: {str(e)}") | |
| if unloaded_count > 0: | |
| self.status_bar.setText(f"Unloaded all {unloaded_count} reference(s)") | |
| else: | |
| self.status_bar.setText("No references were loaded to unload") | |
| self.update_ref_list_display() | |
| self.update_scene_ref_list() | |
| elif action == reload_all_action: | |
| current_refs = cmds.file(query=True, reference=True) or [] | |
| reloaded_count = 0 | |
| for file_path in current_refs: | |
| clean_file_path = self.clean_file_path(file_path) | |
| try: | |
| ref_node = cmds.referenceQuery(file_path, referenceNode=True) | |
| if cmds.referenceQuery(ref_node, isLoaded=True): | |
| cmds.file(clean_file_path, loadReference=True) | |
| reloaded_count += 1 | |
| except RuntimeError as e: | |
| self.status_bar.setText(f"Error reloading {os.path.basename(clean_file_path)}: {str(e)}") | |
| if reloaded_count > 0: | |
| self.status_bar.setText(f"Reloaded all {reloaded_count} reference(s)") | |
| else: | |
| self.status_bar.setText("No references were loaded to reload") | |
| self.update_ref_list_display() | |
| self.update_scene_ref_list() | |
| elif action == remove_all_action: | |
| current_refs = cmds.file(query=True, reference=True) or [] | |
| removed_count = 0 | |
| self.scene_ref_list.clear() | |
| for file_path in current_refs: | |
| try: | |
| cmds.file(file_path, removeReference=True) | |
| if file_path in self.ref_timestamps: | |
| del self.ref_timestamps[file_path] | |
| removed_count += 1 | |
| except RuntimeError as e: | |
| self.status_bar.setText(f"Error removing {os.path.basename(file_path)}: {str(e)}") | |
| if removed_count > 0: | |
| self.status_bar.setText(f"Removed all {removed_count} reference(s)") | |
| else: | |
| self.status_bar.setText("No references to remove") | |
| self.update_scene_ref_list() | |
| self.update_ref_list_display() | |
| elif action == open_source_action and len(selected_items) == 1: | |
| file_path = selected_items[0].data(QtCore.Qt.UserRole) | |
| self.open_maya_source(file_path) | |
| elif action == open_location_action and len(selected_items) == 1: | |
| file_path = selected_items[0].data(QtCore.Qt.UserRole) | |
| self.open_source_location(file_path) | |
| elif action == refresh_action: | |
| self.update_scene_ref_list() | |
| self.status_bar.setText("Scene references list refreshed.") | |
| elif action == taller_action: | |
| self.scene_ref_list_height = min(self.scene_ref_list_height + 50, 400) | |
| self.scene_ref_list.setFixedHeight(self.scene_ref_list_height) | |
| self.scene_ref_list.updateGeometry() | |
| self.status_bar.setText(f"Current Scene References list height set to {self.scene_ref_list_height}px") | |
| elif action == shorter_action: | |
| self.scene_ref_list_height = max(self.scene_ref_list_height - 50, 100) | |
| self.scene_ref_list.setFixedHeight(self.scene_ref_list_height) | |
| self.scene_ref_list.updateGeometry() | |
| self.status_bar.setText(f"Current Scene References list height set to {self.scene_ref_list_height}px") | |
| def show_browser_context_menu(self, pos): | |
| item_at_pos = self.browser_list.itemAt(pos) | |
| if item_at_pos and item_at_pos not in self.browser_list.selectedItems(): | |
| self.browser_list.clearSelection() | |
| item_at_pos.setSelected(True) | |
| selected_items = self.browser_list.selectedItems() | |
| menu = QtWidgets.QMenu(self) | |
| info_action = menu.addAction("Show Asset Info") | |
| open_source_action = menu.addAction("Open Source") | |
| open_location_action = menu.addAction("Open Source Location") | |
| refresh_action = menu.addAction("Refresh List") | |
| menu.addSeparator() | |
| taller_action = menu.addAction("Make Taller") | |
| shorter_action = menu.addAction("Make Shorter") | |
| info_action.setEnabled(len(selected_items) > 0) | |
| open_source_action.setEnabled(len(selected_items) == 1) | |
| open_location_action.setEnabled(len(selected_items) == 1) | |
| refresh_action.setEnabled(True) | |
| taller_action.setEnabled(self.browser_list_height < 400) | |
| shorter_action.setEnabled(self.browser_list_height > 100) | |
| action = menu.exec_(self.browser_list.mapToGlobal(pos)) | |
| if action == info_action: | |
| info_text = "" | |
| for item in selected_items: | |
| file_path = next((asset for asset in self.asset_cache if os.path.basename(asset) == item.text() or asset == item.text()), None) | |
| if file_path: | |
| try: | |
| clean_file_path = self.clean_file_path(file_path) | |
| ctime = time.ctime(os.path.getctime(clean_file_path)) | |
| owner = str(os.stat(clean_file_path).st_uid) | |
| info_text += f"Asset: {os.path.basename(clean_file_path)}\nCreated: {ctime}\nOwner UID: {owner}\n\n" | |
| except OSError as e: | |
| info_text += f"Asset: {os.path.basename(clean_file_path)}\nError retrieving info: {str(e)}\n\n" | |
| if info_text: | |
| QtWidgets.QMessageBox.information(self, "Asset Info", info_text.strip()) | |
| elif action == open_source_action and len(selected_items) == 1: | |
| file_path = next((asset for asset in self.asset_cache if os.path.basename(asset) == selected_items[0].text() or asset == selected_items[0].text()), None) | |
| if file_path: | |
| self.open_maya_source(file_path) | |
| elif action == open_location_action and len(selected_items) == 1: | |
| file_path = next((asset for asset in self.asset_cache if os.path.basename(asset) == selected_items[0].text() or asset == selected_items[0].text()), None) | |
| if file_path: | |
| self.open_source_location(file_path) | |
| elif action == refresh_action: | |
| self.asset_cache = self.cache_assets() | |
| self.update_browser() | |
| self.status_bar.setText("Asset browser list refreshed.") | |
| elif action == taller_action: | |
| self.browser_list_height = min(self.browser_list_height + 50, 400) | |
| self.browser_list.setFixedHeight(self.browser_list_height) | |
| self.browser_list.updateGeometry() | |
| self.status_bar.setText(f"Asset Browser list height set to {self.browser_list_height}px") | |
| elif action == shorter_action: | |
| self.browser_list_height = max(self.browser_list_height - 50, 100) | |
| self.browser_list.setFixedHeight(self.browser_list_height) | |
| self.browser_list.updateGeometry() | |
| self.status_bar.setText(f"Asset Browser list height set to {self.browser_list_height}px") | |
| def open_maya_source(self, file_path): | |
| try: | |
| clean_file_path = self.clean_file_path(file_path) | |
| maya_exe = "maya.exe" # Adjust to your Maya installation path if needed | |
| subprocess.Popen([maya_exe, "-file", clean_file_path]) | |
| self.status_bar.setText(f"Opening {os.path.basename(clean_file_path)} in new Maya instance.") | |
| except Exception as e: | |
| self.status_bar.setText(f"Error opening {os.path.basename(clean_file_path)}: {str(e)}") | |
| def change_project_dir(self, new_dir=None): | |
| if not new_dir: | |
| new_dir = QtWidgets.QFileDialog.getExistingDirectory(self, "Select Project Directory", self.project_dir) | |
| if new_dir: | |
| self.project_dir = new_dir | |
| self.dir_field.setText(new_dir) | |
| self.asset_cache = self.cache_assets() | |
| self.update_browser() | |
| self.update_ref_list_display() | |
| self.project_history.append({"path": new_dir, "timestamp": time.time()}) | |
| self.save_project_history() | |
| self.status_bar.setText(f"Project directory changed to {new_dir}") | |
| def get_preset_dir(self): | |
| user = getpass.getuser() | |
| preset_dir = f"C:/Users/{user}/Documents/maya_json" | |
| if not os.path.exists(preset_dir): | |
| os.makedirs(preset_dir) | |
| return preset_dir | |
| def save_preset(self): | |
| current_refs = cmds.file(query=True, reference=True) or [] | |
| if not current_refs: | |
| self.status_bar.setText("Scene has no references to save.") | |
| return | |
| preset_name, ok = QtWidgets.QInputDialog.getText(self, "Save Preset", "Enter preset name:") | |
| if ok and preset_name: | |
| preset_path = os.path.join(self.get_preset_dir(), f"{preset_name}.json") | |
| with open(preset_path, 'w') as f: | |
| json.dump(current_refs, f, indent=4) | |
| self.status_bar.setText(f"Saved preset to {preset_path}") | |
| def load_preset(self): | |
| preset_dir = self.get_preset_dir() | |
| preset_file, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Load Preset", preset_dir, "JSON Files (*.json)") | |
| if preset_file: | |
| reply = QtWidgets.QMessageBox.warning( | |
| self, | |
| "Confirm Load Preset", | |
| "Loading this preset will overwrite all scene references. Continue?", | |
| QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, | |
| QtWidgets.QMessageBox.No | |
| ) | |
| if reply != QtWidgets.QMessageBox.Yes: | |
| self.status_bar.setText("Load preset cancelled.") | |
| return | |
| with open(preset_file, 'r') as f: | |
| ref_list = json.load(f) | |
| current_refs = cmds.file(query=True, reference=True) or [] | |
| for ref in current_refs: | |
| try: | |
| cmds.file(ref, removeReference=True) | |
| if ref in self.ref_timestamps: | |
| del self.ref_timestamps[ref] | |
| except RuntimeError as e: | |
| self.status_bar.setText(f"Error removing {os.path.basename(ref)}: {str(e)}") | |
| added_count = 0 | |
| for file_path in ref_list: | |
| clean_file_path = self.clean_file_path(file_path) | |
| if not os.path.exists(clean_file_path): | |
| self.status_bar.setText(f"Error: {os.path.basename(clean_file_path)} not found.") | |
| continue | |
| namespace = re.sub(r'[^a-zA-Z0-9_]', '_', os.path.basename(clean_file_path).split(".")[0]) | |
| file_type = "mayaAscii" if clean_file_path.endswith(".ma") else "mayaBinary" if clean_file_path.endswith(".mb") else "FBX" | |
| try: | |
| cmds.file(clean_file_path, reference=True, namespace=namespace, type=file_type) | |
| self.ref_timestamps[file_path] = time.time() | |
| self.status_bar.setText(f"Referenced: {os.path.basename(clean_file_path)}") | |
| added_count += 1 | |
| except RuntimeError as e: | |
| self.status_bar.setText(f"Error referencing {os.path.basename(clean_file_path)}: {str(e)}") | |
| self.status_bar.setText(f"Loaded preset from {preset_file} with {added_count} references") | |
| self.update_scene_ref_list() | |
| def append_preset(self): | |
| preset_dir = self.get_preset_dir() | |
| preset_file, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Append Preset", preset_dir, "JSON Files (*.json)") | |
| if preset_file: | |
| with open(preset_file, 'r') as f: | |
| ref_list = json.load(f) | |
| added_count = 0 | |
| if self.append_preset_to_scene_checkbox.isChecked(): | |
| # Append directly to Current Scene References | |
| for file_path in ref_list: | |
| clean_file_path = self.clean_file_path(file_path) | |
| if not os.path.exists(clean_file_path): | |
| self.status_bar.setText(f"Error: {os.path.basename(clean_file_path)} not found.") | |
| continue | |
| base_namespace = re.sub(r'[^a-zA-Z0-9_]', '_', os.path.basename(clean_file_path).split(".")[0]) | |
| file_type = "mayaAscii" if clean_file_path.endswith(".ma") else "mayaBinary" if clean_file_path.endswith(".mb") else "FBX" | |
| try: | |
| # Generate unique namespace | |
| suffix = 0 | |
| namespace = base_namespace | |
| while cmds.namespace(exists=namespace): | |
| suffix += 1 | |
| namespace = f"{base_namespace}_{suffix}" | |
| cmds.file(clean_file_path, reference=True, namespace=namespace, type=file_type) | |
| self.ref_timestamps[file_path] = time.time() | |
| added_count += 1 | |
| except RuntimeError as e: | |
| self.status_bar.setText(f"Error referencing {os.path.basename(clean_file_path)}: {str(e)}") | |
| self.update_scene_ref_list() | |
| self.status_bar.setText(f"Referenced {added_count} assets to scene from {preset_file}") | |
| else: | |
| # Add to Assets to Reference list | |
| for file_path in ref_list: | |
| clean_file_path = self.clean_file_path(file_path) | |
| if not os.path.exists(clean_file_path): | |
| self.status_bar.setText(f"Error: {os.path.basename(clean_file_path)} not found.") | |
| continue | |
| if file_path not in self.ref_items: | |
| display_text = clean_file_path if self.show_full_path else os.path.basename(clean_file_path) | |
| list_item = QtWidgets.QListWidgetItem(display_text) | |
| list_item.setForeground(QtGui.QColor("white")) | |
| self.ref_list.addItem(list_item) | |
| self.ref_items[file_path] = {"item": list_item} | |
| self.ref_timestamps[file_path] = time.time() | |
| added_count += 1 | |
| self.update_ref_list_display() | |
| self.status_bar.setText(f"Appended {added_count} assets from {preset_file} to reference list") | |
| def open_reference_editor(self): | |
| try: | |
| mel.eval("ReferenceEditor;") | |
| self.status_bar.setText("Opened Reference Editor") | |
| except RuntimeError as e: | |
| self.status_bar.setText(f"Error opening Reference Editor: {str(e)}") | |
| def open_namespace_editor(self): | |
| try: | |
| mel.eval("NamespaceEditor;") | |
| self.status_bar.setText("Opened Namespace Editor") | |
| except RuntimeError as e: | |
| self.status_bar.setText(f"Error opening Namespace Editor: {str(e)}") | |
| def cache_assets(self): | |
| ma_files = glob.glob(os.path.join(self.project_dir, "**", "*.ma"), recursive=True) | |
| mb_files = glob.glob(os.path.join(self.project_dir, "**", "*.mb"), recursive=True) | |
| fbx_files = glob.glob(os.path.join(self.project_dir, "**", "*.fbx"), recursive=True) | |
| return ma_files + mb_files + fbx_files | |
| def load_project_history(self): | |
| history_file = os.path.join(self.get_preset_dir(), "project_history.json") | |
| if os.path.exists(history_file): | |
| try: | |
| with open(history_file, 'r') as f: | |
| history = json.load(f) | |
| return [entry for entry in history if isinstance(entry, dict) and "path" in entry and "timestamp" in entry] | |
| except (json.JSONDecodeError, IOError): | |
| return [] | |
| return [] | |
| def save_project_history(self): | |
| history_file = os.path.join(self.get_preset_dir(), "project_history.json") | |
| os.makedirs(os.path.dirname(history_file), exist_ok=True) | |
| unique_history = {entry["path"]: entry for entry in self.project_history}.values() | |
| self.project_history = sorted(unique_history, key=lambda x: x["timestamp"], reverse=True)[:10] | |
| try: | |
| with open(history_file, 'w') as f: | |
| json.dump(self.project_history, f, indent=4) | |
| except IOError as e: | |
| self.status_bar.setText(f"Error saving project history: {str(e)}") | |
| def load_window_state(self): | |
| state_file = os.path.join(self.get_preset_dir(), "window_state.json") | |
| if os.path.exists(state_file): | |
| try: | |
| with open(state_file, 'r') as f: | |
| state = json.load(f) | |
| self.window_width = state.get("window_width", 600) | |
| self.window_height = state.get("window_height", 900) | |
| self.scene_ref_list_height = state.get("scene_ref_list_height", 200) | |
| self.ref_list_height = state.get("ref_list_height", 200) | |
| self.browser_list_height = state.get("browser_list_height", 200) | |
| self.show_namespace = state.get("show_namespace", False) | |
| self.show_full_path = state.get("show_full_path", False) | |
| self.clear_ref_list = state.get("clear_ref_list", False) | |
| self.append_preset_to_scene = state.get("append_preset_to_scene", False) | |
| self.scene_selection_enabled = state.get("scene_selection_enabled", True) | |
| self.scene_sort_dropdown_text = state.get("scene_sort_mode", "Asset") | |
| self.ref_sort_dropdown_text = state.get("ref_sort_mode", "Asset") | |
| except (json.JSONDecodeError, IOError): | |
| self.window_width = 600 | |
| self.window_height = 900 | |
| self.scene_ref_list_height = 200 | |
| self.ref_list_height = 200 | |
| self.browser_list_height = 200 | |
| self.show_namespace = False | |
| self.show_full_path = False | |
| self.clear_ref_list = False | |
| self.append_preset_to_scene = False | |
| self.scene_selection_enabled = True | |
| self.scene_sort_dropdown_text = "Asset" | |
| self.ref_sort_dropdown_text = "Asset" | |
| else: | |
| self.window_width = 600 | |
| self.window_height = 900 | |
| self.scene_ref_list_height = 200 | |
| self.ref_list_height = 200 | |
| self.browser_list_height = 200 | |
| self.show_namespace = False | |
| self.show_full_path = False | |
| self.clear_ref_list = False | |
| self.append_preset_to_scene = False | |
| self.scene_selection_enabled = True | |
| self.scene_sort_dropdown_text = "Asset" | |
| self.ref_sort_dropdown_text = "Asset" | |
| def save_window_state(self): | |
| state_file = os.path.join(self.get_preset_dir(), "window_state.json") | |
| os.makedirs(os.path.dirname(state_file), exist_ok=True) | |
| state = { | |
| "window_width": self.width(), | |
| "window_height": self.height(), | |
| "scene_ref_list_height": self.scene_ref_list_height, | |
| "ref_list_height": self.ref_list_height, | |
| "browser_list_height": self.browser_list_height, | |
| "show_namespace": self.namespace_checkbox.isChecked(), | |
| "show_full_path": self.path_checkbox.isChecked(), | |
| "clear_ref_list": self.clear_ref_list_checkbox.isChecked(), | |
| "append_preset_to_scene": self.append_preset_to_scene_checkbox.isChecked(), | |
| "scene_selection_enabled": self.scene_selection_checkbox.isChecked(), | |
| "scene_sort_mode": self.scene_sort_dropdown.currentText(), | |
| "ref_sort_mode": self.ref_sort_dropdown.currentText() | |
| } | |
| try: | |
| with open(state_file, 'w') as f: | |
| json.dump(state, f, indent=4) | |
| self.status_bar.setText(f"Saved window state to {state_file}") | |
| except IOError as e: | |
| self.status_bar.setText(f"Error saving window state: {str(e)}") | |
| def update_remove_button_state(self): | |
| self.remove_from_ref_btn.setEnabled(True) | |
| if __name__ == "__main__": | |
| try: | |
| project_dir = "O:/productions/inviz/SKTL/00_cg" | |
| if cmds.about(batch=True): | |
| raise RuntimeError("Cannot run GUI in batch mode") | |
| for widget in QtWidgets.QApplication.allWidgets(): | |
| if isinstance(widget, MassiveReferenceVV): | |
| widget.close() | |
| app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([]) | |
| tool = MassiveReferenceVV(project_dir) | |
| tool.show() | |
| except Exception as e: | |
| print(f"Error launching tool: {str(e)}") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment