388 lines
18 KiB
Python
388 lines
18 KiB
Python
import sys
|
|
import os
|
|
import shutil
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Optional, Tuple
|
|
|
|
from PySide6.QtWidgets import (
|
|
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton,
|
|
QFileDialog, QMessageBox, QGroupBox, QFormLayout, QSpinBox, QDialogButtonBox
|
|
)
|
|
from PySide6.QtCore import Qt, Slot
|
|
|
|
# Constants for bundled resource locations relative to app base
|
|
BUNDLED_CONFIG_SUBDIR_NAME = "config"
|
|
BUNDLED_PRESETS_SUBDIR_NAME = "Presets"
|
|
DEFAULT_USER_DATA_SUBDIR_NAME = "user_data" # For portable path attempt
|
|
|
|
# Files to copy from bundled config to user config
|
|
DEFAULT_CONFIG_FILES = [
|
|
"asset_type_definitions.json",
|
|
"file_type_definitions.json",
|
|
"llm_settings.json",
|
|
"suppliers.json"
|
|
]
|
|
# app_settings.json is NOT copied. user_settings.json is handled separately.
|
|
|
|
USER_SETTINGS_FILENAME = "user_settings.json"
|
|
PERSISTENT_PATH_MARKER_FILENAME = ".first_run_complete"
|
|
PERSISTENT_CONFIG_ROOT_STORAGE_FILENAME = "asset_processor_user_root.txt" # Stores USER_CHOSEN_PATH
|
|
|
|
APP_NAME = "AssetProcessor" # Used for AppData paths
|
|
|
|
def get_app_base_dir() -> Path:
|
|
"""Determines the base directory for the application (executable or script)."""
|
|
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
|
|
# Running in a PyInstaller bundle
|
|
return Path(sys._MEIPASS)
|
|
else:
|
|
# Running as a script
|
|
return Path(__file__).resolve().parent.parent # Assuming this file is in gui/ subdir
|
|
|
|
def get_os_specific_app_data_dir() -> Path:
|
|
"""Gets the OS-specific application data directory."""
|
|
if sys.platform == "win32":
|
|
path_str = os.getenv('APPDATA')
|
|
if path_str:
|
|
return Path(path_str) / APP_NAME
|
|
# Fallback if APPDATA is not set, though unlikely
|
|
return Path.home() / "AppData" / "Roaming" / APP_NAME
|
|
elif sys.platform == "darwin": # macOS
|
|
return Path.home() / "Library" / "Application Support" / APP_NAME
|
|
else: # Linux and other Unix-like
|
|
return Path.home() / ".config" / APP_NAME
|
|
|
|
class FirstTimeSetupDialog(QDialog):
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.setWindowTitle("Asset Processor - First-Time Setup")
|
|
self.setModal(True)
|
|
self.setMinimumWidth(600)
|
|
|
|
self.app_base_dir = get_app_base_dir()
|
|
self.user_chosen_path: Optional[Path] = None
|
|
|
|
self._init_ui()
|
|
self._propose_default_config_path()
|
|
|
|
def _init_ui(self):
|
|
main_layout = QVBoxLayout(self)
|
|
|
|
# Configuration Path Group
|
|
config_path_group = QGroupBox("Configuration Location")
|
|
config_path_layout = QVBoxLayout()
|
|
|
|
self.proposed_path_label = QLabel("Proposed default configuration path:")
|
|
config_path_layout.addWidget(self.proposed_path_label)
|
|
|
|
path_selection_layout = QHBoxLayout()
|
|
self.config_path_edit = QLineEdit()
|
|
self.config_path_edit.setReadOnly(False) # Allow editing, then validate
|
|
path_selection_layout.addWidget(self.config_path_edit)
|
|
|
|
browse_button = QPushButton("Browse...")
|
|
browse_button.clicked.connect(self._browse_config_path)
|
|
path_selection_layout.addWidget(browse_button)
|
|
config_path_layout.addLayout(path_selection_layout)
|
|
config_path_group.setLayout(config_path_layout)
|
|
main_layout.addWidget(config_path_group)
|
|
|
|
# User Settings Group
|
|
user_settings_group = QGroupBox("Initial User Settings")
|
|
user_settings_form_layout = QFormLayout()
|
|
|
|
self.output_base_dir_edit = QLineEdit()
|
|
output_base_dir_browse_button = QPushButton("Browse...")
|
|
output_base_dir_browse_button.clicked.connect(self._browse_output_base_dir)
|
|
output_base_dir_layout = QHBoxLayout()
|
|
output_base_dir_layout.addWidget(self.output_base_dir_edit)
|
|
output_base_dir_layout.addWidget(output_base_dir_browse_button)
|
|
user_settings_form_layout.addRow("Default Library Output Path:", output_base_dir_layout)
|
|
|
|
self.output_dir_pattern_edit = QLineEdit("[supplier]/[asset_category]/[asset_name]")
|
|
user_settings_form_layout.addRow("Asset Structure Pattern:", self.output_dir_pattern_edit)
|
|
|
|
self.output_format_16bit_primary_edit = QLineEdit("png")
|
|
user_settings_form_layout.addRow("Default 16-bit Output Format (Primary):", self.output_format_16bit_primary_edit)
|
|
|
|
self.output_format_8bit_edit = QLineEdit("png")
|
|
user_settings_form_layout.addRow("Default 8-bit Output Format:", self.output_format_8bit_edit)
|
|
|
|
self.resolution_threshold_jpg_spinbox = QSpinBox()
|
|
self.resolution_threshold_jpg_spinbox.setRange(256, 16384)
|
|
self.resolution_threshold_jpg_spinbox.setValue(4096)
|
|
self.resolution_threshold_jpg_spinbox.setSuffix(" px")
|
|
user_settings_form_layout.addRow("JPG Resolution Threshold (for 8-bit):", self.resolution_threshold_jpg_spinbox)
|
|
|
|
user_settings_group.setLayout(user_settings_form_layout)
|
|
main_layout.addWidget(user_settings_group)
|
|
|
|
# Dialog Buttons
|
|
self.button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
|
self.button_box.button(QDialogButtonBox.StandardButton.Ok).setText("Finish Setup")
|
|
self.button_box.accepted.connect(self._on_finish_setup)
|
|
self.button_box.rejected.connect(self.reject)
|
|
main_layout.addWidget(self.button_box)
|
|
|
|
def _propose_default_config_path(self):
|
|
proposed_path = None
|
|
|
|
# 1. Try portable path: user_data/ next to the application base dir
|
|
# If running from script, app_base_dir is .../Asset_processor_tool/gui, so parent is .../Asset_processor_tool
|
|
# If bundled, app_base_dir is the directory of the executable.
|
|
|
|
# Let's refine app_base_dir for portable path logic
|
|
# If script: Path(__file__).parent.parent = Asset_processor_tool
|
|
# If frozen: sys._MEIPASS (which is the temp extraction dir, not ideal for persistent user_data)
|
|
# A better approach for portable if frozen: Path(sys.executable).parent
|
|
|
|
current_app_dir = Path(sys.executable).parent if getattr(sys, 'frozen', False) else self.app_base_dir
|
|
|
|
portable_path_candidate = current_app_dir / DEFAULT_USER_DATA_SUBDIR_NAME
|
|
try:
|
|
portable_path_candidate.mkdir(parents=True, exist_ok=True)
|
|
if os.access(str(portable_path_candidate), os.W_OK):
|
|
proposed_path = portable_path_candidate
|
|
self.proposed_path_label.setText(f"Proposed portable path (writable):")
|
|
else:
|
|
self.proposed_path_label.setText(f"Portable path '{portable_path_candidate}' not writable.")
|
|
except Exception as e:
|
|
self.proposed_path_label.setText(f"Could not use portable path '{portable_path_candidate}': {e}")
|
|
print(f"Error checking/creating portable path: {e}") # For debugging
|
|
|
|
# 2. Fallback to OS-specific app data directory
|
|
if not proposed_path:
|
|
os_specific_path = get_os_specific_app_data_dir()
|
|
try:
|
|
os_specific_path.mkdir(parents=True, exist_ok=True)
|
|
if os.access(str(os_specific_path), os.W_OK):
|
|
proposed_path = os_specific_path
|
|
self.proposed_path_label.setText(f"Proposed standard path (writable):")
|
|
else:
|
|
self.proposed_path_label.setText(f"Standard path '{os_specific_path}' not writable. Please choose a location.")
|
|
except Exception as e:
|
|
self.proposed_path_label.setText(f"Could not use standard path '{os_specific_path}': {e}. Please choose a location.")
|
|
print(f"Error checking/creating standard path: {e}") # For debugging
|
|
|
|
if proposed_path:
|
|
self.config_path_edit.setText(str(proposed_path.resolve()))
|
|
else:
|
|
# Should not happen if OS specific path creation works, but as a last resort:
|
|
self.config_path_edit.setText(str(Path.home())) # Default to home if all else fails
|
|
QMessageBox.warning(self, "Path Issue", "Could not determine a default writable configuration path. Please select one manually.")
|
|
|
|
@Slot()
|
|
def _browse_config_path(self):
|
|
directory = QFileDialog.getExistingDirectory(
|
|
self,
|
|
"Select Configuration Directory",
|
|
self.config_path_edit.text() or str(Path.home())
|
|
)
|
|
if directory:
|
|
self.config_path_edit.setText(directory)
|
|
|
|
@Slot()
|
|
def _browse_output_base_dir(self):
|
|
directory = QFileDialog.getExistingDirectory(
|
|
self,
|
|
"Select Default Library Output Directory",
|
|
self.output_base_dir_edit.text() or str(Path.home())
|
|
)
|
|
if directory:
|
|
self.output_base_dir_edit.setText(directory)
|
|
|
|
def _validate_inputs(self) -> bool:
|
|
# Validate chosen config path
|
|
path_str = self.config_path_edit.text().strip()
|
|
if not path_str:
|
|
QMessageBox.warning(self, "Input Error", "Configuration path cannot be empty.")
|
|
return False
|
|
|
|
self.user_chosen_path = Path(path_str)
|
|
try:
|
|
self.user_chosen_path.mkdir(parents=True, exist_ok=True)
|
|
if not os.access(str(self.user_chosen_path), os.W_OK):
|
|
QMessageBox.warning(self, "Path Error", f"The chosen configuration path '{self.user_chosen_path}' is not writable.")
|
|
return False
|
|
except Exception as e:
|
|
QMessageBox.warning(self, "Path Error", f"Error with chosen configuration path '{self.user_chosen_path}': {e}")
|
|
return False
|
|
|
|
# Validate output base dir
|
|
output_base_dir_str = self.output_base_dir_edit.text().strip()
|
|
if not output_base_dir_str:
|
|
QMessageBox.warning(self, "Input Error", "Default Library Output Path cannot be empty.")
|
|
return False
|
|
try:
|
|
Path(output_base_dir_str).mkdir(parents=True, exist_ok=True) # Check if creatable
|
|
if not os.access(output_base_dir_str, os.W_OK):
|
|
QMessageBox.warning(self, "Path Error", f"The chosen output base path '{output_base_dir_str}' is not writable.")
|
|
return False
|
|
except Exception as e:
|
|
QMessageBox.warning(self, "Path Error", f"Error with output base path '{output_base_dir_str}': {e}")
|
|
return False
|
|
|
|
if not self.output_dir_pattern_edit.text().strip():
|
|
QMessageBox.warning(self, "Input Error", "Asset Structure Pattern cannot be empty.")
|
|
return False
|
|
if not self.output_format_16bit_primary_edit.text().strip():
|
|
QMessageBox.warning(self, "Input Error", "Default 16-bit Output Format cannot be empty.")
|
|
return False
|
|
if not self.output_format_8bit_edit.text().strip():
|
|
QMessageBox.warning(self, "Input Error", "Default 8-bit Output Format cannot be empty.")
|
|
return False
|
|
|
|
return True
|
|
|
|
def _copy_default_files(self):
|
|
if not self.user_chosen_path:
|
|
return
|
|
|
|
bundled_config_dir = self.app_base_dir / BUNDLED_CONFIG_SUBDIR_NAME
|
|
user_target_config_dir = self.user_chosen_path / BUNDLED_CONFIG_SUBDIR_NAME # User files also go into a 'config' subdir
|
|
|
|
try:
|
|
user_target_config_dir.mkdir(parents=True, exist_ok=True)
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Error", f"Could not create user config subdirectory '{user_target_config_dir}': {e}")
|
|
return
|
|
|
|
for filename in DEFAULT_CONFIG_FILES:
|
|
source_file = bundled_config_dir / filename
|
|
target_file = user_target_config_dir / filename
|
|
if not target_file.exists():
|
|
if source_file.is_file():
|
|
try:
|
|
shutil.copy2(str(source_file), str(target_file))
|
|
print(f"Copied '{source_file}' to '{target_file}'")
|
|
except Exception as e:
|
|
QMessageBox.warning(self, "File Copy Error", f"Could not copy '{filename}' to '{target_file}': {e}")
|
|
else:
|
|
print(f"Default config file '{source_file}' not found in bundle.")
|
|
else:
|
|
print(f"User config file '{target_file}' already exists. Skipping copy.")
|
|
|
|
# Copy Presets
|
|
bundled_presets_dir = self.app_base_dir / BUNDLED_PRESETS_SUBDIR_NAME
|
|
user_target_presets_dir = self.user_chosen_path / BUNDLED_PRESETS_SUBDIR_NAME
|
|
|
|
if bundled_presets_dir.is_dir():
|
|
try:
|
|
user_target_presets_dir.mkdir(parents=True, exist_ok=True)
|
|
for item in bundled_presets_dir.iterdir():
|
|
target_item = user_target_presets_dir / item.name
|
|
if not target_item.exists():
|
|
if item.is_file():
|
|
shutil.copy2(str(item), str(target_item))
|
|
print(f"Copied preset '{item.name}' to '{target_item}'")
|
|
# Add elif item.is_dir() for recursive copy if presets can have subdirs
|
|
except Exception as e:
|
|
QMessageBox.warning(self, "Preset Copy Error", f"Could not copy presets to '{user_target_presets_dir}': {e}")
|
|
else:
|
|
print(f"Bundled presets directory '{bundled_presets_dir}' not found.")
|
|
|
|
|
|
def _save_initial_user_settings(self):
|
|
if not self.user_chosen_path:
|
|
return
|
|
|
|
user_settings_path = self.user_chosen_path / USER_SETTINGS_FILENAME
|
|
settings_data = {}
|
|
|
|
# Load existing if it exists (though unlikely for first-time setup, but good practice)
|
|
if user_settings_path.exists():
|
|
try:
|
|
with open(user_settings_path, 'r', encoding='utf-8') as f:
|
|
settings_data = json.load(f)
|
|
except Exception as e:
|
|
QMessageBox.warning(self, "Error Loading Settings", f"Could not load existing user settings from '{user_settings_path}': {e}. Will create a new one.")
|
|
settings_data = {}
|
|
|
|
# Update with new values from dialog
|
|
settings_data['OUTPUT_BASE_DIR'] = self.output_base_dir_edit.text().strip()
|
|
settings_data['OUTPUT_DIRECTORY_PATTERN'] = self.output_dir_pattern_edit.text().strip()
|
|
settings_data['OUTPUT_FORMAT_16BIT_PRIMARY'] = self.output_format_16bit_primary_edit.text().strip().lower()
|
|
settings_data['OUTPUT_FORMAT_8BIT'] = self.output_format_8bit_edit.text().strip().lower()
|
|
settings_data['RESOLUTION_THRESHOLD_FOR_JPG'] = self.resolution_threshold_jpg_spinbox.value()
|
|
|
|
# Ensure general_settings exists for app_version if needed, or other core settings
|
|
if 'general_settings' not in settings_data:
|
|
settings_data['general_settings'] = {}
|
|
# Example: settings_data['general_settings']['some_new_user_setting'] = True
|
|
|
|
try:
|
|
with open(user_settings_path, 'w', encoding='utf-8') as f:
|
|
json.dump(settings_data, f, indent=4)
|
|
print(f"Saved user settings to '{user_settings_path}'")
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Error Saving Settings", f"Could not save user settings to '{user_settings_path}': {e}")
|
|
|
|
|
|
def _save_persistent_info(self):
|
|
if not self.user_chosen_path:
|
|
return
|
|
|
|
# 1. Save USER_CHOSEN_PATH to a persistent location (e.g., AppData)
|
|
persistent_storage_dir = get_os_specific_app_data_dir()
|
|
try:
|
|
persistent_storage_dir.mkdir(parents=True, exist_ok=True)
|
|
persistent_path_file = persistent_storage_dir / PERSISTENT_CONFIG_ROOT_STORAGE_FILENAME
|
|
with open(persistent_path_file, 'w', encoding='utf-8') as f:
|
|
f.write(str(self.user_chosen_path.resolve()))
|
|
print(f"Saved chosen config path to '{persistent_path_file}'")
|
|
except Exception as e:
|
|
QMessageBox.warning(self, "Error Saving Path", f"Could not persistently save the chosen configuration path: {e}")
|
|
# This is not critical enough to stop the setup, but user might need to re-select on next launch.
|
|
|
|
# 2. Create marker file in USER_CHOSEN_PATH
|
|
marker_file = self.user_chosen_path / PERSISTENT_PATH_MARKER_FILENAME
|
|
try:
|
|
with open(marker_file, 'w', encoding='utf-8') as f:
|
|
f.write("Asset Processor first-time setup complete.")
|
|
print(f"Created marker file at '{marker_file}'")
|
|
except Exception as e:
|
|
QMessageBox.warning(self, "Error Creating Marker", f"Could not create first-run marker file at '{marker_file}': {e}")
|
|
|
|
@Slot()
|
|
def _on_finish_setup(self):
|
|
if not self._validate_inputs():
|
|
return
|
|
|
|
# Confirmation before proceeding
|
|
reply = QMessageBox.question(self, "Confirm Setup",
|
|
f"The following path will be used for configuration and user data:\n"
|
|
f"{self.user_chosen_path}\n\n"
|
|
f"Default configuration files and presets will be copied if they don't exist.\n"
|
|
f"Initial user settings will be saved.\n\nProceed with setup?",
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
QMessageBox.StandardButton.No)
|
|
if reply == QMessageBox.StandardButton.No:
|
|
return
|
|
|
|
try:
|
|
self._copy_default_files()
|
|
self._save_initial_user_settings()
|
|
self._save_persistent_info()
|
|
QMessageBox.information(self, "Setup Complete", "First-time setup completed successfully!")
|
|
self.accept()
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Setup Error", f"An unexpected error occurred during setup: {e}")
|
|
# Optionally, attempt cleanup or guide user
|
|
|
|
def get_chosen_config_path(self) -> Optional[Path]:
|
|
"""Returns the path chosen by the user after successful completion."""
|
|
if self.result() == QDialog.DialogCode.Accepted:
|
|
return self.user_chosen_path
|
|
return None
|
|
|
|
if __name__ == '__main__':
|
|
from PySide6.QtWidgets import QApplication
|
|
app = QApplication(sys.argv)
|
|
dialog = FirstTimeSetupDialog()
|
|
if dialog.exec():
|
|
chosen_path = dialog.get_chosen_config_path()
|
|
print(f"Dialog accepted. Chosen config path: {chosen_path}")
|
|
else:
|
|
print("Dialog cancelled.")
|
|
sys.exit() |