Skip to content

Instantly share code, notes, and snippets.

@Pakmanv
Created May 2, 2025 17:40
Show Gist options
  • Select an option

  • Save Pakmanv/5a4663f3209db206a9074ec9cfab83a4 to your computer and use it in GitHub Desktop.

Select an option

Save Pakmanv/5a4663f3209db206a9074ec9cfab83a4 to your computer and use it in GitHub Desktop.
Massive Reference VV 1.19
# -*- 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