cluster4npu/utils/folder_dialog.py
Mason ec940c3f2f Improve assets folder selection and fix macOS tkinter crash
- Replace tkinter with PyQt5 QFileDialog as primary folder selector to fix macOS crashes
- Add specialized assets_folder property handling in dashboard with validation
- Integrate improved folder dialog utility with ExactModelNode
- Provide detailed validation feedback and user-friendly tooltips
- Maintain backward compatibility with tkinter as fallback

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-14 11:26:23 +08:00

252 lines
7.8 KiB
Python

"""
Folder selection utilities using PyQt5 as primary, tkinter as fallback
"""
import os
def select_folder(title="Select Folder", initial_dir="", must_exist=True):
"""
Open a folder selection dialog using PyQt5 (preferred) or tkinter (fallback)
Args:
title (str): Dialog window title
initial_dir (str): Initial directory to open
must_exist (bool): Whether the folder must already exist
Returns:
str: Selected folder path, or empty string if cancelled
"""
# Try PyQt5 first (more reliable on macOS)
try:
from PyQt5.QtWidgets import QApplication, QFileDialog
import sys
# Create QApplication if it doesn't exist
app = QApplication.instance()
if app is None:
app = QApplication(sys.argv)
# Set initial directory
if not initial_dir:
initial_dir = os.getcwd()
elif not os.path.exists(initial_dir):
initial_dir = os.getcwd()
# Open folder selection dialog
folder_path = QFileDialog.getExistingDirectory(
None,
title,
initial_dir,
QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks
)
return folder_path if folder_path else ""
except ImportError:
print("PyQt5 not available, trying tkinter...")
# Fallback to tkinter
try:
import tkinter as tk
from tkinter import filedialog
# Create a root window but keep it hidden
root = tk.Tk()
root.withdraw() # Hide the main window
root.attributes('-topmost', True) # Bring dialog to front
# Set initial directory
if not initial_dir:
initial_dir = os.getcwd()
# Open folder selection dialog
folder_path = filedialog.askdirectory(
title=title,
initialdir=initial_dir,
mustexist=must_exist
)
# Destroy the root window
root.destroy()
return folder_path if folder_path else ""
except ImportError:
print("tkinter also not available")
return ""
except Exception as e:
print(f"Error opening tkinter folder dialog: {e}")
return ""
except Exception as e:
print(f"Error opening PyQt5 folder dialog: {e}")
return ""
def select_assets_folder(initial_dir=""):
"""
Specialized function for selecting Assets folder with validation
Args:
initial_dir (str): Initial directory to open
Returns:
dict: Result with 'path', 'valid', and 'message' keys
"""
folder_path = select_folder(
title="Select Assets Folder (containing Firmware/ and Models/)",
initial_dir=initial_dir
)
if not folder_path:
return {'path': '', 'valid': False, 'message': 'No folder selected'}
# Validate folder structure
validation_result = validate_assets_folder_structure(folder_path)
return {
'path': folder_path,
'valid': validation_result['valid'],
'message': validation_result['message'],
'details': validation_result.get('details', {})
}
def validate_assets_folder_structure(folder_path):
"""
Validate that a folder has the expected Assets structure
Expected structure:
Assets/
├── Firmware/
│ ├── KL520/
│ │ ├── fw_scpu.bin
│ │ └── fw_ncpu.bin
│ └── KL720/
│ ├── fw_scpu.bin
│ └── fw_ncpu.bin
└── Models/
├── KL520/
│ └── model.nef
└── KL720/
└── model.nef
Args:
folder_path (str): Path to validate
Returns:
dict: Validation result with 'valid', 'message', and 'details' keys
"""
if not os.path.exists(folder_path):
return {'valid': False, 'message': 'Folder does not exist'}
if not os.path.isdir(folder_path):
return {'valid': False, 'message': 'Path is not a directory'}
details = {}
issues = []
# Check for Firmware and Models folders
firmware_path = os.path.join(folder_path, 'Firmware')
models_path = os.path.join(folder_path, 'Models')
has_firmware = os.path.exists(firmware_path) and os.path.isdir(firmware_path)
has_models = os.path.exists(models_path) and os.path.isdir(models_path)
details['has_firmware_folder'] = has_firmware
details['has_models_folder'] = has_models
if not has_firmware:
issues.append("Missing 'Firmware' folder")
if not has_models:
issues.append("Missing 'Models' folder")
if not (has_firmware and has_models):
return {
'valid': False,
'message': f"Invalid folder structure: {', '.join(issues)}",
'details': details
}
# Check for series subfolders
expected_series = ['KL520', 'KL720', 'KL630', 'KL730', 'KL540']
firmware_series = []
models_series = []
try:
firmware_dirs = [d for d in os.listdir(firmware_path)
if os.path.isdir(os.path.join(firmware_path, d))]
firmware_series = [d for d in firmware_dirs if d in expected_series]
models_dirs = [d for d in os.listdir(models_path)
if os.path.isdir(os.path.join(models_path, d))]
models_series = [d for d in models_dirs if d in expected_series]
except Exception as e:
return {
'valid': False,
'message': f"Error reading folder contents: {e}",
'details': details
}
details['firmware_series'] = firmware_series
details['models_series'] = models_series
# Find common series (have both firmware and models)
common_series = list(set(firmware_series) & set(models_series))
details['available_series'] = common_series
if not common_series:
return {
'valid': False,
'message': "No series found with both firmware and models folders",
'details': details
}
# Check for actual files in series folders
series_with_files = []
for series in common_series:
has_files = False
# Check firmware files
fw_series_path = os.path.join(firmware_path, series)
if os.path.exists(fw_series_path):
fw_files = [f for f in os.listdir(fw_series_path)
if f.endswith('.bin')]
if fw_files:
has_files = True
# Check model files
model_series_path = os.path.join(models_path, series)
if os.path.exists(model_series_path):
model_files = [f for f in os.listdir(model_series_path)
if f.endswith('.nef')]
if model_files and has_files:
series_with_files.append(series)
details['series_with_files'] = series_with_files
if not series_with_files:
return {
'valid': False,
'message': "No series found with actual firmware and model files",
'details': details
}
return {
'valid': True,
'message': f"Valid Assets folder with {len(series_with_files)} series: {', '.join(series_with_files)}",
'details': details
}
# Example usage
if __name__ == "__main__":
print("Testing folder selection...")
# Test basic folder selection
folder = select_folder("Select any folder")
print(f"Selected: {folder}")
# Test Assets folder selection with validation
result = select_assets_folder()
print(f"Assets folder result: {result}")