Skip to content

Instantly share code, notes, and snippets.

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

  • Save Pakmanv/1fef0bcd051d7a6ecd697686c3065a52 to your computer and use it in GitHub Desktop.

Select an option

Save Pakmanv/1fef0bcd051d7a6ecd697686c3065a52 to your computer and use it in GitHub Desktop.
Asset File Manager VV 1.9.7
# -*- coding: utf-8 -*-
import os
import glob
import json
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.9.7" # Updated to 1.9.7 to reflect "always on top" enforcement
class AssetFileManagerVV(QtWidgets.QDialog):
def clean_file_path(self, file_path):
"""Remove {N} Suffix from file path."""
return re.sub(r'\{[0-9]+\}', '', file_path)
def cache_assets(self):
"""Cache .ma, .mb, and .fbx files in the project directory."""
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 cache_folder_assets(self, folder_path):
"""Cache .ma, .mb, and .fbx files in the specified folder."""
ma_files = glob.glob(os.path.join(folder_path, "*.ma"))
mb_files = glob.glob(os.path.join(folder_path, "*.mb"))
fbx_files = glob.glob(os.path.join(folder_path, "*.fbx"))
return ma_files + mb_files + fbx_files
def __init__(self, project_dir):
super(AssetFileManagerVV, self).__init__()
# NEW: Set window to always stay on top of all windows
self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)
self.project_dir = project_dir
try:
self.full_asset_cache = self.cache_assets()
self.asset_cache = self.full_asset_cache[:]
except Exception:
self.full_asset_cache = []
self.asset_cache = []
self.current_folder = None
self.import_items = {} # Now stores {"item": QListWidgetItem, "color": str}
self.bookmarks = self.load_bookmarks()
self.show_full_path = False
self.import_sort_mode = "asset"
self.group_colors = [
"#FF9999", # Pastel red
"#CCFFCC", # Pastel green
"#ADD8E6", # Pastel blue
"#D9B3FF", # Pastel purple
"#FFFF99", # Pastel yellow
"#FFB3E6", # Pastel pink
]
self.import_list_height = 200
self.browser_list_height = 240
try:
self.project_history = self.load_project_history()
except Exception:
self.project_history = []
self.clear_after_import = False
self.disable_delete_prompt = False
self.incremental_save = False
try:
self.load_window_state()
except Exception:
pass
try:
self.setup_ui()
except Exception:
raise
self.initialize_import_list()
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 validate_file(self, file_path):
"""Validate file before importing."""
try:
if not os.path.exists(file_path):
return False, "File does not exist"
if not os.access(file_path, os.R_OK):
return False, "No read permission"
file_size = os.path.getsize(file_path)
if file_size == 0:
return False, "File is empty"
if file_path.endswith(".ma"):
with open(file_path, 'r', encoding='utf-8') as f:
first_line = f.readline().strip()
if not first_line.startswith("//Maya ASCII"):
return False, "Invalid Maya ASCII file format"
return True, ""
except (IOError, UnicodeDecodeError) as e:
return False, f"Error reading file: {str(e)}"
def setup_ui(self):
self.setWindowTitle(f"Asset File Manager VV {SCRIPT_VERSION}")
self.resize(self.window_width, self.window_height)
layout = QtWidgets.QVBoxLayout()
# Menu Bar
menu_bar = QtWidgets.QMenuBar(self)
menu_bar.setContextMenuPolicy(QtCore.Qt.NoContextMenu)
tools_menu = menu_bar.addMenu("Tools")
tools_menu.setContextMenuPolicy(QtCore.Qt.NoContextMenu)
refresh_ui_action = QtWidgets.QAction("Refresh UI", self)
refresh_ui_action.triggered.connect(self.refresh_ui)
import_action = QtWidgets.QAction("Import Options", self)
import_action.triggered.connect(self.open_import_options)
save_state_action = QtWidgets.QAction("Save Window State", self)
save_state_action.triggered.connect(self.save_window_state)
tools_menu.addAction(refresh_ui_action)
tools_menu.addAction(import_action)
tools_menu.addAction(save_state_action)
layout.addWidget(menu_bar)
# Project Directory
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)
# Session Path Field
session_path_layout = QtWidgets.QHBoxLayout()
session_path_layout.addWidget(QtWidgets.QLabel("Session Path:"))
self.session_path_field = QtWidgets.QLineEdit()
self.session_path_field.setReadOnly(True)
self.session_path_field.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.session_path_field.customContextMenuRequested.connect(self.show_session_path_context_menu)
self.session_path_field.mouseDoubleClickEvent = self.on_session_path_double_click
self.session_path_field.setStyleSheet("color: #888888;") # Changed to brighter grey
self.update_session_path_field()
session_path_layout.addWidget(self.session_path_field)
layout.addLayout(session_path_layout)
# Line Spacer Between Session Path and Current File
line = QtWidgets.QFrame()
line.setFrameShape(QtWidgets.QFrame.HLine)
line.setFrameShadow(QtWidgets.QFrame.Sunken)
layout.addWidget(line)
# Asset Browser Header
browser_header_layout = QtWidgets.QHBoxLayout()
browser_header_layout.addWidget(QtWidgets.QLabel("Asset Browser:"))
self.assets_found_label = QtWidgets.QLabel("Assets Found: 0")
self.assets_found_label.setStyleSheet("font-size: 10px; color: gray;")
browser_header_layout.addStretch()
browser_header_layout.addWidget(self.assets_found_label)
layout.addLayout(browser_header_layout)
# Search Bar
self.search_bar = QtWidgets.QLineEdit()
self.search_bar.setPlaceholderText("e.g., aaron v0408, tv, *sigma*")
self.search_bar.textChanged.connect(self.update_browser)
layout.addWidget(self.search_bar)
# Current File Field (Below Search Bar)
current_file_layout = QtWidgets.QHBoxLayout()
current_file_layout.addWidget(QtWidgets.QLabel("Current File:"))
self.current_file_field = QtWidgets.QLineEdit()
self.current_file_field.setReadOnly(True)
self.current_file_field.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.current_file_field.customContextMenuRequested.connect(self.show_current_file_context_menu)
self.current_file_field.mouseDoubleClickEvent = self.on_current_file_double_click
self.update_current_file_field()
current_file_layout.addWidget(self.current_file_field)
layout.addLayout(current_file_layout)
# Show Full Path Checkbox (Above Browser List)
path_layout = QtWidgets.QHBoxLayout()
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)
path_layout.addWidget(self.path_checkbox)
# Directory Indicator Label
self.directory_indicator = QtWidgets.QLabel("You are inside a directory, right click to go back")
self.directory_indicator.setStyleSheet("color: yellow; font-size: 10px;")
path_layout.addWidget(self.directory_indicator)
path_layout.addStretch()
layout.addLayout(path_layout)
# Asset Browser List
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.doubleClicked.connect(self.on_browser_double_click)
self.browser_list.itemSelectionChanged.connect(self.update_current_file_field_on_selection)
self.browser_list.setFixedHeight(self.browser_list_height)
self.browser_list.setFont(QtGui.QFont()) # Default font
self.browser_list.setStyleSheet("QListWidget::item:selected { background-color: palette(highlight); }")
layout.addWidget(self.browser_list)
# Double-Click Note
browser_note = QtWidgets.QLabel("Note: Double-click a file to list assets in its directory.")
browser_note.setStyleSheet("font-size: 10px; font-style: italic; color: gray;")
layout.addWidget(browser_note)
# Browser Actions
btn_layout = QtWidgets.QHBoxLayout()
self.new_scene_btn = QtWidgets.QPushButton("New Scene")
self.new_scene_btn.setStyleSheet("QPushButton { background-color: #ADD8E6; color: black; }")
self.new_scene_btn.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
self.new_scene_btn.clicked.connect(self.new_scene)
btn_layout.addWidget(self.new_scene_btn)
self.open_scene_btn = QtWidgets.QPushButton("Open Scene (Right Click for More Options)")
self.open_scene_btn.setStyleSheet("QPushButton { background-color: #D9B3FF; color: black; }")
self.open_scene_btn.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
self.open_scene_btn.clicked.connect(self.open_scene)
self.open_scene_btn.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.open_scene_btn.customContextMenuRequested.connect(self.show_open_scene_context_menu)
btn_layout.addWidget(self.open_scene_btn)
self.save_scene_btn = QtWidgets.QPushButton("Save (Right Click for More Options)")
self.save_scene_btn.setStyleSheet("QPushButton { background-color: #CCFFCC; color: black; }")
self.save_scene_btn.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
self.save_scene_btn.clicked.connect(self.save_scene)
self.save_scene_btn.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.save_scene_btn.customContextMenuRequested.connect(self.show_save_context_menu)
btn_layout.addWidget(self.save_scene_btn)
self.add_to_import_btn = QtWidgets.QPushButton("Add Assignment")
self.add_to_import_btn.setStyleSheet("QPushButton { background-color: #FFCC99; color: black; }")
self.add_to_import_btn.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
self.add_to_import_btn.clicked.connect(self.add_to_import_list)
btn_layout.addWidget(self.add_to_import_btn)
layout.addLayout(btn_layout)
# Assignments Header
import_header_layout = QtWidgets.QHBoxLayout()
import_header_layout.addWidget(QtWidgets.QLabel("Assignment(s):"))
import_header_layout.addStretch()
sort_label = QtWidgets.QLabel("Sort by:")
self.import_sort_dropdown = QtWidgets.QComboBox()
self.import_sort_dropdown.addItems(["Alphabetical A-Z", "Alphabetical Z-A", "Version", "Asset"])
self.import_sort_dropdown.setCurrentText(self.import_sort_dropdown_text)
self.import_sort_dropdown.currentTextChanged.connect(self.on_import_sort_changed)
self.import_sort_dropdown.setMinimumWidth(150)
import_header_layout.addWidget(sort_label)
import_header_layout.addWidget(self.import_sort_dropdown)
self.bookmark_dropdown = QtWidgets.QComboBox()
self.bookmark_dropdown.addItem("Bookmarks")
self.update_bookmark_dropdown()
self.bookmark_dropdown.setMinimumWidth(300)
self.bookmark_dropdown.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.bookmark_dropdown.currentIndexChanged.connect(self.on_bookmark_selected)
self.bookmark_dropdown.customContextMenuRequested.connect(self.show_bookmark_context_menu)
import_header_layout.addWidget(self.bookmark_dropdown)
self.assets_registered_label = QtWidgets.QLabel("Asset(s) Registered: 0")
self.assets_registered_label.setStyleSheet("font-size: 10px; color: gray;")
import_header_layout.addWidget(self.assets_registered_label)
layout.addLayout(import_header_layout)
# Assignments List
self.import_list = QtWidgets.QListWidget()
self.import_list.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.import_list.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.import_list.customContextMenuRequested.connect(self.show_import_list_context_menu)
self.import_list.doubleClicked.connect(self.on_import_list_double_click)
self.import_list.setFixedHeight(self.import_list_height)
self.import_list.setFont(QtGui.QFont()) # Default font
self.import_list.setStyleSheet("QListWidget::item:selected { background-color: palette(highlight); }")
layout.addWidget(self.import_list)
# Import Settings Note
import_note = QtWidgets.QLabel("Note: Namespace and resolve settings must be configured in Maya’s global import settings.")
import_note.setStyleSheet("font-size: 10px; font-style: italic; color: gray;")
layout.addWidget(import_note)
# Clear Import List Checkbox
clear_layout = QtWidgets.QHBoxLayout()
self.clear_after_import_checkbox = QtWidgets.QCheckBox("Clear Item(s) After Import")
self.clear_after_import_checkbox.setChecked(self.clear_after_import)
self.clear_after_import_checkbox.stateChanged.connect(self.update_clear_after_import)
clear_layout.addWidget(self.clear_after_import_checkbox)
clear_layout.addStretch()
layout.addLayout(clear_layout)
# Import Actions
import_action_layout = QtWidgets.QHBoxLayout()
self.import_btn = QtWidgets.QPushButton("Import Selections (Right click to import all)")
self.import_btn.setStyleSheet("QPushButton { background-color: #FFCC99; color: black; }")
self.import_btn.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
self.import_btn.clicked.connect(self.import_selections)
self.import_btn.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.import_btn.customContextMenuRequested.connect(self.show_import_context_menu)
import_action_layout.addWidget(self.import_btn)
self.remove_from_import_btn = QtWidgets.QPushButton("Remove from Assignment List")
self.remove_from_import_btn.setStyleSheet("QPushButton { background-color: #FF9999; color: black; }")
self.remove_from_import_btn.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
self.remove_from_import_btn.clicked.connect(self.remove_from_import_list)
self.remove_from_import_btn.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.remove_from_import_btn.customContextMenuRequested.connect(self.show_remove_from_import_context_menu)
import_action_layout.addWidget(self.remove_from_import_btn)
layout.addLayout(import_action_layout)
# Status Bar
self.status_bar = QtWidgets.QLabel("Ready")
layout.addWidget(self.status_bar)
# Disclaimer
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()
self.update_directory_indicator()
def update_directory_indicator(self):
"""Show or hide directory indicator based on current_folder."""
self.directory_indicator.setVisible(bool(self.current_folder))
def update_clear_after_import(self):
"""Update clear_after_import state and save it."""
self.clear_after_import = self.clear_after_import_checkbox.isChecked()
self.save_window_state()
def refresh_ui(self):
"""Refresh all UI lists to reflect current state."""
self.update_import_list_display()
self.update_browser()
self.update_current_file_field()
self.update_session_path_field()
self.update_directory_indicator()
self.status_bar.setText("UI refreshed.")
def open_import_options(self):
"""Open Maya's Import Options dialog."""
try:
mel.eval('ImportOptions; fileOptions2 "Import" "projectViewer Import" false; fo_changeRadioCollection ("Import");')
self.status_bar.setText("Opened Import Options dialog")
except RuntimeError as e:
self.status_bar.setText(f"Error opening Import Options: {str(e)}")
def cleanup_popup_menus(self):
"""Clean up temporary popup menus."""
try:
mel.eval('MarkingMenuPopDown; if (`popupMenu -exists tempMM`) { deleteUI tempMM; } if (`popupMenu -exists tempMM2`) { deleteUI tempMM2; };')
except RuntimeError as e:
self.status_bar.setText(f"Error cleaning up popup menus: {str(e)}")
def show_open_scene_context_menu(self, pos):
selected = self.browser_list.selectedItems()
menu = QtWidgets.QMenu(self)
open_current_action = menu.addAction("Open in Current Session")
open_new_action = menu.addAction("Open in New Session")
open_current_action.setEnabled(len(selected) == 1)
open_new_action.setEnabled(len(selected) == 1)
action = menu.exec_(self.open_scene_btn.mapToGlobal(pos))
if action == open_current_action and len(selected) == 1:
file_path = next((asset for asset in self.asset_cache if os.path.basename(asset) == selected[0].text() or asset == selected[0].text()), None)
if file_path:
self.open_scene(file_path)
elif action == open_new_action and len(selected) == 1:
file_path = next((asset for asset in self.asset_cache if os.path.basename(asset) == selected[0].text() or asset == selected[0].text()), None)
if file_path:
self.open_scene_new_session(file_path)
self.cleanup_popup_menus()
def show_import_context_menu(self, pos):
self.import_list.clearSelection()
self.import_btn.setFocus()
menu = QtWidgets.QMenu(self)
import_all_action = menu.addAction("Import All")
import_all_action.setIcon(self.create_colored_icon("#FFCC99"))
action = menu.exec_(self.import_btn.mapToGlobal(pos))
if action == import_all_action:
self.import_all()
self.cleanup_popup_menus()
def show_remove_from_import_context_menu(self, pos):
menu = QtWidgets.QMenu(self)
clear_all_action = menu.addAction("Clear All Assignments")
action = menu.exec_(self.remove_from_import_btn.mapToGlobal(pos))
if action == clear_all_action:
self.clear_all_import_list()
self.cleanup_popup_menus()
def show_save_context_menu(self, pos):
menu = QtWidgets.QMenu(self)
save_as_action = menu.addAction("Save As")
version_up_action = menu.addAction("Version Up")
incremental_action = QtWidgets.QWidgetAction(self)
incremental_checkbox = QtWidgets.QCheckBox("Incremental Save")
incremental_checkbox.setChecked(self.incremental_save)
incremental_checkbox.stateChanged.connect(self.update_incremental_save)
incremental_action.setDefaultWidget(incremental_checkbox)
menu.addAction(incremental_action)
action = menu.exec_(self.save_scene_btn.mapToGlobal(pos))
if action == save_as_action:
self.save_scene_as()
elif action == version_up_action:
self.version_up_scene()
self.cleanup_popup_menus()
def update_incremental_save(self, state):
"""Update incremental_save state."""
self.incremental_save = bool(state)
self.save_window_state()
def show_current_file_context_menu(self, pos):
"""Show context menu for current file field with navigation option."""
menu = QtWidgets.QMenu(self)
open_location_action = menu.addAction("Open Directory Location")
navigate_browser_action = menu.addAction("Navigate Asset Browser to Folder")
current_file = self.current_file_field.text()
is_valid_path = bool(current_file and current_file != "Untitled")
open_location_action.setEnabled(is_valid_path)
navigate_browser_action.setEnabled(is_valid_path)
action = menu.exec_(self.current_file_field.mapToGlobal(pos))
if action == open_location_action and is_valid_path:
self.open_current_file_location()
elif action == navigate_browser_action and is_valid_path:
self.navigate_browser_to_current_file()
self.cleanup_popup_menus()
def show_session_path_context_menu(self, pos):
"""Show context menu for session path field with navigation options."""
menu = QtWidgets.QMenu(self)
open_location_action = menu.addAction("Open File Directory")
navigate_browser_action = menu.addAction("Navigate Asset Browser to Folder")
session_path = self.session_path_field.text()
# Validate path: not "Untitled" and exists (file or directory)
full_path = os.path.join(self.project_dir, session_path) if self.project_dir and not os.path.isabs(session_path) else session_path
is_valid_path = bool(session_path and session_path != "Untitled" and os.path.exists(full_path))
open_location_action.setEnabled(is_valid_path)
navigate_browser_action.setEnabled(is_valid_path)
action = menu.exec_(self.session_path_field.mapToGlobal(pos))
if action == open_location_action and is_valid_path:
self.open_session_path_location()
elif action == navigate_browser_action and is_valid_path:
self.navigate_browser_to_session_path()
self.cleanup_popup_menus()
def show_import_list_context_menu(self, pos):
item_at_pos = self.import_list.itemAt(pos)
if item_at_pos and item_at_pos not in self.import_list.selectedItems():
self.import_list.clearSelection()
item_at_pos.setSelected(True)
selected_items = self.import_list.selectedItems()
menu = QtWidgets.QMenu(self)
remove_action = menu.addAction("Remove from Assignment List")
rename_action = menu.addAction("Rename File")
open_current_action = menu.addAction("Open in Current Session")
open_new_action = menu.addAction("Open in New Session")
open_location_action = menu.addAction("Open Source Location")
menu.addSeparator()
save_json_action = menu.addAction("Save Assignments to JSON")
load_json_action = menu.addAction("Load Assignments from JSON")
menu.addSeparator()
color_menu = menu.addMenu("Set Text Color")
red_action = color_menu.addAction("Red")
yellow_action = color_menu.addAction("Yellow")
green_action = color_menu.addAction("Green")
white_action = color_menu.addAction("White")
menu.addSeparator()
taller_action = menu.addAction("Make Taller")
shorter_action = menu.addAction("Make Shorter")
taller_action.setEnabled(self.import_list_height < 400)
shorter_action.setEnabled(self.import_list_height > 100)
action = menu.exec_(self.import_list.mapToGlobal(pos))
if action == remove_action:
self.remove_from_import_list()
elif action == rename_action and len(selected_items) == 1:
file_path = next((path for path, info in self.import_items.items() if info["item"] == selected_items[0]), None)
if file_path:
self.rename_file(file_path, from_import_list=True)
elif action == open_current_action and len(selected_items) == 1:
file_path = next((path for path, info in self.import_items.items() if info["item"] == selected_items[0]), None)
if file_path:
self.open_scene(file_path)
elif action == open_new_action and len(selected_items) == 1:
file_path = next((path for path, info in self.import_items.items() if info["item"] == selected_items[0]), None)
if file_path:
self.open_scene_new_session(file_path)
elif action == open_location_action and len(selected_items) == 1:
file_path = next((path for path, info in self.import_items.items() if info["item"] == selected_items[0]), None)
if file_path:
self.open_source_location(file_path)
elif action == save_json_action:
self.save_assignments_to_json()
elif action == load_json_action:
self.load_assignments_from_json()
elif action in (red_action, yellow_action, green_action, white_action):
color = {"Red": "red", "Yellow": "yellow", "Green": "green", "White": "white"}[action.text()]
self.set_import_item_color(selected_items, color)
elif action == taller_action:
self.import_list_height = min(self.import_list_height + 50, 400)
self.import_list.setFixedHeight(self.import_list_height)
self.import_list.updateGeometry()
self.status_bar.setText(f"Assignments list height set to {self.import_list_height}px")
elif action == shorter_action:
self.import_list_height = max(self.import_list_height - 50, 100)
self.import_list.setFixedHeight(self.import_list_height)
self.import_list.updateGeometry()
self.status_bar.setText(f"Assignments list height set to {self.import_list_height}px")
self.cleanup_popup_menus()
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())
self.cleanup_popup_menus()
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)
go_back_action = menu.addAction("Go Back")
rename_action = menu.addAction("Rename File")
info_action = menu.addAction("Show Asset Info")
open_source_action = menu.addAction("Open Source")
open_location_action = menu.addAction("Open Source Location")
delete_action = menu.addAction("Delete File")
disable_prompt_action = QtWidgets.QWidgetAction(self)
disable_prompt_checkbox = QtWidgets.QCheckBox("Disable Delete Prompt")
disable_prompt_checkbox.setChecked(not self.disable_delete_prompt)
disable_prompt_checkbox.stateChanged.connect(self.update_delete_prompt_state)
disable_prompt_action.setDefaultWidget(disable_prompt_checkbox)
menu.addAction(disable_prompt_action)
menu.addSeparator()
refresh_action = menu.addAction("Refresh List")
menu.addSeparator()
taller_action = menu.addAction("Make Taller")
shorter_action = menu.addAction("Make Shorter")
is_single_file = len(selected_items) == 1
if is_single_file:
file_path = next((asset for asset in self.asset_cache if os.path.basename(asset) == selected_items[0].text() or asset == selected[0].text()), None)
is_single_file = file_path and os.path.isfile(self.clean_file_path(file_path))
rename_action.setEnabled(is_single_file)
delete_action.setEnabled(len(selected_items) > 0)
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)
go_back_action.setEnabled(self.current_folder is not None)
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 == go_back_action and self.current_folder is not None:
self.current_folder = None
self.asset_cache = self.full_asset_cache[:]
self.update_browser()
self.update_directory_indicator()
self.bookmark_dropdown.setCurrentIndex(0)
self.status_bar.setText("Returned to full asset list.")
elif action == rename_action and is_single_file:
file_path = next((asset for asset in self.asset_cache if os.path.basename(asset) == selected_items[0].text() or asset == selected[0].text()), None)
if file_path:
self.rename_file(file_path)
elif action == delete_action and selected_items:
self.delete_selected_files(selected_items)
elif 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[0].text()), None)
if file_path:
self.open_scene(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[0].text()), None)
if file_path:
self.open_source_location(file_path)
elif action == refresh_action:
self.full_asset_cache = self.cache_assets()
if self.current_folder:
self.asset_cache = self.cache_folder_assets(self.current_folder)
else:
self.asset_cache = self.full_asset_cache[:]
self.update_browser()
self.update_directory_indicator()
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")
self.cleanup_popup_menus()
def show_bookmark_context_menu(self, pos):
menu = QtWidgets.QMenu(self)
add_bookmark_action = menu.addAction("Add Bookmark")
delete_bookmark_action = menu.addAction("Delete Current Bookmark")
current_index = self.bookmark_dropdown.currentIndex()
delete_bookmark_action.setEnabled(current_index > 0)
action = menu.exec_(self.bookmark_dropdown.mapToGlobal(pos))
if action == add_bookmark_action:
self.add_bookmark()
elif action == delete_bookmark_action and current_index > 0:
self.delete_bookmark(current_index)
self.cleanup_popup_menus()
def add_bookmark(self):
"""Prompt for bookmark name and add current path to bookmarks."""
if not self.current_folder:
QtWidgets.QMessageBox.warning(
self,
"Invalid Path",
"Please choose a valid path to bookmark.",
QtWidgets.QMessageBox.Ok
)
self.status_bar.setText("Bookmark cancelled: No valid path.")
return
name, ok = QtWidgets.QInputDialog.getText(
self,
"Add Bookmark",
"Enter name for bookmark:",
QtWidgets.QLineEdit.Normal,
os.path.basename(self.current_folder)
)
if ok and name:
self.bookmarks.append({"name": name, "path": self.current_folder})
self.save_bookmarks()
self.update_bookmark_dropdown()
for i in range(self.bookmark_dropdown.count()):
if self.bookmark_dropdown.itemText(i) == name:
self.bookmark_dropdown.setCurrentIndex(i)
break
self.status_bar.setText(f"Bookmarked {name}: {self.current_folder}")
def delete_bookmark(self, index):
"""Delete the bookmark at the given dropdown index."""
if 0 < index <= len(self.bookmarks):
bookmark = self.bookmarks.pop(index - 1)
self.save_bookmarks()
self.update_bookmark_dropdown()
self.bookmark_dropdown.setCurrentIndex(0)
self.status_bar.setText(f"Deleted bookmark: {bookmark['name']}")
def update_bookmark_dropdown(self):
"""Update the bookmark dropdown with current bookmarks."""
current_text = self.bookmark_dropdown.currentText()
self.bookmark_dropdown.clear()
self.bookmark_dropdown.addItem("Bookmarks")
for bookmark in self.bookmarks:
self.bookmark_dropdown.addItem(bookmark["name"], userData=bookmark["path"])
for i in range(self.bookmark_dropdown.count()):
if self.bookmark_dropdown.itemText(i) == current_text:
self.bookmark_dropdown.setCurrentIndex(i)
break
def on_bookmark_selected(self, index):
"""Navigate to the bookmarked path when selected."""
if index > 0:
path = self.bookmark_dropdown.itemData(index)
if os.path.exists(path):
self.current_folder = path
self.asset_cache = self.cache_folder_assets(path)
self.update_browser()
self.update_directory_indicator()
self.status_bar.setText(f"Navigated to bookmark: {self.bookmark_dropdown.currentText()}")
else:
self.status_bar.setText(f"Bookmark path not found: {path}")
self.bookmark_dropdown.setCurrentIndex(0)
else:
self.current_folder = None
self.asset_cache = self.full_asset_cache[:]
self.update_browser()
self.update_directory_indicator()
self.bookmark_dropdown.setCurrentIndex(0)
self.status_bar.setText("Returned to full asset list.")
def load_bookmarks(self):
"""Load bookmarks from JSON file."""
bookmark_file = os.path.join(os.path.expanduser("~"), "Documents", "maya_json", "bookmarks.json")
if os.path.exists(bookmark_file):
try:
with open(bookmark_file, 'r') as f:
bookmarks = json.load(f)
return [b for b in bookmarks if isinstance(b, dict) and "name" in b and "path" in b]
except (json.JSONDecodeError, IOError):
return []
return []
def save_bookmarks(self):
"""Save bookmarks to JSON file."""
bookmark_dir = os.path.join(os.path.expanduser("~"), "Documents", "maya_json")
bookmark_file = os.path.join(bookmark_dir, "bookmarks.json")
os.makedirs(bookmark_dir, exist_ok=True)
try:
with open(bookmark_file, 'w') as f:
json.dump(self.bookmarks, f, indent=4)
except IOError as e:
self.status_bar.setText(f"Error saving bookmarks: {str(e)}")
def save_assignments_to_json(self):
"""Save the assignments list to a JSON file using a file dialog."""
default_dir = os.path.join(os.path.expanduser("~"), "Documents", "maya_json")
os.makedirs(default_dir, exist_ok=True)
file_path, _ = QtWidgets.QFileDialog.getSaveFileName(
self,
"Save Assignments",
os.path.join(default_dir, "assignments.json"),
"JSON Files (*.json)"
)
if not file_path:
self.status_bar.setText("Save assignments cancelled.")
return
if not file_path.endswith(".json"):
file_path += ".json"
assignments = [
{"path": path, "color": info.get("color", "white")}
for path, info in self.import_items.items()
]
try:
with open(file_path, 'w') as f:
json.dump(assignments, f, indent=4)
self.status_bar.setText(f"Saved assignments to {file_path}")
except IOError as e:
self.status_bar.setText(f"Error saving assignments: {str(e)}")
def load_assignments_from_json(self):
"""Load assignments from a JSON file using a file dialog."""
default_dir = os.path.join(os.path.expanduser("~"), "Documents", "maya_json")
file_path, _ = QtWidgets.QFileDialog.getOpenFileName(
self,
"Load Assignments",
default_dir,
"JSON Files (*.json)"
)
if not file_path:
self.status_bar.setText("Load assignments cancelled.")
return
if not os.path.exists(file_path):
self.status_bar.setText(f"No assignments file found at {file_path}")
return
try:
with open(file_path, 'r') as f:
assignments = json.load(f)
added_count = 0
for assignment in assignments:
path = assignment.get("path")
color = assignment.get("color", "white")
if path and os.path.exists(path) and path not in self.import_items:
display_text = path if self.show_full_path else os.path.basename(path)
list_item = QtWidgets.QListWidgetItem(display_text)
list_item.setForeground(QtGui.QColor(color))
self.import_list.addItem(list_item)
self.import_items[path] = {"item": list_item, "color": color}
added_count += 1
self.update_import_list_display()
self.status_bar.setText(f"Loaded {added_count} assignments from {file_path}")
except (json.JSONDecodeError, IOError) as e:
self.status_bar.setText(f"Error loading assignments: {str(e)}")
def set_import_item_color(self, selected_items, color):
"""Set the text color for selected items in the assignments list."""
for item in selected_items:
file_path = next((path for path, info in self.import_items.items() if info["item"] == item), None)
if file_path:
self.import_items[file_path]["color"] = color
item.setForeground(QtGui.QColor(color))
self.status_bar.setText(f"Set color to {color} for {len(selected_items)} item(s)")
def open_session_path_location(self):
"""Open the directory of the session path field."""
session_path = self.session_path_field.text()
if session_path and session_path != "Untitled":
try:
full_path = os.path.join(self.project_dir, session_path) if self.project_dir and not os.path.isabs(session_path) else session_path
path = full_path
if not os.path.isdir(path):
path = os.path.dirname(path)
if not os.path.exists(path):
self.status_bar.setText(f"Directory not found: {path}")
return
os.startfile(path)
self.status_bar.setText(f"Opened directory: {path}")
except Exception as e:
self.status_bar.setText(f"Error opening directory: {str(e)}")
else:
self.status_bar.setText("No valid path to open.")
def navigate_browser_to_session_path(self):
"""Navigate the Asset Browser to the folder in the session path field."""
session_path = self.session_path_field.text()
if session_path and session_path != "Untitled":
full_path = os.path.join(self.project_dir, session_path) if self.project_dir and not os.path.isabs(session_path) else session_path
folder_path = full_path
if not os.path.isdir(full_path):
folder_path = os.path.dirname(full_path)
if not os.path.exists(folder_path):
self.status_bar.setText(f"Folder not found: {folder_path}")
return
self.current_folder = folder_path
self.asset_cache = self.cache_folder_assets(folder_path)
self.update_browser()
self.update_directory_indicator()
self.bookmark_dropdown.setCurrentIndex(0)
self.status_bar.setText(f"Navigated Asset Browser to: {folder_path}")
else:
self.status_bar.setText("No valid path to navigate.")
def navigate_browser_to_current_file(self):
"""Navigate the Asset Browser to the folder in the current file field."""
current_file = self.current_file_field.text()
if current_file and current_file != "Untitled":
full_path = os.path.join(self.project_dir, current_file) if self.project_dir and not os.path.isabs(current_file) else current_file
folder_path = full_path
if not os.path.isdir(full_path):
folder_path = os.path.dirname(full_path)
if not os.path.exists(folder_path):
self.status_bar.setText(f"Folder not found: {folder_path}")
return
self.current_folder = folder_path
self.asset_cache = self.cache_folder_assets(folder_path)
self.update_browser()
self.update_directory_indicator()
self.bookmark_dropdown.setCurrentIndex(0)
self.status_bar.setText(f"Navigated Asset Browser to: {folder_path}")
else:
self.status_bar.setText("No valid path to navigate.")
def on_session_path_double_click(self, event):
"""Handle double-click on session path field to navigate Asset Browser."""
self.navigate_browser_to_session_path()
QtWidgets.QLineEdit.mouseDoubleClickEvent(self.session_path_field, event)
def on_current_file_double_click(self, event):
"""Handle double-click on current file field to navigate Asset Browser."""
self.navigate_browser_to_current_file()
QtWidgets.QLineEdit.mouseDoubleClickEvent(self.current_file_field, event)
def update_delete_prompt_state(self, state):
"""Update the disable_delete_prompt state and save it."""
self.disable_delete_prompt = not bool(state)
self.save_window_state()
def delete_selected_files(self, selected_items):
"""Delete selected files with optional confirmation."""
current_scene = cmds.file(q=True, sn=True)
file_paths = []
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:
clean_file_path = self.clean_file_path(file_path)
if clean_file_path == current_scene:
QtWidgets.QMessageBox.warning(
self,
"Cannot Delete",
f"Cannot delete {os.path.basename(clean_file_path)} as it is the current open session.",
QtWidgets.QMessageBox.Ok
)
return
file_paths.append(clean_file_path)
if not file_paths:
self.status_bar.setText("No valid files selected to delete.")
return
if not self.disable_delete_prompt:
file_list = "\n".join(os.path.basename(fp) for fp in file_paths)
reply = QtWidgets.QMessageBox.warning(
self,
"Confirm Delete",
f"Are you sure you want to delete the following files?\n{file_list}\nThis action cannot be undone.",
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
QtWidgets.QMessageBox.No
)
if reply != QtWidgets.QMessageBox.Yes:
self.status_bar.setText("Delete cancelled.")
return
deleted_count = 0
for file_path in file_paths:
try:
os.remove(file_path)
deleted_count += 1
except OSError as e:
self.status_bar.setText(f"Error deleting {os.path.basename(file_path)}: {str(e)}")
continue
if deleted_count > 0:
self.full_asset_cache = self.cache_assets()
if self.current_folder:
self.asset_cache = self.cache_folder_assets(self.current_folder)
else:
self.asset_cache = self.full_asset_cache[:]
self.update_browser()
self.update_directory_indicator()
self.status_bar.setText(f"Deleted {deleted_count} file(s).")
else:
self.status_bar.setText("No files deleted due to errors.")
def rename_file(self, file_path, from_import_list=False):
"""Prompt to rename a file and update lists."""
clean_file_path = self.clean_file_path(file_path)
current_scene = cmds.file(q=True, sn=True)
if clean_file_path == current_scene:
QtWidgets.QMessageBox.warning(
self,
"Cannot Rename",
f"Cannot rename {os.path.basename(clean_file_path)} as it is the current open session.",
QtWidgets.QMessageBox.Ok
)
self.status_bar.setText("Rename cancelled: File is open session.")
return
old_name = os.path.basename(file_path)
old_ext = os.path.splitext(old_name)[1]
base_name = os.path.splitext(old_name)[0]
new_name, ok = QtWidgets.QInputDialog.getText(
self,
"Rename File",
f"Enter new name for {old_name} (extension {old_ext} will be preserved):",
QtWidgets.QLineEdit.Normal,
base_name
)
if not ok or not new_name:
self.status_bar.setText("Rename cancelled.")
return
new_name = re.sub(r'[^a-zA-Z0-9_.-]', '_', new_name.strip())
if not new_name:
self.status_bar.setText("Error: Invalid file name.")
return
new_file_path = os.path.join(os.path.dirname(file_path), f"{new_name}{old_ext}")
if os.path.exists(new_file_path):
self.status_bar.setText(f"Error: File {new_name}{old_ext} already exists.")
return
try:
os.rename(file_path, new_file_path)
self.full_asset_cache = self.cache_assets()
if self.current_folder:
self.asset_cache = self.cache_folder_assets(self.current_folder)
else:
self.asset_cache = self.full_asset_cache[:]
if from_import_list:
if file_path in self.import_items:
item_info = self.import_items.pop(file_path)
display_text = new_file_path if self.show_full_path else os.path.basename(new_file_path)
item_info["item"].setText(display_text)
self.import_items[new_file_path] = item_info
self.update_browser()
self.update_import_list_display()
self.update_session_path_field()
self.update_directory_indicator()
self.status_bar.setText(f"Renamed {old_name} to {new_name}{old_ext}.")
except OSError as e:
self.status_bar.setText(f"Error renaming {old_name}: {str(e)}")
def on_browser_double_click(self, index):
"""Handle double-click to navigate to the directory of the selected file or folder."""
selected = self.browser_list.selectedItems()
if len(selected) != 1:
self.status_bar.setText("Please select one asset or folder to navigate.")
return
file_path = next((asset for asset in self.asset_cache if os.path.basename(asset) == selected[0].text() or asset == selected[0].text()), None)
if not file_path:
self.status_bar.setText("Error: Selected asset not found.")
return
clean_file_path = self.clean_file_path(file_path)
folder_path = os.path.dirname(clean_file_path) if os.path.isfile(clean_file_path) else clean_file_path
if not os.path.exists(folder_path):
self.status_bar.setText(f"Error: Folder not found for {os.path.basename(clean_file_path)}.")
return
self.current_folder = folder_path
self.asset_cache = self.cache_folder_assets(folder_path)
self.update_browser()
self.update_directory_indicator()
self.bookmark_dropdown.setCurrentIndex(0)
self.status_bar.setText(f"Navigated to folder: {folder_path}")
def on_import_list_double_click(self, index):
"""Handle double-click on assignments list to navigate Asset Browser to item's directory."""
selected = self.import_list.selectedItems()
if len(selected) != 1:
self.status_bar.setText("Please select one assignment to navigate.")
return
file_path = next((path for path, info in self.import_items.items() if info["item"] == selected[0]), None)
if not file_path:
self.status_bar.setText("Error: Selected assignment not found.")
return
clean_file_path = self.clean_file_path(file_path)
if not os.path.exists(clean_file_path):
self.status_bar.setText(f"Error: File not found: {os.path.basename(clean_file_path)}")
return
folder_path = os.path.dirname(clean_file_path)
if not os.path.exists(folder_path):
self.status_bar.setText(f"Error: Folder not found for {os.path.basename(clean_file_path)}")
return
self.current_folder = folder_path
self.asset_cache = self.cache_folder_assets(folder_path)
self.update_browser()
self.update_directory_indicator()
self.bookmark_dropdown.setCurrentIndex(0)
self.status_bar.setText(f"Navigated to folder: {folder_path}")
def clear_all_import_list(self):
if not self.import_items:
self.status_bar.setText("Assignments list is already empty.")
return
self.import_list.clearSelection()
self.import_list.clear()
self.import_items.clear()
self.update_import_list_display()
self.status_bar.setText("Cleared all assignments from list.")
def update_dir_field(self):
self.dir_field.setText("Please select project directory" if not self.asset_cache else self.project_dir)
def update_current_file_field(self):
"""Update the current file field with the current scene's file path, using relative path if possible."""
current_file = cmds.file(q=True, sn=True)
if current_file and self.project_dir and current_file.startswith(self.project_dir):
relative_path = os.path.relpath(current_file, self.project_dir)
self.current_file_field.setText(relative_path)
else:
self.current_file_field.setText(current_file if current_file else "Untitled")
def update_current_file_field_on_selection(self):
"""Update the current file field based on browser list selection, using relative path if possible."""
selected = self.browser_list.selectedItems()
if len(selected) == 1:
file_path = next((asset for asset in self.asset_cache if os.path.basename(asset) == selected[0].text() or asset == selected[0].text()), None)
if file_path:
clean_file_path = self.clean_file_path(file_path)
if self.project_dir and clean_file_path.startswith(self.project_dir):
relative_path = os.path.relpath(clean_file_path, self.project_dir)
self.current_file_field.setText(relative_path)
else:
self.current_file_field.setText(clean_file_path)
return
self.update_current_file_field()
def update_session_path_field(self):
"""Update the session path field with the current scene's file path, using relative path if possible."""
current_file = cmds.file(q=True, sn=True)
if current_file and self.project_dir and current_file.startswith(self.project_dir):
relative_path = os.path.relpath(current_file, self.project_dir)
self.session_path_field.setText(relative_path)
else:
self.session_path_field.setText(current_file if current_file else "Untitled")
def open_current_file_location(self):
"""Open the directory of the current file field."""
current_file = self.current_file_field.text()
if current_file and current_file != "Untitled":
try:
full_path = os.path.join(self.project_dir, current_file) if self.project_dir and not os.path.isabs(current_file) else current_file
directory = os.path.dirname(full_path)
if not os.path.exists(directory):
self.status_bar.setText(f"Directory not found for {os.path.basename(current_file)}.")
return
os.startfile(directory)
self.status_bar.setText(f"Opened directory for {os.path.basename(current_file)}.")
except Exception as e:
self.status_bar.setText(f"Error opening directory: {str(e)}")
else:
self.status_bar.setText("No valid file path to open.")
def update_import_list_display(self):
self.import_list.clear()
import_items = []
for file_path in self.import_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
display_text = file_path if self.show_full_path else file_name
import_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
})
if self.import_sort_mode == "alphabetical_asc":
import_items.sort(key=lambda x: (x["sort_name"].lower(), x["version"], x["asset"]))
elif self.import_sort_mode == "alphabetical_desc":
import_items.sort(key=lambda x: (x["sort_name"].lower(), x["version"], x["asset"]), reverse=True)
elif self.import_sort_mode == "version":
import_items.sort(key=lambda x: (x["version"], x["name"], x["asset"]))
else: # "asset"
import_items.sort(key=lambda x: (x["asset"], x["name"], x["version"]))
for item_data in import_items:
list_item = QtWidgets.QListWidgetItem(item_data["text"])
color = self.import_items[item_data["file_path"]].get("color", "white")
list_item.setForeground(QtGui.QColor(color))
self.import_list.addItem(list_item)
self.import_items[item_data["file_path"]]["item"] = list_item
self.assets_registered_label.setText(f"Asset(s) Registered: {len(self.import_items)}")
self.update_remove_button_state()
def on_import_sort_changed(self, text):
sort_mapping = {
"Alphabetical A-Z": "alphabetical_asc",
"Alphabetical Z-A": "alphabetical_desc",
"Version": "version",
"Asset": "asset"
}
self.import_sort_mode = sort_mapping.get(text, "asset")
self.update_import_list_display()
self.save_window_state()
def remove_from_import_list(self):
selected = self.import_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.import_items.items() if info["item"] == item), None)
if file_path:
self.import_list.takeItem(self.import_list.row(item))
del self.import_items[file_path]
self.update_import_list_display()
self.status_bar.setText(f"Removed {len(selected)} assignments from list.")
def add_to_import_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.import_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.import_list.addItem(list_item)
self.import_items[file_path] = {"item": list_item, "color": "white"}
added_count += 1
self.update_import_list_display()
self.status_bar.setText(f"Added {added_count} assignments to list.")
def initialize_import_list(self):
self.import_items.clear()
self.import_list.clear()
self.assets_registered_label.setText("Asset(s) 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 {len(self.asset_cache)} assets{' in folder: ' + self.current_folder if self.current_folder else ''}.")
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}'{' in folder: ' + self.current_folder if self.current_folder else ''}.")
self.update_dir_field()
self.assets_found_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_import_list_display()
self.update_current_file_field()
self.update_session_path_field()
self.update_directory_indicator()
def import_selections(self):
selected = self.import_list.selectedItems()
if not selected:
self.status_bar.setText("No assignments selected to import.")
return
self.import_assets(selected)
def import_all(self):
if not self.import_items:
self.status_bar.setText("No assignments to import.")
return
selected = [self.import_list.item(i) for i in range(self.import_list.count())]
self.import_assets(selected)
def import_assets(self, selected_items):
imported_count = 0
for index, item in enumerate(selected_items):
file_path = next((path for path, info in self.import_items.items() if info["item"] == item), None)
if not file_path:
continue
clean_file_path = self.clean_file_path(file_path)
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.")
continue
is_valid, validation_error = self.validate_file(clean_file_path)
if not is_valid:
self.status_bar.setText(f"Error: {os.path.basename(clean_file_path)} - {validation_error}")
continue
try:
file_type = "mayaAscii" if clean_file_path.endswith(".ma") else "mayaBinary" if clean_file_path.endswith(".mb") else "FBX"
cmds.file(clean_file_path, i=True, type=file_type, preserveReferences=False, loadReferenceDepth="all")
imported_count += 1
try:
mel.eval('editMenuUpdate MayaWindow|mainEditMenu;')
except RuntimeError as e:
self.status_bar.setText(f"Error updating edit menu: {str(e)}")
except RuntimeError as e:
self.status_bar.setText(f"Error importing {os.path.basename(clean_file_path)}: {str(e)}")
continue
if imported_count > 0:
self.status_bar.setText(f"Imported {imported_count} assignment(s) into scene")
else:
self.status_bar.setText("No assignments imported due to errors.")
if self.clear_after_import:
if len(selected_items) < self.import_list.count():
for item in selected_items:
file_path = next((path for path, info in self.import_items.items() if info["item"] == item), None)
if file_path:
self.import_list.takeItem(self.import_list.row(item))
del self.import_items[file_path]
else:
self.clear_all_import_list()
self.update_import_list_display()
def new_scene(self):
"""Create a new Maya scene with confirmation if modified."""
if cmds.file(q=True, modified=True):
reply = QtWidgets.QMessageBox.warning(
self,
"Confirm New Scene",
"The current scene has unsaved changes. Creating a new scene will discard them. Continue?",
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
QtWidgets.QMessageBox.No
)
if reply != QtWidgets.QMessageBox.Yes:
self.status_bar.setText("New scene cancelled.")
return
try:
cmds.file(new=True, force=True)
self.update_current_file_field()
self.update_session_path_field()
self.status_bar.setText("Created new scene.")
except RuntimeError as e:
self.status_bar.setText(f"Error creating new scene: {str(e)}")
def save_scene(self):
"""Save the current scene to its existing file or prompt for a location, with incremental save if enabled."""
current_file = cmds.file(q=True, sn=True)
if current_file and os.path.exists(current_file):
if self.incremental_save:
directory = os.path.dirname(current_file)
file_name = os.path.basename(current_file)
ext = os.path.splitext(file_name)[1]
base_name = os.path.splitext(file_name)[0]
version_match = re.search(r'_v(\d+)$', base_name)
if version_match:
version_num = int(version_match.group(1))
new_version = f"{version_num + 1:03d}"
new_base_name = re.sub(r'_v\d+$', f'_v{new_version}', base_name)
else:
new_base_name = f"{base_name}_v001"
new_version = "001"
new_file = os.path.join(directory, f"{new_base_name}{ext}")
suffix = 0
while os.path.exists(new_file):
suffix += 1
new_file = os.path.join(directory, f"{new_base_name}_{suffix}{ext}")
try:
cmds.file(rename=new_file)
file_type = "mayaAscii" if ext == ".ma" else "mayaBinary"
cmds.file(save=True, type=file_type)
self.full_asset_cache = self.cache_assets()
if self.current_folder:
self.asset_cache = self.cache_folder_assets(self.current_folder)
else:
self.asset_cache = self.full_asset_cache[:]
self.update_browser()
self.update_current_file_field()
self.update_session_path_field()
self.update_directory_indicator()
self.status_bar.setText(f"Saved incrementally to {os.path.basename(new_file)}")
except RuntimeError as e:
self.status_bar.setText(f"Error saving incrementally: {str(e)}")
else:
try:
cmds.file(save=True, type="mayaAscii")
self.update_current_file_field()
self.update_session_path_field()
self.status_bar.setText(f"Saved scene to {os.path.basename(current_file)}")
except RuntimeError as e:
self.status_bar.setText(f"Error saving scene: {str(e)}")
else:
self.save_scene_as()
def save_scene_as(self):
"""Prompt for a new file path to save the scene."""
default_dir = self.project_dir if self.current_folder is None else self.current_folder
default_name = "untitled"
default_path = os.path.join(default_dir, f"{default_name}.ma")
file_path, _ = QtWidgets.QFileDialog.getSaveFileName(
self,
"Save Scene As",
default_path,
"Maya ASCII (*.ma);;Maya Binary (*.mb)"
)
if file_path:
if not file_path.endswith((".ma", ".mb")):
file_path += ".ma"
file_type = "mayaAscii" if file_path.endswith(".ma") else "mayaBinary"
try:
cmds.file(rename=file_path)
cmds.file(save=True, type=file_type)
self.full_asset_cache = self.cache_assets()
if self.current_folder:
self.asset_cache = self.cache_folder_assets(self.current_folder)
else:
self.asset_cache = self.full_asset_cache[:]
self.update_browser()
self.update_current_file_field()
self.update_session_path_field()
self.update_directory_indicator()
self.status_bar.setText(f"Saved scene as {os.path.basename(file_path)}")
except RuntimeError as e:
self.status_bar.setText(f"Error saving scene as {os.path.basename(file_path)}: {str(e)}")
def version_up_scene(self):
"""Version up the current scene by incrementing or adding a version number."""
current_file = cmds.file(q=True, sn=True)
if not current_file:
self.status_bar.setText("Error: Scene has no file path. Use Save As first.")
return
directory = os.path.dirname(current_file)
file_name = os.path.basename(current_file)
ext = os.path.splitext(file_name)[1]
base_name = os.path.splitext(file_name)[0]
version_match = re.search(r'_v(\d+)$', base_name)
if not version_match:
version_match = re.search(r'(\d+)$', base_name)
if version_match:
version_num = int(version_match.group(1))
new_version = f"{version_num + 1:03d}"
if '_v' in base_name:
new_base_name = re.sub(r'_v\d+$', f'_v{new_version}', base_name)
else:
new_base_name = re.sub(r'\d+$', new_version, base_name)
else:
new_base_name = f"{base_name}_v001"
new_version = "001"
new_file = os.path.join(directory, f"{new_base_name}{ext}")
suffix = 0
while os.path.exists(new_file):
suffix += 1
new_file = os.path.join(directory, f"{new_base_name}_{suffix}{ext}")
try:
cmds.file(rename=new_file)
file_type = "mayaAscii" if ext == ".ma" else "mayaBinary"
cmds.file(save=True, type=file_type)
self.full_asset_cache = self.cache_assets()
if self.current_folder:
self.asset_cache = self.cache_folder_assets(self.current_folder)
else:
self.asset_cache = self.full_asset_cache[:]
self.update_browser()
self.update_current_file_field()
self.update_session_path_field()
self.update_directory_indicator()
self.status_bar.setText(f"Versioned up scene to {os.path.basename(new_file)}")
except RuntimeError as e:
self.status_bar.setText(f"Error versioning up scene: {str(e)}")
def open_scene(self, file_path=None):
selected = self.browser_list.selectedItems() if not file_path else []
if not file_path and not selected:
self.status_bar.setText("No items selected to open.")
return
if not file_path and len(selected) > 1:
self.status_bar.setText("Please select only one asset to open.")
return
if not file_path:
file_path = next((asset for asset in self.asset_cache if os.path.basename(asset) == selected[0].text() or asset == selected[0].text()), None)
if file_path:
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.")
return
is_valid, validation_error = self.validate_file(clean_file_path)
if not is_valid:
self.status_bar.setText(f"Error: {os.path.basename(clean_file_path)} - {validation_error}")
return
reply = QtWidgets.QMessageBox.warning(
self,
"Confirm Open Scene",
f"Opening {os.path.basename(clean_file_path)} will replace the current scene. Save your work before proceeding. Continue?",
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
QtWidgets.QMessageBox.No
)
if reply != QtWidgets.QMessageBox.Yes:
self.status_bar.setText("Open scene cancelled.")
return
try:
cmds.file(clean_file_path, open=True, force=True)
try:
mel.eval('editMenuUpdate MayaWindow|mainEditMenu;')
except RuntimeError as e:
self.status_bar.setText(f"Error updating edit menu: {str(e)}")
self.update_current_file_field()
self.update_session_path_field()
self.update_directory_indicator()
self.status_bar.setText(f"Opened scene: {os.path.basename(clean_file_path)}")
except RuntimeError as e:
self.status_bar.setText(f"Error opening {os.path.basename(clean_file_path)}: {str(e)}")
def open_scene_new_session(self, file_path):
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.")
return
is_valid, validation_error = self.validate_file(clean_file_path)
if not is_valid:
self.status_bar.setText(f"Error: {os.path.basename(clean_file_path)} - {validation_error}")
return
try:
maya_exe = "maya.exe"
subprocess.Popen([maya_exe, clean_file_path], shell=False)
self.status_bar.setText(f"Launched new Maya session with {os.path.basename(clean_file_path)}")
except (subprocess.SubprocessError, FileNotFoundError) as e:
self.status_bar.setText(f"Error launching new Maya session: {str(e)}")
def open_source_location(self, file_path):
"""Open the directory of the given file path using the raw path without cleaning."""
try:
directory = os.path.dirname(file_path)
if not os.path.exists(directory):
self.status_bar.setText(f"Directory not found for {os.path.basename(file_path)}.")
return
os.startfile(directory)
self.status_bar.setText(f"Opened source location for {os.path.basename(file_path)}.")
except Exception as e:
self.status_bar.setText(f"Error opening location for {os.path.basename(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.full_asset_cache = self.cache_assets()
self.asset_cache = self.full_asset_cache[:]
self.current_folder = None
self.update_browser()
self.update_import_list_display()
self.update_current_file_field()
self.update_session_path_field()
self.update_directory_indicator()
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 load_project_history(self):
history_file = os.path.join(os.path.expanduser("~"), "Documents", "maya_json", "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):
"""Save project history to JSON file, keeping the 10 most recent entries."""
history_dir = os.path.join(os.path.expanduser("~"), "Documents", "maya_json")
history_file = os.path.join(history_dir, "project_history.json")
os.makedirs(history_dir, 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):
"""Load window state from JSON file."""
state_file = os.path.join(os.path.expanduser("~"), "Documents", "maya_json", "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", 800)
self.window_height = state.get("window_height", 600)
self.import_list_height = state.get("import_list_height", 200)
self.browser_list_height = state.get("browser_list_height", 240)
self.show_full_path = state.get("show_full_path", False)
self.import_sort_mode = state.get("import_sort_mode", "asset")
self.import_sort_dropdown_text = {
"alphabetical_asc": "Alphabetical A-Z",
"alphabetical_desc": "Alphabetical Z-A",
"version": "Version",
"asset": "Asset"
}.get(self.import_sort_mode, "Asset")
self.clear_after_import = state.get("clear_after_import", False)
self.disable_delete_prompt = state.get("disable_delete_prompt", False)
self.incremental_save = state.get("incremental_save", False)
# REMOVED: Loading of stay_on_top state to enforce always on top
except (json.JSONDecodeError, IOError):
pass
def save_window_state(self):
"""Save window state to JSON file."""
state_dir = os.path.join(os.path.expanduser("~"), "Documents", "maya_json")
state_file = os.path.join(state_dir, "window_state.json")
os.makedirs(state_dir, exist_ok=True)
state = {
"window_width": self.width(),
"window_height": self.height(),
"import_list_height": self.import_list_height,
"browser_list_height": self.browser_list_height,
"show_full_path": self.show_full_path,
"import_sort_mode": self.import_sort_mode,
"clear_after_import": self.clear_after_import,
"disable_delete_prompt": self.disable_delete_prompt,
"incremental_save": self.incremental_save,
# REMOVED: stay_on_top state to enforce always on top
}
try:
with open(state_file, 'w') as f:
json.dump(state, f, indent=4)
self.status_bar.setText("Window state saved.")
except IOError as e:
self.status_bar.setText(f"Error saving window state: {str(e)}")
def closeEvent(self, event):
"""Handle window close event and save state."""
self.save_window_state()
super(AssetFileManagerVV, self).closeEvent(event)
# Example usage to launch the tool
def launch_asset_file_manager():
"""Launch the Asset File Manager in Maya."""
project_dir = cmds.workspace(q=True, dir=True) or os.path.expanduser("~")
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)
window = AssetFileManagerVV(project_dir)
window.show()
app.exec_()
if __name__ == "__main__":
launch_asset_file_manager()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment