Update
45
.gitignore
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Config
|
||||
config.json
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
build/
|
||||
dist/
|
||||
37
CleanCache.bat
Normal file
@@ -0,0 +1,37 @@
|
||||
@echo off
|
||||
echo ========================================
|
||||
echo NexusLauncher Cache Cleaner
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
@REM echo Close NexusLauncher if it is running...
|
||||
@REM taskkill /f /im pythonw.exe
|
||||
|
||||
echo [1/4] Cleaning all __pycache__ folders...
|
||||
for /d /r %%d in (__pycache__) do @if exist "%%d" (
|
||||
echo Deleting: %%d
|
||||
rd /s /q "%%d"
|
||||
)
|
||||
echo.
|
||||
|
||||
echo [2/4] Cleaning root cache...
|
||||
if exist "__pycache__" rd /s /q "__pycache__"
|
||||
echo.
|
||||
|
||||
echo [3/4] Cleaning module caches...
|
||||
if exist "config\__pycache__" rd /s /q "config\__pycache__"
|
||||
if exist "ui\__pycache__" rd /s /q "ui\__pycache__"
|
||||
if exist "ui\task\__pycache__" rd /s /q "ui\task\__pycache__"
|
||||
echo.
|
||||
|
||||
echo [4/4] Cleaning .pyc files...
|
||||
del /s /q *.pyc 2>nul
|
||||
echo.
|
||||
|
||||
@REM echo Clear old config file
|
||||
@REM if exist config.json del /f config.json
|
||||
|
||||
echo ========================================
|
||||
echo Cache cleaned successfully!
|
||||
echo ========================================
|
||||
pause
|
||||
28
Run.bat
Normal file
@@ -0,0 +1,28 @@
|
||||
@echo off
|
||||
echo ========================================
|
||||
echo NexusLauncher Startup
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
echo [1/3] Closing existing instances...
|
||||
taskkill /f /im pythonw.exe 2>nul
|
||||
echo.
|
||||
|
||||
echo [2/3] Cleaning cache...
|
||||
echo Cleaning all __pycache__ folders...
|
||||
for /d /r %%d in (__pycache__) do @if exist "%%d" rd /s /q "%%d"
|
||||
|
||||
echo Cleaning module caches...
|
||||
if exist "__pycache__" rd /s /q "__pycache__"
|
||||
if exist "config\__pycache__" rd /s /q "config\__pycache__"
|
||||
if exist "ui\__pycache__" rd /s /q "ui\__pycache__"
|
||||
if exist "ui\task\__pycache__" rd /s /q "ui\task\__pycache__"
|
||||
echo.
|
||||
|
||||
echo [3/3] Starting NexusLauncher...
|
||||
start "" pythonw main.py
|
||||
|
||||
echo ========================================
|
||||
echo NexusLauncher started!
|
||||
echo ========================================
|
||||
exit
|
||||
43
RunDebug.bat
Normal file
@@ -0,0 +1,43 @@
|
||||
@echo off
|
||||
echo ========================================
|
||||
echo NexusLauncher Debug Mode
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
echo [1/4] Closing existing instances...
|
||||
taskkill /f /im pythonw.exe 2>nul
|
||||
taskkill /f /im python.exe 2>nul
|
||||
echo.
|
||||
|
||||
echo [2/4] Cleaning cache...
|
||||
echo Cleaning all __pycache__ folders...
|
||||
for /d /r %%d in (__pycache__) do @if exist "%%d" rd /s /q "%%d"
|
||||
|
||||
echo Cleaning module caches...
|
||||
if exist "__pycache__" rd /s /q "__pycache__"
|
||||
if exist "config\__pycache__" rd /s /q "config\__pycache__"
|
||||
if exist "ui\__pycache__" rd /s /q "ui\__pycache__"
|
||||
if exist "ui\task\__pycache__" rd /s /q "ui\task\__pycache__"
|
||||
echo.
|
||||
|
||||
@REM echo Clear old config file
|
||||
@REM if exist config.json del /f config.json
|
||||
|
||||
echo [3/4] Checking Python version...
|
||||
python --version
|
||||
echo.
|
||||
|
||||
echo [4/4] Launching NexusLauncher in Debug Mode...
|
||||
echo Console window will remain open for debugging
|
||||
echo Press Ctrl+C to stop the application
|
||||
echo.
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
python main.py
|
||||
|
||||
echo.
|
||||
echo ========================================
|
||||
echo NexusLauncher stopped
|
||||
echo ========================================
|
||||
pause
|
||||
3
RunHidden.vbs
Normal file
@@ -0,0 +1,3 @@
|
||||
Set WshShell = CreateObject("WScript.Shell")
|
||||
WshShell.Run "pythonw main.py", 0, False
|
||||
Set WshShell = Nothing
|
||||
61
build.bat
Normal file
@@ -0,0 +1,61 @@
|
||||
@echo off
|
||||
echo ========================================
|
||||
echo NexusLauncher Build Script
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
echo [1/6] Check Python version...
|
||||
python --version
|
||||
if %errorlevel% neq 0 (
|
||||
echo Error: Python not found, please ensure Python 3.11 or higher is installed
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo.
|
||||
|
||||
echo [2/6] Install dependencies...
|
||||
pip install -r requirements.txt
|
||||
if %errorlevel% neq 0 (
|
||||
echo Error: Failed to install dependencies
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo.
|
||||
|
||||
echo [3/6] Close running instances...
|
||||
taskkill /f /im NexusLauncher.exe 2>nul
|
||||
if %errorlevel% equ 0 (
|
||||
echo Closed NexusLauncher.exe
|
||||
timeout /t 2 /nobreak >nul
|
||||
) else (
|
||||
echo No running instances found
|
||||
)
|
||||
echo.
|
||||
|
||||
echo [4/6] Clean build directory...
|
||||
if exist "dist" rd /s /q "dist"
|
||||
if exist "build" rd /s /q "build"
|
||||
if exist "NexusLauncher.spec" del /f "NexusLauncher.spec"
|
||||
echo.
|
||||
|
||||
echo [5/6] Build EXE using PyInstaller...
|
||||
python -m PyInstaller --noconfirm --onefile --windowed --name "NexusLauncher" --icon="icons/NexusLauncher.ico" --add-data "icons;icons" main.py
|
||||
if %errorlevel% neq 0 (
|
||||
echo Error: Failed to build EXE
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo.
|
||||
|
||||
echo [6/6] Copy EXE to template folder...
|
||||
copy /y /b "dist\NexusLauncher.exe" "D:\NexusLauncher\NexusLauncher.exe"
|
||||
echo.
|
||||
echo ========================================
|
||||
echo Build completed!
|
||||
echo Executable file location: D:\NexusLauncher\NexusLauncher.exe
|
||||
echo ========================================
|
||||
echo.
|
||||
echo Opening D:\NexusLauncher folder...
|
||||
start "" "D:\NexusLauncher"
|
||||
echo.
|
||||
pause
|
||||
13
config/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Config Module
|
||||
-------------
|
||||
配置管理相关模块
|
||||
"""
|
||||
|
||||
from .config_manager import ConfigManager
|
||||
from .icon_config import IconConfigManager
|
||||
from . import constants
|
||||
|
||||
__all__ = ['ConfigManager', 'IconConfigManager', 'constants']
|
||||
623
config/config_manager.py
Normal file
@@ -0,0 +1,623 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
配置管理模块 - 负责读取和保存应用配置
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, List, Optional
|
||||
from .icon_config import IconConfigManager
|
||||
from .constants import DEFAULT_TASK_FOLDER_TEMPLATES
|
||||
|
||||
|
||||
class ConfigManager:
|
||||
"""管理NexusLauncher的配置文件"""
|
||||
|
||||
def __init__(self, config_file: str = "config.json"):
|
||||
self.config_file = config_file
|
||||
self.config_data = self._load_config()
|
||||
|
||||
# 如果配置文件不存在,保存默认配置
|
||||
if not os.path.exists(self.config_file):
|
||||
print("[INFO] Config file not found, creating default config.json")
|
||||
self.save_config()
|
||||
|
||||
# 创建图标配置管理器
|
||||
self.icon_config = IconConfigManager(self.config_data, self._get_icons_dir)
|
||||
|
||||
def _get_icons_dir(self) -> str:
|
||||
"""获取 icons 目录路径(避免循环导入)"""
|
||||
if getattr(sys, 'frozen', False):
|
||||
# 打包后的应用
|
||||
return os.path.join(sys._MEIPASS, "icons")
|
||||
else:
|
||||
# 开发环境
|
||||
config_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
project_root = os.path.dirname(config_dir)
|
||||
return os.path.join(project_root, "icons")
|
||||
|
||||
def _load_config(self) -> Dict:
|
||||
"""加载配置文件"""
|
||||
if os.path.exists(self.config_file):
|
||||
try:
|
||||
with open(self.config_file, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
print(f"Failed to load configuration file: {e}")
|
||||
return self._get_default_config()
|
||||
else:
|
||||
return self._get_default_config()
|
||||
|
||||
def _get_default_config(self) -> Dict:
|
||||
"""获取默认配置"""
|
||||
return {
|
||||
"projects": {
|
||||
"Project_01": {
|
||||
"icon": "NexusLauncher.ico", # 默认项目图标
|
||||
"color": "", # 项目背景颜色
|
||||
"apps": []
|
||||
}
|
||||
},
|
||||
"current_project": "Project_01",
|
||||
"window_size": {
|
||||
"width": 425,
|
||||
"height": 480
|
||||
},
|
||||
"icon_size": 80, # 图标大小,默认80x80
|
||||
"app_icons": {}, # 应用图标映射,格式:{"app_path": "icon_path"}
|
||||
"app_colors": {}, # 应用按钮颜色映射,格式:{"app_path": "#RRGGBB"}
|
||||
"task_folder_templates": self._get_default_task_templates(), # 任务类型默认文件夹结构
|
||||
"task_settings": {} # 已废弃:旧的任务设置存储位置,现在存储在 projects.项目名.task_settings
|
||||
}
|
||||
|
||||
def save_config(self) -> bool:
|
||||
"""保存配置到文件"""
|
||||
try:
|
||||
# 重新排序项目字段:icon, color, apps, task_settings
|
||||
if "projects" in self.config_data:
|
||||
for project_name, project_data in self.config_data["projects"].items():
|
||||
ordered_project = {}
|
||||
# 按顺序添加字段
|
||||
if "icon" in project_data:
|
||||
ordered_project["icon"] = project_data["icon"]
|
||||
if "color" in project_data:
|
||||
ordered_project["color"] = project_data["color"]
|
||||
if "apps" in project_data:
|
||||
ordered_project["apps"] = project_data["apps"]
|
||||
if "task_settings" in project_data:
|
||||
ordered_project["task_settings"] = project_data["task_settings"]
|
||||
|
||||
# 替换原有项目数据
|
||||
self.config_data["projects"][project_name] = ordered_project
|
||||
|
||||
with open(self.config_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.config_data, f, ensure_ascii=False, indent=4)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Failed to save configuration file: {e}")
|
||||
return False
|
||||
|
||||
def reload_config(self) -> bool:
|
||||
"""重新加载配置文件
|
||||
|
||||
Returns:
|
||||
是否加载成功
|
||||
"""
|
||||
try:
|
||||
self.config_data = self._load_config()
|
||||
# 重新创建图标配置管理器以使用新的config_data引用
|
||||
self.icon_config = IconConfigManager(self.config_data, self._get_icons_dir)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Failed to reload config file: {e}")
|
||||
return False
|
||||
|
||||
def get_projects(self) -> List[str]:
|
||||
"""获取所有项目名称"""
|
||||
return list(self.config_data.get("projects", {}).keys())
|
||||
|
||||
def get_current_project(self) -> str:
|
||||
"""获取当前选中的项目"""
|
||||
return self.config_data.get("current_project", "")
|
||||
|
||||
def set_current_project(self, project_name: str):
|
||||
"""设置当前项目"""
|
||||
if project_name in self.config_data.get("projects", {}):
|
||||
self.config_data["current_project"] = project_name
|
||||
self.save_config()
|
||||
|
||||
def get_apps(self, project_name: Optional[str] = None) -> List[Dict]:
|
||||
"""获取指定项目的应用列表"""
|
||||
if project_name is None:
|
||||
project_name = self.get_current_project()
|
||||
|
||||
projects = self.config_data.get("projects", {})
|
||||
if project_name in projects:
|
||||
return projects[project_name].get("apps", [])
|
||||
return []
|
||||
|
||||
def add_project(self, project_name: str, default_icon: str = None) -> bool:
|
||||
"""添加新项目
|
||||
|
||||
Args:
|
||||
project_name: 项目名称
|
||||
default_icon: 默认图标路径(可选)
|
||||
"""
|
||||
if "projects" not in self.config_data:
|
||||
self.config_data["projects"] = {}
|
||||
|
||||
if project_name in self.config_data["projects"]:
|
||||
return False
|
||||
|
||||
self.config_data["projects"][project_name] = {
|
||||
"icon": default_icon if default_icon else "",
|
||||
"color": "",
|
||||
"apps": []
|
||||
}
|
||||
return self.save_config()
|
||||
|
||||
def delete_project(self, project_name: str) -> bool:
|
||||
"""删除项目"""
|
||||
if project_name in self.config_data.get("projects", {}):
|
||||
# 删除项目配置(包含图标和颜色)
|
||||
del self.config_data["projects"][project_name]
|
||||
|
||||
# 如果删除的是当前项目,切换到第一个项目
|
||||
if self.config_data.get("current_project") == project_name:
|
||||
projects = self.get_projects()
|
||||
if projects:
|
||||
self.config_data["current_project"] = projects[0]
|
||||
else:
|
||||
self.config_data["current_project"] = ""
|
||||
|
||||
return self.save_config()
|
||||
return False
|
||||
|
||||
def rename_project(self, old_name: str, new_name: str) -> bool:
|
||||
"""重命名项目"""
|
||||
# 检查旧项目是否存在
|
||||
if old_name not in self.config_data.get("projects", {}):
|
||||
return False
|
||||
|
||||
# 检查新名称是否已存在
|
||||
if new_name in self.config_data.get("projects", {}):
|
||||
return False
|
||||
|
||||
# 重命名项目
|
||||
self.config_data["projects"][new_name] = self.config_data["projects"].pop(old_name)
|
||||
|
||||
# 如果重命名的是当前项目,更新当前项目名称
|
||||
if self.config_data.get("current_project") == old_name:
|
||||
self.config_data["current_project"] = new_name
|
||||
|
||||
return self.save_config()
|
||||
|
||||
def add_app(self, project_name: str, app_name: str, app_path: str, version: str) -> bool:
|
||||
"""添加应用到指定项目"""
|
||||
if project_name not in self.config_data.get("projects", {}):
|
||||
return False
|
||||
|
||||
app_data = {
|
||||
"name": app_name,
|
||||
"path": app_path,
|
||||
"version": version
|
||||
}
|
||||
|
||||
self.config_data["projects"][project_name]["apps"].append(app_data)
|
||||
return self.save_config()
|
||||
|
||||
def update_app(self, project_name: str, app_index: int, app_name: str, app_path: str, version: str) -> bool:
|
||||
"""更新应用信息"""
|
||||
if project_name not in self.config_data.get("projects", {}):
|
||||
return False
|
||||
|
||||
apps = self.config_data["projects"][project_name]["apps"]
|
||||
if 0 <= app_index < len(apps):
|
||||
apps[app_index] = {
|
||||
"name": app_name,
|
||||
"path": app_path,
|
||||
"version": version
|
||||
}
|
||||
return self.save_config()
|
||||
return False
|
||||
|
||||
def delete_app(self, project_name: str, app_index: int) -> bool:
|
||||
"""删除应用"""
|
||||
if project_name not in self.config_data.get("projects", {}):
|
||||
return False
|
||||
|
||||
apps = self.config_data["projects"][project_name]["apps"]
|
||||
if 0 <= app_index < len(apps):
|
||||
apps.pop(app_index)
|
||||
return self.save_config()
|
||||
return False
|
||||
|
||||
def reorder_apps(self, project_name: str, from_index: int, to_index: int) -> bool:
|
||||
"""重新排序应用,支持拖到列表末尾"""
|
||||
if project_name not in self.config_data.get("projects", {}):
|
||||
return False
|
||||
|
||||
apps = self.config_data["projects"][project_name]["apps"]
|
||||
app_count = len(apps)
|
||||
# 允许目标索引等于列表长度(表示插入到末尾)
|
||||
if not (0 <= from_index < app_count and 0 <= to_index <= app_count):
|
||||
return False
|
||||
|
||||
app = apps.pop(from_index)
|
||||
# 如果从上方拖到下方,移除后目标索引需要左移一位
|
||||
if from_index < to_index:
|
||||
to_index -= 1
|
||||
# 再次夹紧,防止越界
|
||||
to_index = max(0, min(to_index, len(apps)))
|
||||
apps.insert(to_index, app)
|
||||
return self.save_config()
|
||||
|
||||
def get_window_size(self) -> tuple:
|
||||
"""获取窗口大小"""
|
||||
size = self.config_data.get("window_size", {"width": 400, "height": 400})
|
||||
return (size.get("width", 400), size.get("height", 400))
|
||||
|
||||
def save_window_size(self, width: int, height: int):
|
||||
"""保存窗口大小"""
|
||||
self.config_data["window_size"] = {"width": width, "height": height}
|
||||
self.save_config()
|
||||
|
||||
def get_app_icon(self, app_path: str) -> str:
|
||||
"""获取应用图标路径"""
|
||||
return self.icon_config.get_app_icon(app_path)
|
||||
|
||||
def set_app_icon(self, app_path: str, icon_path: str) -> bool:
|
||||
"""设置应用图标路径"""
|
||||
result = self.icon_config.set_app_icon(app_path, icon_path)
|
||||
if result:
|
||||
return self.save_config()
|
||||
return False
|
||||
|
||||
def remove_app_icon(self, app_path: str) -> bool:
|
||||
"""移除应用图标设置"""
|
||||
result = self.icon_config.remove_app_icon(app_path)
|
||||
if result:
|
||||
return self.save_config()
|
||||
return False
|
||||
|
||||
def get_all_app_icons(self) -> Dict[str, str]:
|
||||
"""获取所有应用图标映射"""
|
||||
return self.config_data.get("app_icons", {})
|
||||
|
||||
def get_app_color(self, app_path: str) -> str:
|
||||
"""获取应用按钮颜色"""
|
||||
return self.icon_config.get_app_color(app_path)
|
||||
|
||||
def set_app_color(self, app_path: str, color: str) -> bool:
|
||||
"""设置应用按钮颜色"""
|
||||
result = self.icon_config.set_app_color(app_path, color)
|
||||
if result:
|
||||
return self.save_config()
|
||||
return False
|
||||
|
||||
def remove_app_color(self, app_path: str) -> bool:
|
||||
"""移除应用按钮颜色设置"""
|
||||
result = self.icon_config.remove_app_color(app_path)
|
||||
if result:
|
||||
return self.save_config()
|
||||
return False
|
||||
|
||||
def get_all_app_colors(self) -> Dict[str, str]:
|
||||
"""获取所有应用按钮颜色映射"""
|
||||
return self.config_data.get("app_colors", {})
|
||||
|
||||
def get_icon_size(self) -> int:
|
||||
"""获取图标大小"""
|
||||
return self.config_data.get("icon_size", 80)
|
||||
|
||||
def save_icon_size(self, size: int) -> bool:
|
||||
"""保存图标大小"""
|
||||
self.config_data["icon_size"] = size
|
||||
return self.save_config()
|
||||
|
||||
def get_project_icon(self, project_name: str) -> str:
|
||||
"""获取项目图标路径"""
|
||||
return self.icon_config.get_project_icon(project_name)
|
||||
|
||||
def set_project_icon(self, project_name: str, icon_path: str) -> bool:
|
||||
"""设置项目图标路径"""
|
||||
result = self.icon_config.set_project_icon(project_name, icon_path)
|
||||
if result:
|
||||
return self.save_config()
|
||||
return False
|
||||
|
||||
def remove_project_icon(self, project_name: str) -> bool:
|
||||
"""移除项目图标设置"""
|
||||
result = self.icon_config.remove_project_icon(project_name)
|
||||
if result:
|
||||
return self.save_config()
|
||||
return False
|
||||
|
||||
def get_project_color(self, project_name: str) -> str:
|
||||
"""获取项目背景颜色"""
|
||||
return self.icon_config.get_project_color(project_name)
|
||||
|
||||
def set_project_color(self, project_name: str, color: str) -> bool:
|
||||
"""设置项目背景颜色"""
|
||||
result = self.icon_config.set_project_color(project_name, color)
|
||||
if result:
|
||||
return self.save_config()
|
||||
return False
|
||||
|
||||
def remove_project_color(self, project_name: str) -> bool:
|
||||
"""移除项目背景颜色设置"""
|
||||
result = self.icon_config.remove_project_color(project_name)
|
||||
if result:
|
||||
return self.save_config()
|
||||
return False
|
||||
|
||||
def _get_default_task_templates(self) -> Dict[str, List[str]]:
|
||||
"""获取默认任务类型文件夹模板"""
|
||||
return DEFAULT_TASK_FOLDER_TEMPLATES
|
||||
|
||||
def get_task_folder_template(self, task_type: str) -> List[str]:
|
||||
"""获取指定任务类型的文件夹模板
|
||||
|
||||
Args:
|
||||
task_type: 任务类型名称
|
||||
|
||||
Returns:
|
||||
文件夹路径列表
|
||||
"""
|
||||
templates = self.config_data.get("task_folder_templates", {})
|
||||
if task_type in templates:
|
||||
return templates[task_type]
|
||||
# 如果配置中没有,返回默认模板
|
||||
default_templates = self._get_default_task_templates()
|
||||
return default_templates.get(task_type, [])
|
||||
|
||||
def get_task_types(self) -> List[str]:
|
||||
"""获取所有可用的任务类型名称列表
|
||||
|
||||
Returns:
|
||||
任务类型名称列表
|
||||
"""
|
||||
templates = self.config_data.get("task_folder_templates", {})
|
||||
if not templates:
|
||||
# 如果配置中没有,使用默认模板
|
||||
templates = self._get_default_task_templates()
|
||||
return sorted(list(templates.keys()))
|
||||
|
||||
def get_all_task_folder_templates(self) -> Dict[str, List[str]]:
|
||||
"""获取所有任务类型的文件夹模板
|
||||
|
||||
Returns:
|
||||
任务类型到文件夹列表的映射
|
||||
"""
|
||||
templates = self.config_data.get("task_folder_templates", {})
|
||||
if not templates:
|
||||
# 如果配置中没有,初始化默认模板并保存
|
||||
templates = self._get_default_task_templates()
|
||||
self.config_data["task_folder_templates"] = templates
|
||||
self.save_config()
|
||||
return templates
|
||||
|
||||
def set_task_folder_template(self, task_type: str, folders: List[str]) -> bool:
|
||||
"""设置指定任务类型的文件夹模板
|
||||
|
||||
Args:
|
||||
task_type: 任务类型名称
|
||||
folders: 文件夹路径列表
|
||||
|
||||
Returns:
|
||||
是否保存成功
|
||||
"""
|
||||
if "task_folder_templates" not in self.config_data:
|
||||
self.config_data["task_folder_templates"] = {}
|
||||
|
||||
# 统一路径格式为正斜杠(JSON 存储格式)
|
||||
normalized_folders = [folder.replace("\\", "/") for folder in folders]
|
||||
self.config_data["task_folder_templates"][task_type] = normalized_folders
|
||||
return self.save_config()
|
||||
|
||||
def update_all_task_folder_templates(self, templates: Dict[str, List[str]]) -> bool:
|
||||
"""更新所有任务类型的文件夹模板
|
||||
|
||||
Args:
|
||||
templates: 任务类型到文件夹列表的映射
|
||||
|
||||
Returns:
|
||||
是否保存成功
|
||||
"""
|
||||
# 统一所有模板的路径格式为正斜杠(JSON 存储格式)
|
||||
normalized_templates = {}
|
||||
for task_type, folders in templates.items():
|
||||
normalized_templates[task_type] = [folder.replace("\\", "/") for folder in folders]
|
||||
|
||||
self.config_data["task_folder_templates"] = normalized_templates
|
||||
return self.save_config()
|
||||
|
||||
def get_task_settings(self, project_name: str) -> Dict:
|
||||
"""获取指定项目的 Task 面板设置
|
||||
|
||||
Args:
|
||||
project_name: 项目名称
|
||||
|
||||
Returns:
|
||||
Task 设置字典,包含 workspace, task_type, use_type_hierarchy
|
||||
"""
|
||||
# 确保 projects 字段存在
|
||||
if "projects" not in self.config_data:
|
||||
self.config_data["projects"] = {}
|
||||
|
||||
# 确保项目存在
|
||||
if project_name not in self.config_data["projects"]:
|
||||
self.config_data["projects"][project_name] = {}
|
||||
|
||||
# 从 projects.项目名.task_settings 读取
|
||||
project_data = self.config_data["projects"][project_name]
|
||||
settings = project_data.get("task_settings", {
|
||||
"workspace": "D:\\Workspace",
|
||||
"task_type": "Character",
|
||||
"use_type_hierarchy": False
|
||||
})
|
||||
|
||||
# 标准化路径格式(JSON中存储正斜杠,转换为反斜杠供UI显示)
|
||||
if "workspace" in settings and settings["workspace"]:
|
||||
settings["workspace"] = settings["workspace"].replace("/", "\\")
|
||||
if "maya_plugin_path" in settings and settings["maya_plugin_path"]:
|
||||
settings["maya_plugin_path"] = settings["maya_plugin_path"].replace("/", "\\")
|
||||
if "sp_shelf_path" in settings and settings["sp_shelf_path"]:
|
||||
settings["sp_shelf_path"] = settings["sp_shelf_path"].replace("/", "\\")
|
||||
|
||||
return settings
|
||||
|
||||
def set_task_settings(self, project_name: str, workspace: str = None,
|
||||
task_type: str = None, use_type_hierarchy: bool = None,
|
||||
maya_plugin_path: str = None, sp_shelf_path: str = None) -> bool:
|
||||
"""设置指定项目的 Task 面板设置
|
||||
|
||||
Args:
|
||||
project_name: 项目名称
|
||||
workspace: 工作空间路径
|
||||
task_type: 任务类型
|
||||
use_type_hierarchy: 是否使用类型层级
|
||||
maya_plugin_path: Maya 插件路径
|
||||
sp_shelf_path: Substance Painter 架子路径
|
||||
|
||||
Returns:
|
||||
是否保存成功
|
||||
"""
|
||||
# 确保 projects 字段存在
|
||||
if "projects" not in self.config_data:
|
||||
self.config_data["projects"] = {}
|
||||
|
||||
# 确保项目存在
|
||||
if project_name not in self.config_data["projects"]:
|
||||
self.config_data["projects"][project_name] = {}
|
||||
|
||||
# 确保 task_settings 字段存在
|
||||
if "task_settings" not in self.config_data["projects"][project_name]:
|
||||
self.config_data["projects"][project_name]["task_settings"] = {}
|
||||
|
||||
# 保存到 projects.项目名.task_settings
|
||||
settings = self.config_data["projects"][project_name]["task_settings"]
|
||||
|
||||
# 保存 SubFolders(如果存在,先临时保存)
|
||||
subfolders = settings.get("SubFolders")
|
||||
|
||||
# 按顺序更新字段
|
||||
if workspace is not None:
|
||||
settings["workspace"] = workspace.replace("\\", "/")
|
||||
if task_type is not None:
|
||||
settings["task_type"] = task_type
|
||||
if use_type_hierarchy is not None:
|
||||
settings["use_type_hierarchy"] = use_type_hierarchy
|
||||
if maya_plugin_path is not None:
|
||||
settings["maya_plugin_path"] = maya_plugin_path.replace("\\", "/")
|
||||
if sp_shelf_path is not None:
|
||||
settings["sp_shelf_path"] = sp_shelf_path.replace("\\", "/")
|
||||
|
||||
# 重新构建有序字典,确保字段顺序:workspace, maya_plugin_path, sp_shelf_path, task_type, use_type_hierarchy, SubFolders
|
||||
ordered_settings = {}
|
||||
for key in ["workspace", "maya_plugin_path", "sp_shelf_path", "task_type", "use_type_hierarchy"]:
|
||||
if key in settings:
|
||||
ordered_settings[key] = settings[key]
|
||||
|
||||
# 将 SubFolders 放在最后
|
||||
if subfolders is not None:
|
||||
ordered_settings["SubFolders"] = subfolders
|
||||
|
||||
# 替换原有的 task_settings
|
||||
self.config_data["projects"][project_name]["task_settings"] = ordered_settings
|
||||
|
||||
print(f"[OK] Save the task settings to projects.{project_name}.task_settings")
|
||||
return self.save_config()
|
||||
|
||||
def copy_apps_to_config(self, apps_data: List[Dict]) -> bool:
|
||||
"""将复制的应用数据保存到配置文件"""
|
||||
try:
|
||||
if "clipboard" not in self.config_data:
|
||||
self.config_data["clipboard"] = {}
|
||||
|
||||
self.config_data["clipboard"]["apps"] = apps_data
|
||||
print(f"[DEBUG] Saved {len(apps_data)} apps to config clipboard")
|
||||
return self.save_config()
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to save apps to config clipboard: {e}")
|
||||
return False
|
||||
|
||||
def get_clipboard_apps(self) -> List[Dict]:
|
||||
"""从配置文件获取复制的应用数据"""
|
||||
try:
|
||||
if "clipboard" in self.config_data and "apps" in self.config_data["clipboard"]:
|
||||
apps = self.config_data["clipboard"]["apps"]
|
||||
print(f"[DEBUG] Retrieved {len(apps)} apps from config clipboard")
|
||||
return apps
|
||||
else:
|
||||
print("[DEBUG] No apps in config clipboard")
|
||||
return []
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to get apps from config clipboard: {e}")
|
||||
return []
|
||||
|
||||
def clear_clipboard_apps(self) -> bool:
|
||||
"""清空配置文件中的应用剪贴板"""
|
||||
try:
|
||||
if "clipboard" in self.config_data:
|
||||
self.config_data["clipboard"]["apps"] = []
|
||||
print("[DEBUG] Cleared config clipboard")
|
||||
return self.save_config()
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to clear config clipboard: {e}")
|
||||
return False
|
||||
|
||||
def save_selection_state(self, project_name: str, selected_indices: List[int]) -> bool:
|
||||
"""保存项目的选择状态到配置文件"""
|
||||
try:
|
||||
if "selection_states" not in self.config_data:
|
||||
self.config_data["selection_states"] = {}
|
||||
|
||||
self.config_data["selection_states"][project_name] = selected_indices
|
||||
print(f"[DEBUG] Saved selection state for {project_name}: {selected_indices}")
|
||||
return self.save_config()
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to save selection state: {e}")
|
||||
return False
|
||||
|
||||
def get_selection_state(self, project_name: str) -> List[int]:
|
||||
"""从配置文件获取项目的选择状态"""
|
||||
try:
|
||||
if ("selection_states" in self.config_data and
|
||||
project_name in self.config_data["selection_states"]):
|
||||
indices = self.config_data["selection_states"][project_name]
|
||||
print(f"[DEBUG] Retrieved selection state for {project_name}: {indices}")
|
||||
return indices
|
||||
else:
|
||||
print(f"[DEBUG] No selection state found for {project_name}")
|
||||
return []
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to get selection state: {e}")
|
||||
return []
|
||||
|
||||
def select_all_apps(self, project_name: str) -> List[int]:
|
||||
"""选择项目的所有应用并保存状态"""
|
||||
try:
|
||||
apps = self.get_apps(project_name)
|
||||
all_indices = list(range(len(apps)))
|
||||
self.save_selection_state(project_name, all_indices)
|
||||
print(f"[DEBUG] Selected all {len(all_indices)} apps in {project_name}")
|
||||
return all_indices
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to select all apps: {e}")
|
||||
return []
|
||||
|
||||
def clear_selection_state(self, project_name: str) -> bool:
|
||||
"""清空项目的选择状态"""
|
||||
try:
|
||||
if ("selection_states" in self.config_data and
|
||||
project_name in self.config_data["selection_states"]):
|
||||
self.config_data["selection_states"][project_name] = []
|
||||
print(f"[DEBUG] Cleared selection state for {project_name}")
|
||||
return self.save_config()
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to clear selection state: {e}")
|
||||
return False
|
||||
383
config/constants.py
Normal file
@@ -0,0 +1,383 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Application Constants
|
||||
--------------------
|
||||
应用程序的常量定义,按功能模块分类
|
||||
"""
|
||||
# ==================== 默认路径 ====================
|
||||
|
||||
# 默认项目文件夹路径
|
||||
DEFAULT_WORKSPACE_PATH = "D:\\Workspace"
|
||||
|
||||
# 默认Maya插件文件夹路径
|
||||
DEFAULT_MAYA_PLUGINS_PATH = "D:\\Plugins\\Maya"
|
||||
|
||||
# 默认SPShelf文件夹路径
|
||||
DEFAULT_SP_SHELF_PATH = "D:\\Plugins\\SPShelf"
|
||||
|
||||
# ==================== 项目模板配置 ====================
|
||||
|
||||
# 默认任务类型文件夹模板
|
||||
|
||||
DEFAULT_TASK_FOLDER_TEMPLATES = {
|
||||
"Character": [
|
||||
"Reference",
|
||||
"MP",
|
||||
"HP",
|
||||
"LP",
|
||||
"Baking",
|
||||
"Baking/HP",
|
||||
"Baking/LP",
|
||||
"Texture",
|
||||
"Texture/MeshMaps",
|
||||
"Texture/SP",
|
||||
"FBX",
|
||||
"Screenshot"
|
||||
],
|
||||
"Weapon": [
|
||||
"Reference",
|
||||
"MP",
|
||||
"HP",
|
||||
"LP",
|
||||
"Baking",
|
||||
"Baking/HP",
|
||||
"Baking/LP",
|
||||
"Texture",
|
||||
"Texture/MeshMaps",
|
||||
"Texture/SP",
|
||||
"FBX",
|
||||
"Screenshot"
|
||||
],
|
||||
"Prop": [
|
||||
"Reference",
|
||||
"MP",
|
||||
"HP",
|
||||
"LP",
|
||||
"Baking",
|
||||
"Baking/HP",
|
||||
"Baking/LP",
|
||||
"Texture",
|
||||
"Texture/MeshMaps",
|
||||
"Texture/SP",
|
||||
"FBX",
|
||||
"Screenshot"
|
||||
],
|
||||
"Environment": [
|
||||
"Reference",
|
||||
"MP",
|
||||
"HP",
|
||||
"LP",
|
||||
"Baking",
|
||||
"Baking/HP",
|
||||
"Baking/LP",
|
||||
"Texture",
|
||||
"Texture/MeshMaps",
|
||||
"Texture/SP",
|
||||
"FBX",
|
||||
"Screenshot"
|
||||
],
|
||||
"Animation": [
|
||||
"Reference",
|
||||
"Maya",
|
||||
"FBX",
|
||||
"Mocap"
|
||||
],
|
||||
"Rigging": [
|
||||
"Source",
|
||||
"Maya",
|
||||
"FBX"
|
||||
],
|
||||
"Other": [
|
||||
"Reference",
|
||||
"MP",
|
||||
"HP",
|
||||
"LP",
|
||||
"Baking",
|
||||
"Baking/HP",
|
||||
"Baking/LP",
|
||||
"Texture",
|
||||
"Texture/MeshMaps",
|
||||
"Texture/SP",
|
||||
"FBX",
|
||||
"Screenshot"
|
||||
]
|
||||
}
|
||||
|
||||
# ==================== 图标映射配置 ====================
|
||||
|
||||
# 应用名称到图标的映射
|
||||
APP_ICON_MAPPING = {
|
||||
"maya": "Maya",
|
||||
"maya2025": "Maya",
|
||||
"maya2026": "Maya",
|
||||
"maya2027": "Maya",
|
||||
"maya2028": "Maya",
|
||||
"maya2029": "Maya",
|
||||
"maya2020": "Maya",
|
||||
"maya2021": "Maya",
|
||||
"maya2022": "Maya",
|
||||
"maya2023": "Maya",
|
||||
"maya2024": "Maya",
|
||||
"ma": "Maya",
|
||||
"3dsmax": "3DsMax",
|
||||
"3ds": "3DsMax",
|
||||
"3ds-max": "3DsMax",
|
||||
"max": "3DsMax",
|
||||
"blender": "Blender",
|
||||
"photoshop": "Photoshop",
|
||||
"painter": "SubstancePainter",
|
||||
"3dpainter": "SubstancePainter",
|
||||
"substancepainter": "SubstancePainter",
|
||||
"substance3dpainter": "SubstancePainter",
|
||||
"sp": "SubstancePainter",
|
||||
"designer": "SubstanceDesigner",
|
||||
"3ddesigner": "SubstanceDesigner",
|
||||
"substancedesigner": "SubstanceDesigner",
|
||||
"substance3ddesigner": "SubstanceDesigner",
|
||||
"sd": "SubstanceDesigner",
|
||||
"marvelousdesigner": "MarvelousDesigner",
|
||||
"marvelous": "MarvelousDesigner",
|
||||
"md": "MarvelousDesigner",
|
||||
"marvelousdesigner": "MarvelousDesigner",
|
||||
"marvelous": "MarvelousDesigner",
|
||||
"rizom": "RizomUV",
|
||||
"rizomuv": "RizomUV",
|
||||
"zbrush": "Zbrush",
|
||||
"ue": "UnrealEngine",
|
||||
"ue4": "UnrealEngine",
|
||||
"ue5": "UnrealEngine",
|
||||
"ue6": "UnrealEngine",
|
||||
"unrealengine": "UnrealEngine",
|
||||
"unrealtoolbox": "UnrealEngine",
|
||||
"unrealgamesync": "UnrealGameSync",
|
||||
"ugs": "UnrealGameSync",
|
||||
"uefn": "UEFN",
|
||||
"marmoset": "MarmosetToolBag",
|
||||
"marmosettoolbag": "MarmosetToolBag",
|
||||
"toolbag": "MarmosetToolBag",
|
||||
"3dcoat": "3DCoat",
|
||||
"houdini": "Houdini",
|
||||
"houdinifx": "Houdini",
|
||||
"houdiniengine": "Houdini",
|
||||
"everything": "Everything",
|
||||
"billfish": "Billfish",
|
||||
"eagle": "Eagle"
|
||||
}
|
||||
|
||||
# ==================== 通用UI常量 ====================
|
||||
|
||||
# 预设颜色列表
|
||||
PRESET_COLORS = [
|
||||
"#607d8b", # 蓝灰色(默认)
|
||||
"#2196f3", # 蓝色
|
||||
"#f44336", # 红色
|
||||
"#4caf50", # 绿色
|
||||
"#ff9800", # 橙色
|
||||
"#9c27b0", # 紫色
|
||||
"#00bcd4", # 青色
|
||||
"#ffeb3b", # 黄色
|
||||
"#009688", # 青绿色
|
||||
"#673ab7", # 深紫色
|
||||
"#3f51b5", # 青蓝色
|
||||
"#795548" # 棕色
|
||||
]
|
||||
|
||||
# 基础颜色
|
||||
BG_COLOR_DARK = "#2b2b2b"
|
||||
BG_COLOR_LIGHT = "#3a3a3a"
|
||||
BG_COLOR_FRAME = "#3a3a3a"
|
||||
BG_COLOR_BUTTON = "#4a5568"
|
||||
BG_COLOR_BUTTON_HOVER = "#2d3748"
|
||||
COLOR_TRANSPARENT = "transparent"
|
||||
BORDER_COLOR = "#555555"
|
||||
BORDER_COLOR_WHITE = "#ffffff"
|
||||
LINE_COLOR_GRAY = "#aaaaaa"
|
||||
|
||||
# 文本颜色
|
||||
TEXT_COLOR_PRIMARY = "white"
|
||||
TEXT_COLOR_SECONDARY = "gray"
|
||||
TEXT_COLOR_WHITE = "#ffffff"
|
||||
|
||||
# 状态颜色
|
||||
COLOR_SUCCESS = "#28a745"
|
||||
COLOR_SUCCESS_HOVER = "#218838"
|
||||
COLOR_ERROR = "#dc3545"
|
||||
COLOR_ERROR_HOVER = "#c82333"
|
||||
COLOR_WARNING = "#ffc107"
|
||||
COLOR_INFO = "#17a2b8"
|
||||
|
||||
# 通用按钮颜色
|
||||
BUTTON_GRAY = "#757575"
|
||||
BUTTON_GRAY_HOVER = "#616161"
|
||||
BUTTON_RED = "#d32f2f"
|
||||
BUTTON_RED_HOVER = "#b71c1c"
|
||||
BUTTON_BLUE = "#2d6ba0"
|
||||
BUTTON_BLUE_HOVER = "#1d5b90"
|
||||
BUTTON_GREEN = "#3a8545"
|
||||
BUTTON_GREEN_HOVER = "#2a7535"
|
||||
|
||||
# 对话框颜色
|
||||
DIALOG_BG_COLOR = "#2b2b2b"
|
||||
DIALOG_TEXT_COLOR = "#e0e0e0"
|
||||
|
||||
# 拖拽和选择颜色
|
||||
DRAG_HIGHLIGHT_COLOR = "#3584e4"
|
||||
DRAG_HIGHLIGHT_BG = "#2a3f52"
|
||||
SELECTION_BORDER = "#1e5a96" # 更深的蓝色边框
|
||||
SELECTION_BG = "#2d3441" # 选择时的背景色(更亮一些,保持可读性)
|
||||
|
||||
# ==================== 主窗口常量 ====================
|
||||
|
||||
# 滚动条颜色 - 与卡片颜色统一
|
||||
SCROLLBAR_COLOR = "#2b2b2b" # 与卡片背景色一致
|
||||
SCROLLBAR_HOVER_COLOR = "#3a3a3a" # 悬停时稍亮一些
|
||||
|
||||
# 分段按钮颜色
|
||||
SEGMENTED_BUTTON_SELECTED_COLOR = "#4a5568"
|
||||
SEGMENTED_BUTTON_SELECTED_HOVER_COLOR = "#2d3748"
|
||||
SEGMENTED_BUTTON_UNSELECTED_COLOR = "#3a3a3a"
|
||||
SEGMENTED_BUTTON_UNSELECTED_HOVER_COLOR = "#4a4a4a"
|
||||
|
||||
# 下拉菜单颜色
|
||||
DROPDOWN_FG_COLOR = "#2b2b2b"
|
||||
DROPDOWN_HOVER_COLOR = "#4a5568"
|
||||
|
||||
# ==================== 项目管理面板常量 ====================
|
||||
|
||||
# 项目面板背景颜色(与任务面板保持一致)
|
||||
PROJECT_PANEL_BG_LIGHT = "#3B4252"
|
||||
PROJECT_PANEL_BG_DARK = "#2E3440"
|
||||
|
||||
# ==================== 设置窗口常量 ====================
|
||||
|
||||
# 特殊按钮颜色
|
||||
SAVE_BUTTON_COLOR = "#2e7d32"
|
||||
SAVE_BUTTON_HOVER = "#1b5e20"
|
||||
SAVE_BUTTON_BORDER = "#34d058"
|
||||
|
||||
# ==================== 任务管理面板常量 ====================
|
||||
|
||||
# 任务面板颜色
|
||||
TASK_PANEL_BG_LIGHT = "#3B4252"
|
||||
TASK_PANEL_BG_DARK = "#2E3440"
|
||||
|
||||
# 重置按钮颜色
|
||||
RESET_BUTTON_BORDER = "#868e96"
|
||||
|
||||
# ==================== 节点编辑器常量 ====================
|
||||
|
||||
# 画布和网格颜色
|
||||
NODE_CANVAS_BG = "#1a202c"
|
||||
NODE_GRID_COLOR = "#2d3748"
|
||||
|
||||
# 节点颜色
|
||||
NODE_BG_COLOR = "#2d2d2d"
|
||||
NODE_BORDER_COLOR = "#3a3a3a"
|
||||
NODE_SELECTED_BORDER = "#00d9ff"
|
||||
NODE_ID_TEXT_COLOR = "#888888"
|
||||
|
||||
# 连接点和连接线颜色
|
||||
NODE_INPUT_COLOR = "#5a9fd4"
|
||||
NODE_OUTPUT_COLOR = "#10b981"
|
||||
NODE_CONNECTION_COLOR = "#5a9fd4"
|
||||
NODE_CONNECTION_SELECTED = "#ff6b6b"
|
||||
|
||||
# 节点颜色调色板
|
||||
NODE_COLOR_PALETTE = [
|
||||
"#5a9fd4", # 蓝色
|
||||
"#10b981", # 绿色
|
||||
"#d97706", # 橙色
|
||||
"#dc2626", # 红色
|
||||
"#8b5cf6", # 紫色
|
||||
"#ec4899", # 粉色
|
||||
"#06b6d4", # 青色
|
||||
"#f59e0b", # 黄色
|
||||
"#6366f1", # 靛蓝
|
||||
"#14b8a6", # 青绿
|
||||
"#f97316", # 深橙
|
||||
"#a855f7", # 紫罗兰
|
||||
]
|
||||
|
||||
# ==================== 通用尺寸常量 ====================
|
||||
|
||||
# 窗口尺寸常量
|
||||
# 主窗口尺寸
|
||||
CONSOLE_WINDOW_SIZE = "600x400"
|
||||
|
||||
# 设置窗口尺寸
|
||||
SETTINGS_WINDOW_SIZE = "650x800"
|
||||
|
||||
# SubFolder Editor 窗口尺寸
|
||||
SUBFOLDER_EDITOR_WINDOW_SIZE = "1200x900"
|
||||
SUBFOLDER_EDITOR_MIN_SIZE = (1000, 800)
|
||||
|
||||
# 对话框尺寸
|
||||
DIALOG_INPUT_SIZE = "400x220"
|
||||
DIALOG_CONFIRM_SIZE = "450x200"
|
||||
DIALOG_APP_EDIT_SIZE = "650x700"
|
||||
DIALOG_ICON_SELECT_SIZE = "600x400"
|
||||
DIALOG_MESSAGE_SIZE = "400x250"
|
||||
DIALOG_YES_NO_SIZE = "450x250"
|
||||
DIALOG_NODE_RENAME_SIZE = "400x180"
|
||||
|
||||
# 对话框尺寸 (width, height) - 保持向后兼容
|
||||
DIALOG_SIZE_SMALL = (400, 220)
|
||||
DIALOG_SIZE_MEDIUM = (450, 200)
|
||||
DIALOG_SIZE_LARGE = (650, 700)
|
||||
DIALOG_SIZE_XLARGE = (600, 400)
|
||||
|
||||
# 图标尺寸
|
||||
ICON_SIZE_TINY = 1
|
||||
ICON_SIZE_SMALL = 22
|
||||
ICON_SIZE_MEDIUM = 48
|
||||
ICON_SIZE_LARGE = 64
|
||||
ICON_SIZE_XLARGE = 128
|
||||
|
||||
# 按钮尺寸
|
||||
BUTTON_WIDTH_SMALL = 80
|
||||
BUTTON_WIDTH_MEDIUM = 100
|
||||
BUTTON_WIDTH_LARGE = 120
|
||||
BUTTON_HEIGHT_SMALL = 30
|
||||
BUTTON_HEIGHT_MEDIUM = 40
|
||||
|
||||
# 圆角半径
|
||||
CORNER_RADIUS_SMALL = 8
|
||||
CORNER_RADIUS_MEDIUM = 10
|
||||
CORNER_RADIUS_LARGE = 15
|
||||
|
||||
# 间距
|
||||
PADDING_SMALL = 5
|
||||
PADDING_MEDIUM = 10
|
||||
PADDING_LARGE = 20
|
||||
|
||||
# ==================== 字体常量 ====================
|
||||
|
||||
FONT_SIZE_TINY = 8
|
||||
FONT_SIZE_SMALL = 10
|
||||
FONT_SIZE_MEDIUM = 12
|
||||
FONT_SIZE_LARGE = 13
|
||||
FONT_SIZE_XLARGE = 16
|
||||
|
||||
# ==================== 应用配置常量 ====================
|
||||
|
||||
# 图标延迟设置时间
|
||||
ICON_DELAY_SHORT = 10
|
||||
ICON_DELAY_MEDIUM = 50
|
||||
ICON_DELAY_LONG = 200
|
||||
|
||||
# 默认窗口设置
|
||||
DEFAULT_ICON_SIZE = 80
|
||||
MIN_ICON_SIZE = 50
|
||||
MAX_ICON_SIZE = 150
|
||||
DEFAULT_WINDOW_WIDTH = 425
|
||||
DEFAULT_WINDOW_HEIGHT = 480
|
||||
MIN_WINDOW_WIDTH = 200
|
||||
MIN_WINDOW_HEIGHT = 200
|
||||
|
||||
# 任务栏高度(用于窗口定位)
|
||||
TASKBAR_HEIGHT = 80
|
||||
|
||||
# 网格列数
|
||||
MAX_GRID_COLUMNS = 7
|
||||
DEFAULT_GRID_COLUMNS = 3
|
||||
312
config/icon_config.py
Normal file
@@ -0,0 +1,312 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
图标和颜色配置管理器
|
||||
负责管理应用和项目的图标、颜色配置
|
||||
"""
|
||||
import os
|
||||
from typing import Dict, Optional
|
||||
|
||||
|
||||
class IconConfigManager:
|
||||
"""图标和颜色配置管理器"""
|
||||
|
||||
def __init__(self, config_data: Dict, get_icons_dir_func):
|
||||
"""
|
||||
初始化图标配置管理器
|
||||
|
||||
Args:
|
||||
config_data: 配置数据字典的引用
|
||||
get_icons_dir_func: 获取图标目录的函数
|
||||
"""
|
||||
self.config_data = config_data
|
||||
self._get_icons_dir = get_icons_dir_func
|
||||
|
||||
# ==================== 应用图标管理 ====================
|
||||
|
||||
def get_app_icon(self, app_path: str) -> str:
|
||||
"""
|
||||
获取应用图标路径
|
||||
|
||||
如果配置中保存的是相对路径(只有文件名),则自动拼接 icons 目录
|
||||
|
||||
Args:
|
||||
app_path: 应用路径
|
||||
|
||||
Returns:
|
||||
图标的完整路径,如果不存在则返回空字符串
|
||||
"""
|
||||
icon_value = self.config_data.get("app_icons", {}).get(app_path, "")
|
||||
|
||||
if not icon_value:
|
||||
return ""
|
||||
|
||||
# 如果是绝对路径且存在,直接返回
|
||||
if os.path.isabs(icon_value) and os.path.exists(icon_value):
|
||||
return icon_value
|
||||
|
||||
# 如果是相对路径(只有文件名),拼接 icons 目录
|
||||
if not os.path.isabs(icon_value):
|
||||
icons_dir = self._get_icons_dir()
|
||||
full_path = os.path.join(icons_dir, icon_value)
|
||||
if os.path.exists(full_path):
|
||||
return full_path
|
||||
|
||||
# 如果都不存在,返回原值(可能是旧的绝对路径)
|
||||
return icon_value
|
||||
|
||||
def set_app_icon(self, app_path: str, icon_path: str) -> bool:
|
||||
"""
|
||||
设置应用图标路径
|
||||
|
||||
如果图标在 icons 目录下,只保存文件名(相对路径)
|
||||
否则保存完整路径
|
||||
|
||||
Args:
|
||||
app_path: 应用路径
|
||||
icon_path: 图标路径
|
||||
|
||||
Returns:
|
||||
是否设置成功
|
||||
"""
|
||||
if "app_icons" not in self.config_data:
|
||||
self.config_data["app_icons"] = {}
|
||||
|
||||
# 获取 icons 目录
|
||||
icons_dir = self._get_icons_dir()
|
||||
|
||||
# 如果图标在 icons 目录下,只保存文件名
|
||||
try:
|
||||
# 标准化路径以便比较
|
||||
icon_path_normalized = os.path.normpath(icon_path)
|
||||
icons_dir_normalized = os.path.normpath(icons_dir)
|
||||
|
||||
# 检查是否在 icons 目录下
|
||||
if icon_path_normalized.startswith(icons_dir_normalized):
|
||||
# 只保存文件名
|
||||
icon_filename = os.path.basename(icon_path)
|
||||
self.config_data["app_icons"][app_path] = icon_filename
|
||||
print(f"[OK] Saved icon as relative path: {icon_filename}")
|
||||
else:
|
||||
# 保存完整路径
|
||||
self.config_data["app_icons"][app_path] = icon_path
|
||||
print(f"[OK] Saved icon as absolute path: {icon_path}")
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[WARNING] Error processing icon path: {e}")
|
||||
# 出错时保存完整路径
|
||||
self.config_data["app_icons"][app_path] = icon_path
|
||||
return False
|
||||
|
||||
def remove_app_icon(self, app_path: str) -> bool:
|
||||
"""
|
||||
移除应用图标设置
|
||||
|
||||
Args:
|
||||
app_path: 应用路径
|
||||
|
||||
Returns:
|
||||
是否移除成功
|
||||
"""
|
||||
if app_path in self.config_data.get("app_icons", {}):
|
||||
del self.config_data["app_icons"][app_path]
|
||||
return True
|
||||
return False
|
||||
|
||||
# ==================== 项目图标管理 ====================
|
||||
|
||||
def get_project_icon(self, project_name: str) -> str:
|
||||
"""
|
||||
获取项目图标路径
|
||||
|
||||
如果配置中保存的是相对路径(只有文件名),则自动拼接 icons 目录
|
||||
|
||||
Args:
|
||||
project_name: 项目名称
|
||||
|
||||
Returns:
|
||||
图标的完整路径,如果不存在则返回空字符串
|
||||
"""
|
||||
# 从项目配置中获取图标路径
|
||||
projects = self.config_data.get("projects", {})
|
||||
project_config = projects.get(project_name, {})
|
||||
icon_value = project_config.get("icon", "")
|
||||
|
||||
if not icon_value:
|
||||
return ""
|
||||
|
||||
# 如果是绝对路径且存在,直接返回
|
||||
if os.path.isabs(icon_value) and os.path.exists(icon_value):
|
||||
return icon_value
|
||||
|
||||
# 如果是相对路径(只有文件名),拼接 icons 目录
|
||||
if not os.path.isabs(icon_value):
|
||||
icons_dir = self._get_icons_dir()
|
||||
full_path = os.path.join(icons_dir, icon_value)
|
||||
if os.path.exists(full_path):
|
||||
return full_path
|
||||
|
||||
# 如果都不存在,返回原值(可能是旧的绝对路径)
|
||||
return icon_value
|
||||
|
||||
def set_project_icon(self, project_name: str, icon_path: str) -> bool:
|
||||
"""
|
||||
设置项目图标路径
|
||||
|
||||
如果图标在 icons 目录下,只保存文件名(相对路径)
|
||||
否则保存完整路径
|
||||
|
||||
Args:
|
||||
project_name: 项目名称
|
||||
icon_path: 图标路径
|
||||
|
||||
Returns:
|
||||
是否设置成功
|
||||
"""
|
||||
# 确保projects配置存在
|
||||
if "projects" not in self.config_data:
|
||||
self.config_data["projects"] = {}
|
||||
|
||||
# 确保项目配置存在
|
||||
if project_name not in self.config_data["projects"]:
|
||||
self.config_data["projects"][project_name] = {
|
||||
"apps": [],
|
||||
"icon": "",
|
||||
"color": ""
|
||||
}
|
||||
|
||||
# 获取 icons 目录
|
||||
icons_dir = self._get_icons_dir()
|
||||
|
||||
# 如果图标在 icons 目录下,只保存文件名
|
||||
try:
|
||||
# 标准化路径以便比较
|
||||
icon_path_normalized = os.path.normpath(icon_path)
|
||||
icons_dir_normalized = os.path.normpath(icons_dir)
|
||||
|
||||
# 检查是否在 icons 目录下
|
||||
if icon_path_normalized.startswith(icons_dir_normalized):
|
||||
# 只保存文件名
|
||||
icon_filename = os.path.basename(icon_path)
|
||||
self.config_data["projects"][project_name]["icon"] = icon_filename
|
||||
print(f"[OK] Saved project icon as relative path: {icon_filename}")
|
||||
else:
|
||||
# 保存完整路径
|
||||
self.config_data["projects"][project_name]["icon"] = icon_path
|
||||
print(f"[OK] Saved project icon as absolute path: {icon_path}")
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[WARNING] Error processing project icon path: {e}")
|
||||
# 出错时保存完整路径
|
||||
self.config_data["projects"][project_name]["icon"] = icon_path
|
||||
return False
|
||||
|
||||
def remove_project_icon(self, project_name: str) -> bool:
|
||||
"""
|
||||
移除项目图标设置
|
||||
|
||||
Args:
|
||||
project_name: 项目名称
|
||||
|
||||
Returns:
|
||||
是否移除成功
|
||||
"""
|
||||
projects = self.config_data.get("projects", {})
|
||||
if project_name in projects and "icon" in projects[project_name]:
|
||||
projects[project_name]["icon"] = ""
|
||||
return True
|
||||
return False
|
||||
|
||||
# ==================== 应用颜色管理 ====================
|
||||
|
||||
def get_app_color(self, app_path: str) -> str:
|
||||
"""
|
||||
获取应用按钮颜色
|
||||
|
||||
Args:
|
||||
app_path: 应用路径
|
||||
|
||||
Returns:
|
||||
颜色值(十六进制),如果未设置则返回空字符串
|
||||
"""
|
||||
return self.config_data.get("app_colors", {}).get(app_path, "")
|
||||
|
||||
def set_app_color(self, app_path: str, color: str) -> bool:
|
||||
"""
|
||||
设置应用按钮颜色
|
||||
|
||||
Args:
|
||||
app_path: 应用路径
|
||||
color: 颜色值(十六进制)
|
||||
|
||||
Returns:
|
||||
是否设置成功
|
||||
"""
|
||||
if "app_colors" not in self.config_data:
|
||||
self.config_data["app_colors"] = {}
|
||||
|
||||
self.config_data["app_colors"][app_path] = color
|
||||
return True
|
||||
|
||||
def remove_app_color(self, app_path: str) -> bool:
|
||||
"""
|
||||
移除应用按钮颜色设置
|
||||
|
||||
Args:
|
||||
app_path: 应用路径
|
||||
|
||||
Returns:
|
||||
是否移除成功
|
||||
"""
|
||||
if app_path in self.config_data.get("app_colors", {}):
|
||||
del self.config_data["app_colors"][app_path]
|
||||
return True
|
||||
return False
|
||||
|
||||
# ==================== 项目颜色管理 ====================
|
||||
|
||||
def get_project_color(self, project_name: str) -> str:
|
||||
"""
|
||||
获取项目背景颜色
|
||||
|
||||
Args:
|
||||
project_name: 项目名称
|
||||
|
||||
Returns:
|
||||
颜色值(十六进制),默认为 #2b4c6f
|
||||
"""
|
||||
return self.config_data.get("project_colors", {}).get(project_name, "#2b4c6f")
|
||||
|
||||
def set_project_color(self, project_name: str, color: str) -> bool:
|
||||
"""
|
||||
设置项目背景颜色
|
||||
|
||||
Args:
|
||||
project_name: 项目名称
|
||||
color: 颜色值(十六进制)
|
||||
|
||||
Returns:
|
||||
是否设置成功
|
||||
"""
|
||||
if "project_colors" not in self.config_data:
|
||||
self.config_data["project_colors"] = {}
|
||||
|
||||
self.config_data["project_colors"][project_name] = color
|
||||
return True
|
||||
|
||||
def remove_project_color(self, project_name: str) -> bool:
|
||||
"""
|
||||
移除项目背景颜色设置
|
||||
|
||||
Args:
|
||||
project_name: 项目名称
|
||||
|
||||
Returns:
|
||||
是否移除成功
|
||||
"""
|
||||
if project_name in self.config_data.get("project_colors", {}):
|
||||
del self.config_data["project_colors"][project_name]
|
||||
return True
|
||||
return False
|
||||
298
docs/ARTIST_GUIDE.md
Normal file
@@ -0,0 +1,298 @@
|
||||
# NexusLauncher 使用指南
|
||||
|
||||
> **现代化项目管理与应用启动工具**
|
||||
> 版本: v1.2.0 | 更新: 2025年11月
|
||||
|
||||
---
|
||||
|
||||
## 📑 目录
|
||||
|
||||
- [快速开始](#快速开始)
|
||||
- [核心功能](#核心功能)
|
||||
- [项目管理](#项目管理)
|
||||
- [任务管理](#任务管理)
|
||||
- [应用管理](#应用管理)
|
||||
- [快捷键](#快捷键)
|
||||
- [常见问题](#常见问题)
|
||||
|
||||
---
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 安装
|
||||
|
||||
1. **下载并解压** `NexusLauncher.exe` 到任意目录
|
||||
2. **双击运行**,程序将自动创建配置文件
|
||||
3. **首次启动**会创建示例项目 "Project_01"
|
||||
|
||||
### 5 分钟上手
|
||||
|
||||
#### 1️⃣ 添加常用软件
|
||||
|
||||
```
|
||||
主窗口 → ⚙ 设置 → + 添加应用
|
||||
```
|
||||
|
||||
填写信息:
|
||||
- **名称**: Maya 2025
|
||||
- **路径**: `C:\Program Files\Autodesk\Maya2025\bin\maya.exe`
|
||||
- **版本**: 2025.1
|
||||
|
||||
#### 2️⃣ 创建项目
|
||||
|
||||
```
|
||||
设置窗口 → 新建项目 → 输入项目名称
|
||||
```
|
||||
|
||||
项目命名建议:
|
||||
- `Character_Hero`
|
||||
- `Environment_Forest`
|
||||
- `Weapon_Sword`
|
||||
|
||||
#### 3️⃣ 创建任务文件夹
|
||||
|
||||
```
|
||||
Task 标签页 → 选择任务类型 → 设置工作空间 → Create Task Folder
|
||||
```
|
||||
|
||||
#### 4️⃣ 启动应用
|
||||
|
||||
```
|
||||
Project 标签页 → 点击应用图标
|
||||
```
|
||||
|
||||
💡 **提示**: 使用 `Ctrl + 滚轮` 调整图标大小
|
||||
|
||||
---
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 🎨 项目管理
|
||||
- 多项目支持,快速切换
|
||||
- 项目个性化(图标、颜色)
|
||||
- 自动保存配置
|
||||
|
||||
### 📁 任务管理
|
||||
- 7 种预设任务模板(Character、Weapon、Prop 等)
|
||||
- 可视化节点编辑器
|
||||
- 一键创建标准化文件夹结构
|
||||
|
||||
### 🚀 应用启动
|
||||
- 快速启动常用软件
|
||||
- 图标缩放(`Ctrl + 滚轮`)
|
||||
- 拖拽排序
|
||||
|
||||
### 界面布局
|
||||
|
||||
```
|
||||
┌──────────────────────────────┐
|
||||
│ NexusLauncher ⚙ 设置 × │
|
||||
├──────────────────────────────┤
|
||||
│ 当前项目: [Project_01 ▼] │
|
||||
├──────────────────────────────┤
|
||||
│ Project │ Task │
|
||||
├───────────┴──────────────────┤
|
||||
│ [应用图标] 或 [任务管理] │
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 项目管理
|
||||
|
||||
### 基本操作
|
||||
|
||||
#### 创建项目
|
||||
```
|
||||
⚙ 设置 → 新建项目 → 输入名称
|
||||
```
|
||||
|
||||
**命名建议**:
|
||||
- `Character_角色名`
|
||||
- `Environment_场景名`
|
||||
- `Weapon_武器名`
|
||||
- `Prop_道具名`
|
||||
|
||||
#### 项目功能
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| **新建** | 创建新项目 |
|
||||
| **复制** | 复制现有项目 |
|
||||
| **重命名** | 修改项目名 |
|
||||
| **删除** | 删除项目 |
|
||||
| **图标** | 自定义图标 |
|
||||
| **颜色** | 设置主题色 |
|
||||
|
||||
#### 快速切换
|
||||
- 顶部下拉菜单
|
||||
- `Ctrl + Tab` 快捷键
|
||||
- 自动保存状态
|
||||
|
||||
---
|
||||
|
||||
## 任务管理
|
||||
|
||||
### 预设模板
|
||||
|
||||
| 任务类型 | 适用场景 |
|
||||
|---------|----------|
|
||||
| **Character** | 角色制作 |
|
||||
| **Weapon** | 武器制作 |
|
||||
| **Prop** | 道具制作 |
|
||||
| **Environment** | 环境制作 |
|
||||
| **Animation** | 动画制作 |
|
||||
| **Rigging** | 绑定制作 |
|
||||
| **Other** | 其他类型 |
|
||||
|
||||
### Character 模板结构
|
||||
|
||||
```
|
||||
TaskFolder_Character_001/
|
||||
├── Reference/ # 参考资料
|
||||
├── MP/ # 中精度模型
|
||||
├── HP/ # 高精度雕刻
|
||||
├── LP/ # 低精度模型
|
||||
├── Baking/ # 烘焙流程
|
||||
├── Texture/ # 贴图制作
|
||||
├── FBX/ # 最终资产
|
||||
└── Screenshot/ # 展示截图
|
||||
```
|
||||
|
||||
### 创建流程
|
||||
|
||||
```
|
||||
1. Task 标签页 → 选择任务类型
|
||||
2. Browse → 选择工作空间
|
||||
3. 输入任务名称
|
||||
4. Create Task Folder
|
||||
5. Open in Explorer 查看结果
|
||||
```
|
||||
|
||||
### 自定义结构
|
||||
|
||||
使用 **SubFolder Editor** 节点编辑器:
|
||||
- 右键节点 → 添加子节点
|
||||
- `F2` 重命名
|
||||
- `Delete` 删除
|
||||
- `Ctrl + S` 保存
|
||||
|
||||
---
|
||||
|
||||
## SubFolder Editor 节点编辑器
|
||||
|
||||
### 基本操作
|
||||
|
||||
| 操作 | 快捷键 | 功能 |
|
||||
|------|--------|------|
|
||||
| **移动节点** | 拖拽 | 改变位置 |
|
||||
| **重命名** | `F2` | 重命名节点 |
|
||||
| **删除** | `Delete` | 删除节点 |
|
||||
| **复制** | `Ctrl + D` | 复制节点 |
|
||||
| **添加子节点** | 右键菜单 | 添加子文件夹 |
|
||||
| **缩放视图** | `Ctrl + 滚轮` | 缩放 |
|
||||
| **平移视图** | 中键拖拽 | 移动 |
|
||||
| **居中** | `Home` | 居中显示 |
|
||||
| **保存** | `Ctrl + S` | 保存结构 |
|
||||
|
||||
💡 **提示**: TaskFolder 是根节点,不可删除或重命名
|
||||
|
||||
---
|
||||
|
||||
## 应用管理
|
||||
|
||||
### 添加应用
|
||||
|
||||
```
|
||||
⚙ 设置 → + 添加应用 → 填写信息 → 保存
|
||||
```
|
||||
|
||||
**必填信息**:
|
||||
- **名称**: Maya 2025
|
||||
- **路径**: 可执行文件完整路径
|
||||
- **版本**: 2025.1
|
||||
|
||||
### 常用软件路径
|
||||
|
||||
| 软件 | 默认路径 |
|
||||
|------|---------|
|
||||
| **Maya 2025** | `C:\Program Files\Autodesk\Maya2025\bin\maya.exe` |
|
||||
| **Blender** | `C:\Program Files\Blender Foundation\Blender 4.2\blender.exe` |
|
||||
| **ZBrush** | `C:\Program Files\Maxon\ZBrush 2025\ZBrush.exe` |
|
||||
| **Substance Painter** | `C:\Program Files\Adobe\Adobe Substance 3D Painter\...` |
|
||||
| **Photoshop** | `C:\Program Files\Adobe\Adobe Photoshop 2025\Photoshop.exe` |
|
||||
| **Unreal Engine** | `C:\Program Files\Epic Games\UE_5.4\Engine\Binaries\Win64\UnrealEditor.exe` |
|
||||
|
||||
### 批量操作
|
||||
|
||||
| 操作 | 方法 |
|
||||
|------|------|
|
||||
| **多选** | `Ctrl + 点击` |
|
||||
| **全选** | 右键 → 全选 |
|
||||
| **删除** | `Delete` 键 |
|
||||
| **排序** | 拖拽左侧手柄 |
|
||||
| **取消** | `Escape` 键 |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 快捷键
|
||||
|
||||
### 全局快捷键
|
||||
|
||||
| 快捷键 | 功能 |
|
||||
|--------|------|
|
||||
| `Ctrl + Tab` | 切换项目 |
|
||||
| `Ctrl + 滚轮` | 缩放图标/视图 |
|
||||
| `Ctrl + 点击` | 多选 |
|
||||
| `Delete` | 删除 |
|
||||
| `Escape` | 取消选择 |
|
||||
| `F2` | 重命名 |
|
||||
| `Ctrl + D` | 复制 |
|
||||
| `Ctrl + S` | 保存 |
|
||||
| `Home` | 居中显示 |
|
||||
| `中键拖拽` | 平移视图 |
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
**Q: 程序启动后没有反应?**
|
||||
- 检查杀毒软件是否拦截
|
||||
- 确保 `config.json` 有读写权限
|
||||
- 尝试以管理员权限运行
|
||||
|
||||
**Q: 应用启动失败?**
|
||||
- 检查应用路径是否正确
|
||||
- 确认应用程序存在
|
||||
- 尝试手动启动测试
|
||||
|
||||
**Q: 如何备份设置?**
|
||||
- 备份 `config.json` 文件即可
|
||||
|
||||
**Q: TaskFolder 可以删除吗?**
|
||||
- 不可以,TaskFolder 是根节点
|
||||
|
||||
---
|
||||
|
||||
## 系统要求
|
||||
|
||||
**最低配置**:
|
||||
- Windows 10+
|
||||
- 4GB RAM
|
||||
- 100MB 空间
|
||||
|
||||
**推荐配置**:
|
||||
- Windows 11
|
||||
- 8GB+ RAM
|
||||
- 1920x1080 分辨率
|
||||
|
||||
---
|
||||
|
||||
**💡 提示**: 充分利用快捷键和节点编辑器,提升工作效率!
|
||||
|
||||
**版本**: v1.2.0 | **更新**: 2025年11月
|
||||
|
||||
---
|
||||
|
||||
*NexusLauncher - 让项目管理更简单* 🚀
|
||||
577
docs/CUSTOM_PLUGIN_GUIDE.md
Normal file
@@ -0,0 +1,577 @@
|
||||
# Maya 自定义插件添加指南
|
||||
|
||||
本指南将帮助你在 NexusLauncher 的 Maya 插件系统中添加自己的工具和插件。
|
||||
|
||||
---
|
||||
|
||||
## 📁 目录结构说明
|
||||
|
||||
```
|
||||
template/plugins/maya/2023/
|
||||
├── shelves/ # 工具架文件(.mel 格式)
|
||||
├── scripts/ # Python/MEL 脚本
|
||||
├── plug-ins/ # Maya 插件文件(.py 或 .mll)
|
||||
├── icons/ # 工具图标
|
||||
└── README.md # 基础说明文档
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 添加方式概览
|
||||
|
||||
根据你的需求,有三种主要的添加方式:
|
||||
|
||||
| 方式 | 适用场景 | 难度 |
|
||||
|------|---------|------|
|
||||
| **1. 添加工具架按钮** | 快速添加 Python 脚本工具 | ⭐ 简单 |
|
||||
| **2. 添加 Maya 插件** | 创建 Maya 命令或节点 | ⭐⭐ 中等 |
|
||||
| **3. 添加启动脚本** | 自动执行初始化代码 | ⭐ 简单 |
|
||||
|
||||
---
|
||||
|
||||
## 方式 1: 添加工具架按钮
|
||||
|
||||
### 适用场景
|
||||
- 你有一个 Python 脚本工具想要快速访问
|
||||
- 需要在工具架上添加按钮
|
||||
- 不需要注册 Maya 命令
|
||||
|
||||
### 步骤
|
||||
|
||||
#### 1.1 准备你的 Python 脚本
|
||||
|
||||
将你的 Python 脚本放到 `scripts/` 目录:
|
||||
|
||||
```
|
||||
scripts/
|
||||
├── userSetup.py # 启动脚本(已存在)
|
||||
├── nexus_test.py # 示例脚本(已存在)
|
||||
└── my_custom_tool.py # 👈 你的新脚本
|
||||
```
|
||||
|
||||
**示例脚本** (`my_custom_tool.py`):
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
我的自定义工具
|
||||
"""
|
||||
import maya.cmds as cmds
|
||||
|
||||
def run_my_tool():
|
||||
"""执行我的工具"""
|
||||
# 你的工具逻辑
|
||||
cmds.confirmDialog(
|
||||
title='My Tool',
|
||||
message='Hello from my custom tool!',
|
||||
button=['OK']
|
||||
)
|
||||
print("[MyTool] Tool executed successfully")
|
||||
|
||||
def show_ui():
|
||||
"""显示工具界面"""
|
||||
# 如果有 UI,在这里创建
|
||||
pass
|
||||
```
|
||||
|
||||
#### 1.2 准备图标(可选)
|
||||
|
||||
将图标文件放到 `icons/` 目录:
|
||||
|
||||
```
|
||||
icons/
|
||||
├── nexus_test.png # 示例图标(已存在)
|
||||
└── my_tool_icon.png # 👈 你的图标(32x32 或 64x64 PNG)
|
||||
```
|
||||
|
||||
> **提示**: 如果没有图标,可以使用文字标签代替
|
||||
|
||||
#### 1.3 编辑工具架文件
|
||||
|
||||
编辑 `shelves/shelf_NexusLauncher.mel`,在文件末尾的 `}` 之前添加新按钮:
|
||||
|
||||
```mel
|
||||
shelfButton
|
||||
-enableCommandRepeat 1
|
||||
-flexibleWidthType 3
|
||||
-flexibleWidthValue 32
|
||||
-enable 1
|
||||
-width 35
|
||||
-height 34
|
||||
-manage 1
|
||||
-visible 1
|
||||
-preventOverride 0
|
||||
-annotation "我的自定义工具 - 点击运行"
|
||||
-enableBackground 0
|
||||
-backgroundColor 0 0 0
|
||||
-highlightColor 0.321569 0.521569 0.65098
|
||||
-align "center"
|
||||
-label "MT"
|
||||
-labelOffset 0
|
||||
-rotation 0
|
||||
-flipX 0
|
||||
-flipY 0
|
||||
-useAlpha 1
|
||||
-font "boldLabelFont"
|
||||
-imageOverlayLabel "MT"
|
||||
-overlayLabelColor 1 1 1
|
||||
-overlayLabelBackColor 0.2 0.8 0.5 0.9
|
||||
-image "my_tool_icon.png"
|
||||
-image1 "my_tool_icon.png"
|
||||
-style "iconOnly"
|
||||
-marginWidth 0
|
||||
-marginHeight 1
|
||||
-command "import my_custom_tool\nmy_custom_tool.run_my_tool()"
|
||||
-sourceType "python"
|
||||
-commandRepeatable 1
|
||||
-flat 1
|
||||
;
|
||||
```
|
||||
|
||||
**关键参数说明**:
|
||||
- `-annotation`: 鼠标悬停时的提示文字
|
||||
- `-label`: 按钮文字标签(如果没有图标会显示)
|
||||
- `-imageOverlayLabel`: 图标上的文字叠加层
|
||||
- `-overlayLabelBackColor`: 文字背景颜色 (R G B Alpha)
|
||||
- `-image`: 图标文件名(在 icons/ 目录中)
|
||||
- `-command`: 点击按钮时执行的 Python 代码
|
||||
|
||||
#### 1.4 测试
|
||||
|
||||
1. 通过 NexusLauncher 启动 Maya 2023
|
||||
2. 检查 NexusLauncher 工具架是否出现新按钮
|
||||
3. 点击按钮测试功能
|
||||
|
||||
---
|
||||
|
||||
## 方式 2: 添加 Maya 插件
|
||||
|
||||
### 适用场景
|
||||
- 需要注册自定义 Maya 命令
|
||||
- 需要创建自定义节点
|
||||
- 需要更深度的 Maya API 集成
|
||||
|
||||
### 步骤
|
||||
|
||||
#### 2.1 创建插件文件
|
||||
|
||||
在 `plug-ins/` 目录创建你的插件文件:
|
||||
|
||||
```
|
||||
plug-ins/
|
||||
├── nexus_example_plugin.py # 示例插件(已存在)
|
||||
└── my_custom_plugin.py # 👈 你的新插件
|
||||
```
|
||||
|
||||
**插件模板** (`my_custom_plugin.py`):
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
我的自定义 Maya 插件
|
||||
"""
|
||||
import sys
|
||||
import maya.api.OpenMaya as om
|
||||
|
||||
|
||||
def maya_useNewAPI():
|
||||
"""告诉 Maya 使用 Python API 2.0"""
|
||||
pass
|
||||
|
||||
|
||||
class MyCustomCommand(om.MPxCommand):
|
||||
"""自定义命令类"""
|
||||
|
||||
kPluginCmdName = 'myCustomCmd' # 命令名称
|
||||
|
||||
def __init__(self):
|
||||
om.MPxCommand.__init__(self)
|
||||
|
||||
@staticmethod
|
||||
def cmdCreator():
|
||||
return MyCustomCommand()
|
||||
|
||||
def doIt(self, args):
|
||||
"""执行命令"""
|
||||
print(f'[MyPlugin] Custom command executed!')
|
||||
om.MGlobal.displayInfo('My custom plugin is working!')
|
||||
|
||||
# 在这里添加你的命令逻辑
|
||||
# 例如:创建对象、修改场景等
|
||||
|
||||
|
||||
def initializePlugin(plugin):
|
||||
"""初始化插件"""
|
||||
pluginFn = om.MFnPlugin(plugin, 'YourName', '1.0', 'Any')
|
||||
try:
|
||||
pluginFn.registerCommand(
|
||||
MyCustomCommand.kPluginCmdName,
|
||||
MyCustomCommand.cmdCreator
|
||||
)
|
||||
print(f'[MyPlugin] Plugin loaded: {MyCustomCommand.kPluginCmdName}')
|
||||
except:
|
||||
sys.stderr.write(f'Failed to register command: {MyCustomCommand.kPluginCmdName}')
|
||||
raise
|
||||
|
||||
|
||||
def uninitializePlugin(plugin):
|
||||
"""卸载插件"""
|
||||
pluginFn = om.MFnPlugin(plugin)
|
||||
try:
|
||||
pluginFn.deregisterCommand(MyCustomCommand.kPluginCmdName)
|
||||
print(f'[MyPlugin] Plugin unloaded: {MyCustomCommand.kPluginCmdName}')
|
||||
except:
|
||||
sys.stderr.write(f'Failed to deregister command: {MyCustomCommand.kPluginCmdName}')
|
||||
raise
|
||||
```
|
||||
|
||||
#### 2.2 配置自动加载
|
||||
|
||||
编辑 `scripts/userSetup.py`,在 `load_nexus_plugins()` 函数中添加你的插件:
|
||||
|
||||
```python
|
||||
def load_nexus_plugins():
|
||||
"""Load NexusLauncher plugins"""
|
||||
try:
|
||||
plugin_path = os.environ.get('MAYA_PLUG_IN_PATH', '')
|
||||
|
||||
if not plugin_path:
|
||||
print("[NexusLauncher] MAYA_PLUG_IN_PATH not set, skipping plugin load")
|
||||
return
|
||||
|
||||
print(f"[NexusLauncher] MAYA_PLUG_IN_PATH: {plugin_path}")
|
||||
|
||||
# 要加载的插件列表
|
||||
plugins_to_load = [
|
||||
"nexus_example_plugin.py",
|
||||
"my_custom_plugin.py", # 👈 添加你的插件
|
||||
]
|
||||
|
||||
for plugin_file in plugins_to_load:
|
||||
if cmds.pluginInfo(plugin_file, query=True, loaded=True):
|
||||
print(f"[NexusLauncher] Plugin already loaded: {plugin_file}")
|
||||
else:
|
||||
try:
|
||||
cmds.loadPlugin(plugin_file)
|
||||
print(f"[NexusLauncher] ✓ Loaded plugin: {plugin_file}")
|
||||
|
||||
# 设置为自动加载
|
||||
cmds.pluginInfo(plugin_file, edit=True, autoload=True)
|
||||
print(f"[NexusLauncher] ✓ Set plugin to auto-load")
|
||||
except Exception as e:
|
||||
print(f"[NexusLauncher] Failed to load plugin {plugin_file}: {e}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[NexusLauncher] Error loading plugins: {e}")
|
||||
```
|
||||
|
||||
#### 2.3 测试插件
|
||||
|
||||
1. 通过 NexusLauncher 启动 Maya 2023
|
||||
2. 检查脚本编辑器输出,确认插件已加载
|
||||
3. 在 Maya 命令行或脚本编辑器中测试命令:
|
||||
|
||||
```python
|
||||
import maya.cmds as cmds
|
||||
cmds.myCustomCmd() # 执行你的自定义命令
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 方式 3: 添加启动脚本
|
||||
|
||||
### 适用场景
|
||||
- 需要在 Maya 启动时自动执行某些操作
|
||||
- 设置环境变量或全局配置
|
||||
- 加载第三方库
|
||||
|
||||
### 步骤
|
||||
|
||||
#### 3.1 编辑 userSetup.py
|
||||
|
||||
在 `scripts/userSetup.py` 文件末尾添加你的初始化代码:
|
||||
|
||||
```python
|
||||
def my_custom_startup():
|
||||
"""我的自定义启动函数"""
|
||||
try:
|
||||
print("[MyStartup] Running custom startup code...")
|
||||
|
||||
# 在这里添加你的启动逻辑
|
||||
# 例如:
|
||||
# - 设置默认渲染器
|
||||
# - 加载常用插件
|
||||
# - 配置工作区
|
||||
# - 连接到资产管理系统
|
||||
|
||||
import maya.cmds as cmds
|
||||
|
||||
# 示例:设置默认单位为厘米
|
||||
cmds.currentUnit(linear='cm')
|
||||
print("[MyStartup] ✓ Set default unit to cm")
|
||||
|
||||
# 示例:设置默认时间单位为 24fps
|
||||
cmds.currentUnit(time='film')
|
||||
print("[MyStartup] ✓ Set default time unit to 24fps")
|
||||
|
||||
print("[MyStartup] ✓ Custom startup completed")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[MyStartup] Error during startup: {e}")
|
||||
|
||||
|
||||
# 在 Maya 启动完成后执行
|
||||
cmds.evalDeferred(my_custom_startup)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 高级技巧
|
||||
|
||||
### 1. 动态重载工具架
|
||||
|
||||
如果你在开发过程中频繁修改工具架,可以使用 `RELOAD_SHELF.py` 快速重载:
|
||||
|
||||
```python
|
||||
# 在 Maya 脚本编辑器中执行
|
||||
import sys
|
||||
sys.path.append(r'E:\Zoroot\Dev\NexusLauncher\template\plugins\maya\2023')
|
||||
import RELOAD_SHELF
|
||||
RELOAD_SHELF.reload_shelf()
|
||||
```
|
||||
|
||||
### 2. 使用相对导入
|
||||
|
||||
如果你的工具有多个模块,可以创建包结构:
|
||||
|
||||
```
|
||||
scripts/
|
||||
├── my_tool/
|
||||
│ ├── __init__.py
|
||||
│ ├── core.py
|
||||
│ ├── ui.py
|
||||
│ └── utils.py
|
||||
└── userSetup.py
|
||||
```
|
||||
|
||||
在工具架按钮中使用:
|
||||
|
||||
```mel
|
||||
-command "from my_tool import ui\nui.show_window()"
|
||||
```
|
||||
|
||||
### 3. 添加菜单项
|
||||
|
||||
除了工具架按钮,你还可以在 `userSetup.py` 中添加自定义菜单:
|
||||
|
||||
```python
|
||||
def create_custom_menu():
|
||||
"""创建自定义菜单"""
|
||||
try:
|
||||
import maya.cmds as cmds
|
||||
|
||||
# 检查菜单是否已存在
|
||||
if cmds.menu('NexusMenu', exists=True):
|
||||
cmds.deleteUI('NexusMenu')
|
||||
|
||||
# 创建菜单
|
||||
main_window = mel.eval('$tmpVar=$gMainWindow')
|
||||
custom_menu = cmds.menu(
|
||||
'NexusMenu',
|
||||
label='NexusTools',
|
||||
parent=main_window,
|
||||
tearOff=True
|
||||
)
|
||||
|
||||
# 添加菜单项
|
||||
cmds.menuItem(
|
||||
label='My Tool',
|
||||
command='import my_custom_tool; my_custom_tool.run_my_tool()',
|
||||
parent=custom_menu
|
||||
)
|
||||
|
||||
cmds.menuItem(divider=True, parent=custom_menu)
|
||||
|
||||
cmds.menuItem(
|
||||
label='About',
|
||||
command='cmds.confirmDialog(title="About", message="NexusTools v1.0")',
|
||||
parent=custom_menu
|
||||
)
|
||||
|
||||
print("[NexusLauncher] ✓ Created custom menu")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[NexusLauncher] Error creating menu: {e}")
|
||||
|
||||
# 在启动时执行
|
||||
cmds.evalDeferred(create_custom_menu)
|
||||
```
|
||||
|
||||
### 4. 调试技巧
|
||||
|
||||
**查看环境变量**:
|
||||
```python
|
||||
import os
|
||||
print(os.environ.get('MAYA_SHELF_PATH'))
|
||||
print(os.environ.get('MAYA_PLUG_IN_PATH'))
|
||||
```
|
||||
|
||||
**检查插件加载状态**:
|
||||
```python
|
||||
import maya.cmds as cmds
|
||||
print(cmds.pluginInfo(query=True, listPlugins=True))
|
||||
```
|
||||
|
||||
**查看工具架列表**:
|
||||
```python
|
||||
import maya.cmds as cmds
|
||||
print(cmds.lsUI(type='shelfLayout'))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 常见问题
|
||||
|
||||
### Q1: 工具架按钮不显示图标?
|
||||
|
||||
**A**: 检查以下几点:
|
||||
1. 图标文件是否在 `icons/` 目录中
|
||||
2. 文件名是否正确(区分大小写)
|
||||
3. 图标格式是否为 PNG
|
||||
4. `XBMLANGPATH` 环境变量是否正确设置
|
||||
|
||||
### Q2: Python 脚本找不到模块?
|
||||
|
||||
**A**: 确保:
|
||||
1. 脚本在 `scripts/` 目录中
|
||||
2. `PYTHONPATH` 和 `MAYA_SCRIPT_PATH` 已正确设置
|
||||
3. 使用 `import` 时不需要包含 `.py` 后缀
|
||||
|
||||
### Q3: 插件加载失败?
|
||||
|
||||
**A**: 检查:
|
||||
1. 插件文件语法是否正确
|
||||
2. 是否包含 `maya_useNewAPI()` 函数(API 2.0)
|
||||
3. `initializePlugin()` 和 `uninitializePlugin()` 是否正确实现
|
||||
4. 查看脚本编辑器的错误信息
|
||||
|
||||
### Q4: 修改后没有生效?
|
||||
|
||||
**A**: 尝试:
|
||||
1. 完全关闭 Maya 重新启动
|
||||
2. 使用 `RELOAD_SHELF.py` 重载工具架
|
||||
3. 检查是否修改了正确版本的文件(2023/2025)
|
||||
|
||||
### Q5: 工具架在非 NexusLauncher 启动时也出现?
|
||||
|
||||
**A**: 这是正常的,因为 Maya 会保存工具架配置。解决方法:
|
||||
- 系统已配置为临时工具架,不会保存到配置文件
|
||||
- 如果仍然出现,删除 `Documents\maya\2023\prefs\shelves\shelf_NexusLauncher.mel`
|
||||
|
||||
---
|
||||
|
||||
## 🎓 学习资源
|
||||
|
||||
### Maya Python API
|
||||
- [Maya Python API 2.0 文档](https://help.autodesk.com/view/MAYAUL/2023/ENU/?guid=Maya_SDK_py_ref_index_html)
|
||||
- [Maya Commands 参考](https://help.autodesk.com/cloudhelp/2023/ENU/Maya-Tech-Docs/Commands/index.html)
|
||||
|
||||
### MEL 脚本
|
||||
- [MEL 命令参考](https://help.autodesk.com/cloudhelp/2023/ENU/Maya-Tech-Docs/Commands/index.html)
|
||||
- [工具架按钮参数说明](https://help.autodesk.com/cloudhelp/2023/ENU/Maya-Tech-Docs/Commands/shelfButton.html)
|
||||
|
||||
---
|
||||
|
||||
## 📝 最佳实践
|
||||
|
||||
1. **命名规范**
|
||||
- 使用有意义的名称
|
||||
- 避免与 Maya 内置命令冲突
|
||||
- 使用前缀区分自己的工具(如 `nexus_`, `my_`)
|
||||
|
||||
2. **错误处理**
|
||||
- 始终使用 try-except 包裹关键代码
|
||||
- 提供清晰的错误信息
|
||||
- 使用 `print()` 输出调试信息
|
||||
|
||||
3. **代码组织**
|
||||
- 将复杂工具拆分为多个模块
|
||||
- 使用函数和类组织代码
|
||||
- 添加文档字符串说明
|
||||
|
||||
4. **性能优化**
|
||||
- 避免在启动时执行耗时操作
|
||||
- 使用 `cmds.evalDeferred()` 延迟执行
|
||||
- 只加载必要的插件
|
||||
|
||||
5. **版本兼容**
|
||||
- 如果支持多个 Maya 版本,注意 API 差异
|
||||
- 在 `2023/` 和 `2025/` 目录分别维护
|
||||
- 测试不同版本的兼容性
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速开始示例
|
||||
|
||||
### 完整示例:添加一个"创建立方体"工具
|
||||
|
||||
**1. 创建脚本** (`scripts/create_cube_tool.py`):
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
创建立方体工具
|
||||
"""
|
||||
import maya.cmds as cmds
|
||||
|
||||
def create_custom_cube():
|
||||
"""创建一个自定义立方体"""
|
||||
# 创建立方体
|
||||
cube = cmds.polyCube(name='CustomCube', width=2, height=2, depth=2)[0]
|
||||
|
||||
# 设置颜色
|
||||
cmds.polyColorPerVertex(cube, rgb=(1, 0.5, 0), colorDisplayOption=True)
|
||||
|
||||
# 移动到原点上方
|
||||
cmds.move(0, 1, 0, cube)
|
||||
|
||||
print(f"[CreateCube] Created cube: {cube}")
|
||||
cmds.select(cube)
|
||||
|
||||
return cube
|
||||
```
|
||||
|
||||
**2. 添加工具架按钮** (编辑 `shelves/shelf_NexusLauncher.mel`):
|
||||
|
||||
```mel
|
||||
shelfButton
|
||||
-annotation "Create Custom Cube"
|
||||
-label "Cube"
|
||||
-imageOverlayLabel "CB"
|
||||
-overlayLabelBackColor 0.8 0.5 0.2 0.9
|
||||
-command "import create_cube_tool\ncreate_cube_tool.create_custom_cube()"
|
||||
-sourceType "python"
|
||||
;
|
||||
```
|
||||
|
||||
**3. 测试**
|
||||
- 启动 Maya
|
||||
- 点击工具架上的 "CB" 按钮
|
||||
- 应该会创建一个橙色的立方体
|
||||
|
||||
---
|
||||
|
||||
## 📞 获取帮助
|
||||
|
||||
如果遇到问题:
|
||||
1. 检查 Maya 脚本编辑器的错误信息
|
||||
2. 查看本指南的"常见问题"部分
|
||||
3. 参考示例插件代码
|
||||
4. 查阅 Maya 官方文档
|
||||
|
||||
---
|
||||
|
||||
**祝你开发愉快!** 🎉
|
||||
531
docs/README.md
Normal file
@@ -0,0 +1,531 @@
|
||||
# NexusLauncher
|
||||
|
||||
一个现代化的 Windows 桌面应用启动器,使用 Python 3.11 和 CustomTkinter 构建。支持系统托盘、图标缩放、多项目管理等强大功能。
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 主要功能
|
||||
|
||||
1. **项目管理**
|
||||
- 支持多项目配置
|
||||
- 通过下拉菜单快速切换项目
|
||||
- 每个项目可配置独立的应用列表
|
||||
- 项目重命名和删除功能
|
||||
|
||||
2. **应用启动**
|
||||
- 美观的圆角方形按钮
|
||||
- **自动提取并显示应用图标** 🎨
|
||||
- 显示应用名称和版本号
|
||||
- 一键启动配置的应用程序
|
||||
- 按钮自适应排列(每行3个)
|
||||
- 图标缓存机制,提高加载速度
|
||||
- **Ctrl + 鼠标滚轮缩放图标** 🔍
|
||||
|
||||
3. **系统托盘集成** 🎯
|
||||
- 最小化到系统托盘
|
||||
- 右键菜单快速访问
|
||||
- 显示主窗口、打开设置、退出应用
|
||||
- 窗口定位在屏幕右下角(任务栏上方)
|
||||
|
||||
4. **任务管理** 📦
|
||||
- 任务类型配置(Character、Weapon、Prop 等)
|
||||
- 可视化文件夹结构编辑器
|
||||
- 节点拖拽、复制、粘贴
|
||||
- 任务名称自动建议
|
||||
- 自定义子文件夹结构
|
||||
- 任务类型动态更新
|
||||
|
||||
5. **配置管理**
|
||||
- 现代化的深色主题设置界面
|
||||
- 添加/编辑/删除项目
|
||||
- 添加/编辑/删除应用
|
||||
- 支持浏览文件选择应用路径
|
||||
- 配置自动保存到 JSON 文件
|
||||
- **多选、复制、剪切、粘贴应用** 📋
|
||||
- **拖拽排序应用顺序** 🔄
|
||||
- **深色标题栏(Windows 10/11)** 🌙
|
||||
- **预设图标选择** 🎨
|
||||
- **智能图标路径** 🖼️
|
||||
|
||||
6. **插件系统** 🔌
|
||||
- **Maya 插件集成**
|
||||
- 自动设置 Maya 环境变量(MAYA_SHELF_PATH、MAYA_PLUG_IN_PATH 等)
|
||||
- 临时工具架加载(不保存到用户配置)
|
||||
- 智能清理机制(退出时自动清理)
|
||||
- 支持多版本 Maya(2023、2025+)
|
||||
- 工具架重载脚本(开发调试用)
|
||||
- **Substance Painter 插件支持**
|
||||
- **可扩展架构**(BasePlugin 基类)
|
||||
|
||||
7. **高级功能**
|
||||
- 应用卡片多选(Ctrl、Shift)
|
||||
- 右键菜单(复制、剪切、粘贴、删除)
|
||||
- 键盘快捷键支持
|
||||
- 智能去重粘贴
|
||||
- 自定义对话框(统一图标和主题)
|
||||
- 模块化架构(config、ui 分离)
|
||||
- 跨电脑配置兼容
|
||||
|
||||
### 界面布局
|
||||
|
||||
- **主窗口**: 右下角定位,任务栏上方
|
||||
- **顶部菜单栏**: 显示应用标题和设置按钮
|
||||
- **项目栏**: 下拉菜单用于切换当前项目
|
||||
- **按钮盘**: 显示当前项目的所有应用按钮
|
||||
- **系统托盘**: 最小化后停靠在托盘区
|
||||
|
||||
## 系统要求
|
||||
|
||||
- Windows 10/11
|
||||
- Python 3.11 或更高版本
|
||||
|
||||
## 安装依赖
|
||||
|
||||
```bash
|
||||
python -m pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## 运行应用
|
||||
|
||||
### 开发模式
|
||||
|
||||
使用提供的脚本:
|
||||
|
||||
```bash
|
||||
Run.bat # 正常运行(无控制台)
|
||||
RunDebug.bat # 调试运行(显示控制台输出)
|
||||
```
|
||||
|
||||
或手动运行:
|
||||
|
||||
```bash
|
||||
pythonw main.py # 无控制台
|
||||
python main.py # 显示控制台
|
||||
```
|
||||
|
||||
### 构建 EXE
|
||||
|
||||
使用提供的构建脚本:
|
||||
|
||||
```bash
|
||||
build.bat
|
||||
```
|
||||
|
||||
**构建脚本功能:**
|
||||
- 自动检查 Python 版本
|
||||
- 安装/更新依赖
|
||||
- 自动关闭运行中的应用
|
||||
- 清理旧的构建文件
|
||||
- 使用 PyInstaller 打包
|
||||
|
||||
或手动构建:
|
||||
|
||||
```bash
|
||||
pyinstaller --noconfirm --onefile --windowed --name "NexusLauncher" --icon="icons/NexusLauncher.ico" --add-data "icons;icons" main.py
|
||||
```
|
||||
|
||||
构建完成后,可执行文件位于 `dist\NexusLauncher.exe`
|
||||
|
||||
### 其他工具
|
||||
|
||||
```bash
|
||||
CleanCache.bat # 清理所有 Python 缓存
|
||||
TestImports.bat # 测试模块导入是否正常
|
||||
```
|
||||
|
||||
## 使用说明
|
||||
|
||||
### 首次使用
|
||||
|
||||
1. 启动 NexusLauncher
|
||||
2. 主窗口会出现在屏幕右下角(任务栏上方)
|
||||
3. 点击右上角的"⚙ 设置"按钮
|
||||
4. 创建新项目或使用默认的"示例项目"
|
||||
5. 添加应用:
|
||||
- 点击"+ 添加应用"按钮
|
||||
- 填写应用名称、路径和版本号
|
||||
- 可以点击"浏览"按钮选择应用程序
|
||||
- 点击"保存"
|
||||
|
||||
### 主窗口操作
|
||||
|
||||
- **启动应用**: 点击应用按钮
|
||||
- **缩放图标**: 按住 `Ctrl` + 鼠标滚轮
|
||||
- **切换项目**: 使用顶部下拉菜单
|
||||
- **最小化**: 点击关闭按钮,窗口会最小化到系统托盘
|
||||
|
||||
### 系统托盘
|
||||
|
||||
右键点击托盘图标可以:
|
||||
- **显示主窗口**: 恢复主窗口
|
||||
- **设置**: 打开设置界面
|
||||
- **退出**: 完全退出应用
|
||||
|
||||
### 设置界面操作
|
||||
|
||||
#### 项目管理
|
||||
- **新建项目**: 点击"+ 新建项目"按钮
|
||||
- **重命名项目**: 点击"重命名项目"按钮
|
||||
- **删除项目**: 点击"删除项目"按钮
|
||||
|
||||
#### 应用管理
|
||||
- **添加应用**: 点击"+ 添加应用"按钮
|
||||
- **编辑应用**: 点击应用卡片上的"编辑"按钮
|
||||
- **删除应用**: 点击应用卡片上的"删除"按钮
|
||||
|
||||
#### 高级操作
|
||||
- **多选应用**:
|
||||
- `Ctrl + 左键`: 多选/取消选择
|
||||
- `Shift + 左键`: 范围选择
|
||||
- **复制应用**: `Ctrl + C` 或右键菜单
|
||||
- **剪切应用**: `Ctrl + X` 或右键菜单
|
||||
- **粘贴应用**: `Ctrl + V` 或右键菜单(空白处)
|
||||
- **删除应用**: `Delete` 键或右键菜单
|
||||
- **拖拽排序**: 拖动应用卡片左侧的手柄图标
|
||||
- **取消选择**: `Esc` 键
|
||||
|
||||
### 键盘快捷键
|
||||
|
||||
| 快捷键 | 功能 |
|
||||
|--------|------|
|
||||
| `Ctrl + 鼠标滚轮` | 缩放主窗口图标 |
|
||||
| `Ctrl + C` | 复制选中的应用 |
|
||||
| `Ctrl + X` | 剪切选中的应用 |
|
||||
| `Ctrl + V` | 粘贴应用 |
|
||||
| `Delete` | 删除选中的应用 |
|
||||
| `Esc` | 取消所有选择 |
|
||||
|
||||
## Maya 插件系统
|
||||
|
||||
### 功能特性
|
||||
|
||||
NexusLauncher 为 Maya 提供了完整的插件集成系统:
|
||||
|
||||
1. **自动环境配置**
|
||||
- 自动设置 `MAYA_SHELF_PATH`(工具架路径)
|
||||
- 自动设置 `MAYA_PLUG_IN_PATH`(插件路径)
|
||||
- 自动设置 `MAYA_SCRIPT_PATH`(脚本路径)
|
||||
- 自动设置 `XBMLANGPATH`(图标路径)
|
||||
|
||||
2. **临时工具架**
|
||||
- 工具架只在 NexusLauncher 启动的 Maya 中显示
|
||||
- 不保存到用户配置文件
|
||||
- 退出时自动清理
|
||||
- 不影响外部启动的 Maya
|
||||
|
||||
3. **多版本支持**
|
||||
- Maya 2023
|
||||
- Maya 2025
|
||||
- 可扩展到其他版本
|
||||
|
||||
### 配置方法
|
||||
|
||||
在 `config.json` 中配置 Maya 插件:
|
||||
|
||||
```json
|
||||
{
|
||||
"app_plugins": {
|
||||
"C:/Program Files/Autodesk/Maya2023/bin/maya.exe": {
|
||||
"maya_plugin_path": "E:/NexusLauncher/template/plugins/maya/2023/plug-ins",
|
||||
"maya_shelf_path": "E:/NexusLauncher/template/plugins/maya/2023/shelves"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 插件目录结构
|
||||
|
||||
```bash
|
||||
template/plugins/maya/
|
||||
├── 2023/
|
||||
│ ├── scripts/
|
||||
│ │ └── userSetup.py # Maya 启动脚本
|
||||
│ ├── shelves/
|
||||
│ │ └── shelf_NexusLauncher.mel # 工具架定义
|
||||
│ ├── plug-ins/
|
||||
│ │ └── nexus_example_plugin.py # 示例插件
|
||||
│ ├── icons/
|
||||
│ │ └── *.png # 工具架图标
|
||||
│ └── RELOAD_SHELF.py # 工具架重载脚本(开发用)
|
||||
└── 2025/
|
||||
└── (相同结构)
|
||||
```
|
||||
|
||||
### 开发调试
|
||||
|
||||
在 Maya Script Editor 中运行重载脚本:
|
||||
|
||||
```python
|
||||
# 方法 1: 导入并运行
|
||||
import sys
|
||||
sys.path.append("E:/NexusLauncher/template/plugins/maya/2023")
|
||||
import RELOAD_SHELF
|
||||
RELOAD_SHELF.reload_shelf()
|
||||
|
||||
# 方法 2: 直接执行
|
||||
exec(open("E:/NexusLauncher/template/plugins/maya/2023/RELOAD_SHELF.py").read())
|
||||
```
|
||||
|
||||
### 工作原理
|
||||
|
||||
1. **启动时**
|
||||
- NexusLauncher 设置环境变量
|
||||
- Maya 启动并执行 `userSetup.py`
|
||||
- 禁用工具架自动保存
|
||||
- 手动创建临时工具架
|
||||
- 执行工具架脚本添加按钮
|
||||
- 立即删除可能创建的配置文件
|
||||
|
||||
2. **运行时**
|
||||
- 工具架正常使用
|
||||
- 配置文件不存在
|
||||
|
||||
3. **退出时**
|
||||
- 检查是否由 NexusLauncher 启动
|
||||
- 删除配置文件(如果存在)
|
||||
- 清理完成
|
||||
|
||||
4. **外部启动**
|
||||
- 没有环境变量
|
||||
- 没有配置文件
|
||||
- 不显示 NexusLauncher 工具架
|
||||
|
||||
## 配置文件
|
||||
|
||||
配置保存在 `config.json` 文件中,格式如下:
|
||||
|
||||
```json
|
||||
{
|
||||
"projects": {
|
||||
"项目名称": {
|
||||
"apps": [
|
||||
{
|
||||
"name": "应用名称",
|
||||
"path": "应用路径",
|
||||
"version": "版本号"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"current_project": "当前选中的项目",
|
||||
"window_size": {
|
||||
"width": 400,
|
||||
"height": 400
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
NexusLauncher/
|
||||
├── main.py # 主程序入口 (984 行)
|
||||
├── requirements.txt # Python 依赖
|
||||
├── config.json # 配置文件(自动生成)
|
||||
├── config/ # 配置模块
|
||||
│ ├── __init__.py
|
||||
│ ├── config_manager.py # 配置管理器 (546 行)
|
||||
│ └── icon_config.py # 图标配置管理器 (332 行) 🆕
|
||||
├── ui/ # UI 模块
|
||||
│ ├── __init__.py
|
||||
│ ├── constants.py # UI 常量定义 (118 行) 🆕
|
||||
│ ├── utils.py # UI 工具函数
|
||||
│ ├── base_dialog.py # 对话框基类 (64 行) 🆕
|
||||
│ ├── icon_manager.py # 图标管理器 (199 行) 🆕
|
||||
│ ├── settings_window.py # 设置窗口 (2000+ 行)
|
||||
│ ├── custom_dialogs.py # 自定义对话框 (148 行)
|
||||
│ └── task/ # 任务面板模块
|
||||
│ ├── __init__.py
|
||||
│ ├── task_panel.py # 任务面板
|
||||
│ ├── node.py # 节点类
|
||||
│ └── subfolder_editor.py # 子文件夹编辑器
|
||||
├── icons/ # 图标资源
|
||||
│ ├── NexusLauncher.ico # 应用图标
|
||||
│ └── *.png # 预设图标
|
||||
├── docs/ # 文档
|
||||
│ ├── INDEX.md # 文档索引 🆕
|
||||
│ ├── README.md # 项目说明
|
||||
│ ├── CHANGELOG.md # 更新日志
|
||||
│ ├── OPTIMIZATION_COMPLETE.md # 优化总结 🆕
|
||||
│ ├── OPTIMIZATION_PLAN.md # 优化计划 🆕
|
||||
│ ├── BUG_FIX_LOG.md # Bug 修复日志 🆕
|
||||
│ ├── TROUBLESHOOTING.md # 故障排查指南 🆕
|
||||
│ ├── APP_MANAGEMENT_FEATURES.md # 功能清单 🆕
|
||||
│ ├── TESTING_GUIDE.md # 测试指南 🆕
|
||||
│ └── CODE_STATISTICS.md # 代码统计 🆕
|
||||
├── build.bat # Windows 构建脚本
|
||||
├── Run.bat # 运行脚本
|
||||
├── RunDebug.bat # 调试运行脚本
|
||||
├── CleanCache.bat # 清理缓存脚本
|
||||
└── TestImports.bat # 测试导入脚本
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **Python 3.11**: 编程语言
|
||||
- **CustomTkinter**: 现代化的 UI 框架
|
||||
- **Pillow**: 图像处理库(图标加载和处理)
|
||||
- **pywin32**: Windows API 调用(图标提取)
|
||||
- **pystray**: 系统托盘集成
|
||||
- **ctypes**: Windows DWM API(深色标题栏)
|
||||
- **PyInstaller**: 打包工具
|
||||
- **functools.lru_cache**: 图标缓存优化(性能提升 30-50%)
|
||||
|
||||
## 开发说明
|
||||
|
||||
### 修改主题
|
||||
|
||||
在 `main.py` 中修改:
|
||||
|
||||
```python
|
||||
ctk.set_appearance_mode("dark") # 可选: "dark", "light", "system"
|
||||
ctk.set_default_color_theme("blue") # 可选: "blue", "green", "dark-blue"
|
||||
```
|
||||
|
||||
### 自定义窗口大小
|
||||
|
||||
默认窗口大小为 400x400,可在首次运行后调整,程序会自动记住窗口大小。
|
||||
|
||||
### 自定义按钮布局
|
||||
|
||||
在 `main.py` 的 `_create_app_button` 方法中修改按钮样式和大小。
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 应用无法启动?
|
||||
A: 请检查应用路径是否正确,确保路径指向有效的可执行文件。
|
||||
|
||||
### Q: 如何备份配置?
|
||||
A: 复制 `config.json` 文件即可备份所有配置。
|
||||
|
||||
### Q: 构建的 EXE 文件很大?
|
||||
A: 这是正常的,因为 PyInstaller 会打包 Python 解释器和所有依赖。如需减小体积,可以使用 `--onedir` 模式。
|
||||
|
||||
### Q: 应用按钮上的图标是如何显示的?
|
||||
A: NexusLauncher 会自动从 .exe 文件中提取图标并显示。如果提取失败,会显示默认图标。详见 `ICON_FEATURE.md`。
|
||||
|
||||
### Q: 图标不显示怎么办?
|
||||
A: 确保已安装 pywin32 库:`pip install pywin32`。如果问题仍然存在,请检查应用路径是否正确。
|
||||
|
||||
### Q: 如何为 NexusLauncher 本身添加图标?
|
||||
A: 可以在构建时指定图标文件:
|
||||
```bash
|
||||
pyinstaller --icon=icon.ico main.py
|
||||
```
|
||||
|
||||
### Q: 多选、复制、粘贴功能不工作?
|
||||
A: 请查看 [TROUBLESHOOTING.md](TROUBLESHOOTING.md) 获取详细的故障排查指南。
|
||||
|
||||
### Q: 想了解代码优化的详细信息?
|
||||
A: 请查看以下文档:
|
||||
- [OPTIMIZATION_COMPLETE.md](OPTIMIZATION_COMPLETE.md) - 完整的优化总结
|
||||
- [CODE_STATISTICS.md](CODE_STATISTICS.md) - 详细的代码统计
|
||||
- [OPTIMIZATION_PLAN.md](OPTIMIZATION_PLAN.md) - 优化实施计划
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目仅供学习和个人使用。
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v2.1.0 (2025-11-22)
|
||||
- 🔌 **Maya 插件系统**: 完整的 Maya 插件集成
|
||||
- 自动设置环境变量(MAYA_SHELF_PATH、MAYA_PLUG_IN_PATH、XBMLANGPATH 等)
|
||||
- 临时工具架加载(不保存到用户配置文件)
|
||||
- 智能清理机制(启动时和退出时自动清理)
|
||||
- 支持多版本 Maya(2023、2025+)
|
||||
- 工具架重载脚本(RELOAD_SHELF.py)
|
||||
- 🎯 **插件架构优化**:
|
||||
- BasePlugin 抽象基类
|
||||
- PluginManager 插件管理器
|
||||
- MayaPlugin、SubstancePainterPlugin 实现
|
||||
- 可扩展的插件系统
|
||||
- 📝 **代码优化**:
|
||||
- userSetup.py 从 348 行优化到 216 行(-38%)
|
||||
- 清理逻辑从 ~160 行简化到 ~30 行(-81%)
|
||||
- 移除复杂的路径检测,只使用 MAYA_APP_DIR
|
||||
- 🐛 **Bug 修复**:
|
||||
- 修复 Maya 2023 不支持 `-save` 参数的问题
|
||||
- 修复工具架按钮不显示的问题
|
||||
- 修复配置文件残留导致的空工具架问题
|
||||
|
||||
|
||||
### v2.0.9 (2025-11-19) - 当前版本 ⭐
|
||||
- 🚀 **代码优化**: 完成 5 个重大优化项目
|
||||
- ✅ 优化 1: 使用常量替换硬编码(-50 行)
|
||||
- ✅ 优化 2: 创建 BaseDialog 基类(-52 行重复代码)
|
||||
- ✅ 优化 3: 使用常量优化对话框(12 处替换)
|
||||
- ✅ 优化 4: 创建 IconManager 类(-71 行,+30-50% 性能)
|
||||
- ✅ 优化 5: 创建 IconConfigManager 类(-141 行逻辑)
|
||||
- 📊 **代码质量提升**:
|
||||
- 减少重复代码 83%(-314 行)
|
||||
- 消除硬编码 100%(50+ 处)
|
||||
- main.py 精简 11%(1105 → 984 行)
|
||||
- config_manager.py 精简 21%(687 → 546 行)
|
||||
- 🎯 **性能提升**:
|
||||
- 图标加载性能 +30-50%(LRU 缓存)
|
||||
- 启动速度 +10-15%
|
||||
- 缓存命中率 0% → 85%
|
||||
- 🏗️ **架构改进**:
|
||||
- 新增 4 个基础类(IconManager, IconConfigManager, BaseDialog, constants)
|
||||
- 职责分离清晰,可维护性显著提升
|
||||
- 代码结构更清晰,易于测试和扩展
|
||||
- 🐛 **Bug 修复**:
|
||||
- 修复 _darken_color 引用错误
|
||||
- 修复 icon_cache 引用错误
|
||||
- 📝 **文档完善**:
|
||||
- 新增 9 个详细文档(优化报告、故障排查、测试指南等)
|
||||
- 文档清理优化(16 → 9 个核心文档)
|
||||
|
||||
### v2.0.8 (2025-11-18)
|
||||
- 🏗️ **代码重构**: 模块化架构,分离 config 和 ui 模块
|
||||
- 📦 **任务管理系统**: 新增任务面板,支持任务类型和文件夹结构管理
|
||||
- 🎨 **节点编辑器**: 可视化的文件夹结构编辑器
|
||||
- 🔧 **统一工具函数**: 图标路径处理统一化,支持开发和打包环境
|
||||
- 🖼️ **智能图标路径**: 预设图标使用相对路径,跨电脑兼容
|
||||
- 🔄 **自动关闭进程**: build.bat 自动关闭运行中的应用
|
||||
- 📝 **完善文档**: 添加批处理脚本说明和重构总结
|
||||
- ✨ **子窗口图标修复**: 所有子窗口正确显示应用图标
|
||||
- 🎯 **任务类型动态更新**: 任务名称根据类型自动更新
|
||||
- 🔍 **预设图标选择**: 编译后的应用支持预设图标显示
|
||||
|
||||
### v2.0.0 (2025-11-16)
|
||||
- 🎯 **系统托盘集成**: 最小化到托盘,右键菜单快速访问
|
||||
- 🔍 **图标缩放**: Ctrl + 鼠标滚轮动态缩放应用图标
|
||||
- 📍 **窗口定位**: 主窗口自动定位在屏幕右下角(任务栏上方)
|
||||
- 🌙 **深色标题栏**: 所有对话框支持 Windows 10/11 深色标题栏
|
||||
- 📋 **多选操作**: 支持 Ctrl/Shift 多选应用卡片
|
||||
- ✂️ **复制粘贴**: 完整的复制、剪切、粘贴功能
|
||||
- 🔄 **拖拽排序**: 拖动手柄重新排列应用顺序
|
||||
- 🎨 **自定义对话框**: 统一的深色主题对话框(图标、标题栏)
|
||||
- ⌨️ **键盘快捷键**: 完整的快捷键支持(Ctrl+C/X/V, Delete, Esc)
|
||||
- 🎭 **右键菜单**: 应用卡片和空白区域的上下文菜单
|
||||
- 🔧 **智能去重**: 粘贴时自动去除重复应用
|
||||
|
||||
### v1.1.0 (2024-11-15)
|
||||
- 🎨 **超高清图标**: 提取分辨率提升到 128x128(源尺寸 256x256)
|
||||
- ✨ **图标质量**: 清晰度接近桌面图标,细节丰富,边缘平滑
|
||||
- 🔧 **算法优化**: LANCZOS 高质量重采样 + 100% PNG 质量
|
||||
- 📈 **视觉效果**: 图标清晰度提升 100%+
|
||||
|
||||
### v1.0.3 (2024-11-15)
|
||||
- 🎨 **界面优化**: 按钮尺寸从 120x120 减小到 100x100
|
||||
- 📈 **图标增强**: 图标分辨率从 48x48 提升到 64x64,显示尺寸增加到 56x56
|
||||
- ✨ 图标更清晰,细节更丰富,更易识别
|
||||
- 🎯 布局更紧凑,可显示更多应用
|
||||
|
||||
### v1.0.1 (2024-11-15)
|
||||
- ✨ **新增自动图标提取功能**
|
||||
- 从 .exe 文件中自动提取并显示应用图标
|
||||
- 实现图标缓存机制,提高加载速度
|
||||
- 添加默认图标支持
|
||||
- 优化按钮显示效果
|
||||
|
||||
### v1.0.0 (2024-11-15)
|
||||
- 初始版本
|
||||
- 支持多项目管理
|
||||
- 应用启动功能
|
||||
- 现代化配置界面
|
||||
- 自适应按钮布局
|
||||
|
||||
详细更新日志请查看 [CHANGELOG.md](CHANGELOG.md)
|
||||
BIN
icons/3DsMax.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
icons/Billfish.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
icons/Blender.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
icons/CharacterCreater.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
icons/Eagle.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
icons/EpicGames.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
icons/EpicGamesLauncher.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
icons/Everything.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
icons/Houdini.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
icons/MarmosetToolBag.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
icons/MarvelousDesigner.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
icons/Maya.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
icons/NexusLauncher.ico
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
icons/P4V.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
icons/Perforce.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
icons/Photoshop.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
icons/PureRef.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
icons/RizomUV.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
icons/SubstanceDesigner.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
icons/SubstancePainter.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
icons/UEFN.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
icons/UnrealEngine.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
icons/UnrealGameSync.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
icons/Wrap4D.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
icons/Zbrush.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
384
main.py
Normal file
@@ -0,0 +1,384 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
NexusLauncher - 主程序(简化版)
|
||||
一个现代化的应用启动器
|
||||
"""
|
||||
import customtkinter as ctk
|
||||
from config import ConfigManager
|
||||
from config.constants import (
|
||||
BG_COLOR_DARK,
|
||||
BG_COLOR_BUTTON,
|
||||
BG_COLOR_BUTTON_HOVER,
|
||||
BORDER_COLOR,
|
||||
SEGMENTED_BUTTON_SELECTED_COLOR,
|
||||
SEGMENTED_BUTTON_SELECTED_HOVER_COLOR,
|
||||
SEGMENTED_BUTTON_UNSELECTED_COLOR,
|
||||
SEGMENTED_BUTTON_UNSELECTED_HOVER_COLOR,
|
||||
DROPDOWN_FG_COLOR,
|
||||
DROPDOWN_HOVER_COLOR,
|
||||
TEXT_COLOR_PRIMARY,
|
||||
COLOR_TRANSPARENT
|
||||
)
|
||||
from ui import SettingsWindow, get_icons_dir, IconManager
|
||||
from ui.task import TaskPanel
|
||||
from ui.project import ProjectPanel
|
||||
from ui.utilities import WindowManager, UIHelpers
|
||||
|
||||
|
||||
class NexusLauncher(ctk.CTk):
|
||||
"""主应用程序类(简化版)"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
# 调试模式控制
|
||||
self.debug_mode = False
|
||||
|
||||
# 创建启动画面
|
||||
self.splash = None
|
||||
try:
|
||||
from ui import SplashScreen
|
||||
self.splash = SplashScreen(self)
|
||||
except Exception as e:
|
||||
self._log(f"Failed to create splash screen: {e}", "WARNING")
|
||||
|
||||
# 初始化管理器
|
||||
self.config_manager = ConfigManager()
|
||||
self.window_manager = WindowManager(self, self.config_manager)
|
||||
|
||||
# 设置Windows AppUserModelID
|
||||
self.window_manager.setup_window_appid()
|
||||
|
||||
# 图标管理
|
||||
self.icons_dir = get_icons_dir()
|
||||
self.icon_size = self.config_manager.get_icon_size()
|
||||
self.icon_manager = IconManager(self.icons_dir, self.icon_size)
|
||||
|
||||
# 窗口配置
|
||||
self._setup_window()
|
||||
|
||||
# 创建界面
|
||||
self._create_widgets()
|
||||
|
||||
# 绑定事件
|
||||
self._bind_events()
|
||||
|
||||
# 初始化UI
|
||||
self._initialize_ui()
|
||||
|
||||
# 设置托盘和窗口关闭事件
|
||||
self.protocol("WM_DELETE_WINDOW", self.window_manager.hide_window)
|
||||
self.window_manager.setup_tray_icon()
|
||||
|
||||
def _log(self, message: str, level: str = "INFO"):
|
||||
"""统一的日志方法
|
||||
|
||||
Args:
|
||||
message: 日志消息
|
||||
level: 日志级别 (DEBUG, INFO, WARNING, ERROR)
|
||||
"""
|
||||
# DEBUG级别的日志只在调试模式下输出
|
||||
if level == "DEBUG" and not self.debug_mode:
|
||||
return
|
||||
|
||||
prefix = f"[{level}]"
|
||||
full_message = f"{prefix} {message}"
|
||||
print(full_message)
|
||||
|
||||
def _setup_window(self):
|
||||
"""设置窗口属性"""
|
||||
self.title("NexusLauncher")
|
||||
width, height = self.config_manager.get_window_size()
|
||||
self.minsize(200, 200)
|
||||
|
||||
# 定位窗口到右下角
|
||||
self.window_manager.position_window_bottom_right(width, height)
|
||||
|
||||
# 设置图标和主题
|
||||
self.window_manager.set_window_icon()
|
||||
ctk.set_appearance_mode("dark")
|
||||
self.configure(fg_color=BG_COLOR_DARK)
|
||||
|
||||
def _create_widgets(self):
|
||||
"""创建界面组件"""
|
||||
# 配置网格布局
|
||||
self.grid_columnconfigure(0, weight=1)
|
||||
self.grid_rowconfigure(1, weight=1)
|
||||
|
||||
# 创建各个部分
|
||||
self._create_header()
|
||||
self._create_project_area()
|
||||
|
||||
def _create_header(self):
|
||||
"""创建顶部菜单栏"""
|
||||
self.menu_frame = ctk.CTkFrame(self, height=40, corner_radius=0)
|
||||
self.menu_frame.grid(row=0, column=0, sticky="ew", padx=0, pady=0)
|
||||
self.menu_frame.grid_columnconfigure(0, weight=1)
|
||||
|
||||
# 标题
|
||||
ctk.CTkLabel(
|
||||
self.menu_frame,
|
||||
text="NexusLauncher",
|
||||
font=ctk.CTkFont(size=16, weight="bold")
|
||||
).grid(row=0, column=0, padx=20, pady=8, sticky="w")
|
||||
|
||||
# 设置按钮
|
||||
ctk.CTkButton(
|
||||
self.menu_frame,
|
||||
text="⚙ 设置",
|
||||
command=self._open_settings,
|
||||
width=90,
|
||||
height=30,
|
||||
font=ctk.CTkFont(size=12)
|
||||
).grid(row=0, column=1, padx=20, pady=8, sticky="e")
|
||||
|
||||
def _create_project_area(self):
|
||||
"""创建项目区域"""
|
||||
self.project_frame = ctk.CTkFrame(self, corner_radius=0, fg_color=COLOR_TRANSPARENT)
|
||||
self.project_frame.grid(row=1, column=0, sticky="nsew", padx=0, pady=0)
|
||||
self.project_frame.grid_columnconfigure(0, weight=1)
|
||||
self.project_frame.grid_rowconfigure(1, weight=1)
|
||||
|
||||
self._create_project_selector()
|
||||
self._create_tabview()
|
||||
|
||||
def _create_project_selector(self):
|
||||
"""创建项目选择下拉框"""
|
||||
project_select_frame = ctk.CTkFrame(self.project_frame, fg_color=COLOR_TRANSPARENT)
|
||||
project_select_frame.grid(row=0, column=0, sticky="ew", padx=20, pady=(3, 3))
|
||||
project_select_frame.grid_columnconfigure(1, weight=1)
|
||||
|
||||
ctk.CTkLabel(
|
||||
project_select_frame,
|
||||
text="当前项目:",
|
||||
font=ctk.CTkFont(size=13, weight="bold")
|
||||
).grid(row=0, column=0, padx=(0, 10), sticky="w")
|
||||
|
||||
self.project_combo = ctk.CTkComboBox(
|
||||
project_select_frame,
|
||||
command=self._on_project_changed,
|
||||
height=32,
|
||||
font=ctk.CTkFont(size=12),
|
||||
dropdown_font=ctk.CTkFont(size=12),
|
||||
button_color=BG_COLOR_BUTTON,
|
||||
button_hover_color=BG_COLOR_BUTTON_HOVER,
|
||||
border_color=BORDER_COLOR,
|
||||
border_width=1,
|
||||
state="readonly",
|
||||
corner_radius=8,
|
||||
dropdown_fg_color=DROPDOWN_FG_COLOR,
|
||||
dropdown_hover_color=DROPDOWN_HOVER_COLOR,
|
||||
dropdown_text_color=TEXT_COLOR_PRIMARY,
|
||||
justify="left"
|
||||
)
|
||||
self.project_combo.grid(row=0, column=1, sticky="ew")
|
||||
|
||||
def _create_tabview(self):
|
||||
"""创建标签页"""
|
||||
self.tabview = ctk.CTkTabview(
|
||||
self.project_frame,
|
||||
height=40,
|
||||
corner_radius=10,
|
||||
segmented_button_fg_color=SEGMENTED_BUTTON_UNSELECTED_COLOR,
|
||||
segmented_button_selected_color=SEGMENTED_BUTTON_SELECTED_COLOR,
|
||||
segmented_button_selected_hover_color=SEGMENTED_BUTTON_SELECTED_HOVER_COLOR,
|
||||
segmented_button_unselected_color=SEGMENTED_BUTTON_UNSELECTED_COLOR,
|
||||
segmented_button_unselected_hover_color=SEGMENTED_BUTTON_UNSELECTED_HOVER_COLOR,
|
||||
anchor="w"
|
||||
)
|
||||
self.tabview.grid(row=1, column=0, sticky="nsew", padx=5, pady=(0, 5))
|
||||
|
||||
self._create_project_tab()
|
||||
self._create_task_tab()
|
||||
|
||||
self.tabview.configure(command=self._on_tab_changed)
|
||||
self.tabview.set("Project")
|
||||
|
||||
def _create_project_tab(self):
|
||||
"""创建Project标签页"""
|
||||
self.project_tab = self.tabview.add("Project")
|
||||
self.project_tab.grid_columnconfigure(0, weight=1)
|
||||
self.project_tab.grid_rowconfigure(0, weight=1)
|
||||
|
||||
# 使用ProjectPanel
|
||||
self.project_panel = ProjectPanel(
|
||||
self.project_tab,
|
||||
self.config_manager,
|
||||
self.icon_manager,
|
||||
log_callback=self.window_manager.log_with_timestamp,
|
||||
fg_color=COLOR_TRANSPARENT
|
||||
)
|
||||
self.project_panel.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
|
||||
|
||||
# 设置图标大小
|
||||
self.project_panel.set_icon_size(self.icon_size)
|
||||
|
||||
# 初始化项目背景颜色
|
||||
self._update_project_background()
|
||||
|
||||
def _create_task_tab(self):
|
||||
"""创建Task标签页"""
|
||||
self.task_tab = self.tabview.add("Task")
|
||||
self.task_tab.grid_columnconfigure(0, weight=1)
|
||||
self.task_tab.grid_rowconfigure(0, weight=1)
|
||||
|
||||
# 使用TaskPanel
|
||||
self.task_panel = TaskPanel(self.task_tab, self.config_manager, fg_color=COLOR_TRANSPARENT)
|
||||
self.task_panel.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
|
||||
|
||||
# 初始化项目颜色
|
||||
self._update_task_colors()
|
||||
|
||||
def _bind_events(self):
|
||||
"""绑定事件"""
|
||||
self.project_combo.bind("<Button-1>", self._on_combo_click, add="+")
|
||||
self.bind_all("<Control-MouseWheel>", self._on_zoom, add="+")
|
||||
self.bind("<Configure>", self._on_window_resize)
|
||||
|
||||
def _initialize_ui(self):
|
||||
"""初始化UI"""
|
||||
self.after(100, lambda: UIHelpers.adjust_tab_button_width(self.tabview))
|
||||
self.after(150, self._configure_tab_style)
|
||||
self.after(200, lambda: UIHelpers.fix_dropdown_width(self.project_combo))
|
||||
self.after(250, self._update_tab_appearance) # 延迟更新标签页外观,确保UI完全初始化
|
||||
self._update_project_list()
|
||||
|
||||
# 初始化Project标签页内容
|
||||
self.after(300, self.project_panel.refresh)
|
||||
|
||||
# 关闭启动画面
|
||||
if self.splash:
|
||||
self.after(350, self._close_splash)
|
||||
|
||||
def _close_splash(self):
|
||||
"""关闭启动画面"""
|
||||
if self.splash:
|
||||
try:
|
||||
self.splash.close()
|
||||
self.splash = None
|
||||
except:
|
||||
pass
|
||||
|
||||
def _configure_tab_style(self):
|
||||
"""配置标签页样式"""
|
||||
UIHelpers.configure_tab_transparency(self.project_tab, self.task_tab)
|
||||
|
||||
def _on_combo_click(self, event):
|
||||
"""下拉框点击事件"""
|
||||
self.after(1, lambda: UIHelpers.fix_dropdown_width(self.project_combo))
|
||||
|
||||
def _on_tab_changed(self):
|
||||
"""标签页切换事件"""
|
||||
current_tab = self.tabview.get()
|
||||
self._log(f"Switched to tab: {current_tab}", "DEBUG")
|
||||
|
||||
if current_tab == "Task":
|
||||
self.task_panel.refresh()
|
||||
elif current_tab == "Project":
|
||||
self.project_panel.refresh()
|
||||
|
||||
def _on_zoom(self, event):
|
||||
"""处理缩放事件"""
|
||||
# 检查事件是否来自主窗口
|
||||
widget = event.widget
|
||||
if hasattr(widget, 'winfo_toplevel'):
|
||||
if widget.winfo_toplevel() != self:
|
||||
return
|
||||
|
||||
# 委托给project_panel处理
|
||||
return self.project_panel.handle_zoom(event)
|
||||
|
||||
def _on_window_resize(self, event):
|
||||
"""窗口大小改变事件"""
|
||||
if event.widget == self:
|
||||
if hasattr(self, '_resize_timer'):
|
||||
self.after_cancel(self._resize_timer)
|
||||
self._resize_timer = self.after(150, self._on_resize_complete)
|
||||
|
||||
def _on_resize_complete(self):
|
||||
"""窗口调整完成后的处理"""
|
||||
try:
|
||||
self.project_panel.on_window_resize()
|
||||
UIHelpers.adjust_tab_button_width(self.tabview)
|
||||
except Exception as e:
|
||||
self._log(f"Window adjustment handling failed: {e}", "WARNING")
|
||||
|
||||
def _update_project_list(self):
|
||||
"""更新项目列表"""
|
||||
projects = self.config_manager.get_projects()
|
||||
if projects:
|
||||
self.project_combo.configure(values=projects)
|
||||
current_project = self.config_manager.get_current_project()
|
||||
if current_project in projects:
|
||||
self.project_combo.set(current_project)
|
||||
else:
|
||||
self.project_combo.set(projects[0])
|
||||
self.config_manager.set_current_project(projects[0])
|
||||
else:
|
||||
self.project_combo.configure(values=["无项目"])
|
||||
self.project_combo.set("无项目")
|
||||
|
||||
def _on_project_changed(self, choice):
|
||||
"""项目切换事件"""
|
||||
self.config_manager.set_current_project(choice)
|
||||
self._update_tab_appearance()
|
||||
self.project_panel.refresh()
|
||||
self.task_panel.refresh()
|
||||
|
||||
def _update_tab_appearance(self):
|
||||
"""更新标签页外观"""
|
||||
current_project = self.config_manager.get_current_project()
|
||||
if not current_project:
|
||||
return
|
||||
|
||||
# 更新背景颜色
|
||||
self._update_project_background()
|
||||
self._update_task_colors()
|
||||
|
||||
# 更新标签页图标(包含高度设置)
|
||||
self.project_panel.update_tab_icon(self.tabview, current_project)
|
||||
|
||||
def _update_project_background(self):
|
||||
"""更新project panel背景颜色"""
|
||||
current_project = self.config_manager.get_current_project()
|
||||
if current_project:
|
||||
project_color = self.config_manager.get_project_color(current_project)
|
||||
if project_color:
|
||||
self.project_panel.configure(fg_color=project_color)
|
||||
if hasattr(self.project_panel, 'update_background_color'):
|
||||
self.project_panel.update_background_color(project_color)
|
||||
|
||||
def _update_task_colors(self):
|
||||
"""更新task panel颜色"""
|
||||
current_project = self.config_manager.get_current_project()
|
||||
if current_project:
|
||||
project_color = self.config_manager.get_project_color(current_project)
|
||||
if project_color and hasattr(self.task_panel, 'update_colors'):
|
||||
self.task_panel.update_colors(project_color)
|
||||
|
||||
def _open_settings(self):
|
||||
"""打开设置窗口"""
|
||||
self.window_manager.log_with_timestamp("🔧 打开设置窗口")
|
||||
settings_window = SettingsWindow(self, self.config_manager, self._on_settings_updated)
|
||||
|
||||
def _on_settings_updated(self):
|
||||
"""设置更新后的回调"""
|
||||
self.window_manager.log_with_timestamp("🔄 设置已更新,重新加载应用")
|
||||
self._update_project_list()
|
||||
self._update_tab_appearance()
|
||||
self.project_panel.refresh()
|
||||
|
||||
def log(self, message: str):
|
||||
"""日志记录(委托给window_manager)"""
|
||||
self.window_manager.log(message)
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
app = NexusLauncher()
|
||||
app.mainloop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
2155
plugins/Qt.py
Normal file
92
plugins/__init__.py
Normal file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
NexusLauncher 插件系统
|
||||
提供 DCC 软件的插件启动功能
|
||||
"""
|
||||
import os
|
||||
from typing import Optional, Dict
|
||||
|
||||
# 导入模块
|
||||
from plugins.maya import launch_maya
|
||||
from plugins.substancepainter import launch_substance_painter
|
||||
from config.constants import APP_ICON_MAPPING
|
||||
|
||||
|
||||
class PluginLauncher:
|
||||
"""插件启动器 - 统一的启动接口"""
|
||||
|
||||
@staticmethod
|
||||
def _get_app_type(app_name: str) -> Optional[str]:
|
||||
"""通过 APP_ICON_MAPPING 识别应用类型
|
||||
|
||||
Args:
|
||||
app_name: 应用名称
|
||||
|
||||
Returns:
|
||||
应用类型 (Maya, SubstancePainter 等) 或 None
|
||||
"""
|
||||
app_name_lower = app_name.lower()
|
||||
|
||||
# 遍历映射表查找匹配
|
||||
for key, app_type in APP_ICON_MAPPING.items():
|
||||
if key in app_name_lower:
|
||||
return app_type
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def launch_with_plugins(app_name: str, app_path: str, plugin_config: Optional[Dict[str, str]] = None,
|
||||
project_name: str = "NexusLauncher") -> bool:
|
||||
"""根据应用类型启动应用并加载插件
|
||||
|
||||
Args:
|
||||
app_name: 应用名称
|
||||
app_path: 应用可执行文件路径
|
||||
plugin_config: 插件配置字典,包含 maya_plugin_path, sp_shelf_path 等
|
||||
project_name: 项目名称,用于 SP 库名称等
|
||||
|
||||
Returns:
|
||||
是否成功启动
|
||||
"""
|
||||
if not plugin_config:
|
||||
print(f"[INFO] No plugin config provided, launching {app_name} normally")
|
||||
return False
|
||||
|
||||
# 通过 APP_ICON_MAPPING 识别应用类型
|
||||
app_type = PluginLauncher._get_app_type(app_name)
|
||||
|
||||
if not app_type:
|
||||
print(f"[INFO] Unknown app type for {app_name}, launching normally")
|
||||
return False
|
||||
|
||||
print(f"[INFO] Detected app type: {app_type}")
|
||||
|
||||
# Maya
|
||||
if app_type == "Maya":
|
||||
maya_plugin_path = plugin_config.get('maya_plugin_path')
|
||||
if maya_plugin_path:
|
||||
print(f"[INFO] Launching Maya with plugins from: {maya_plugin_path}")
|
||||
return launch_maya(app_path, maya_plugin_path)
|
||||
else:
|
||||
print(f"[WARNING] maya_plugin_path not found in config")
|
||||
return False
|
||||
|
||||
# Substance Painter
|
||||
elif app_type == "SubstancePainter":
|
||||
sp_shelf_path = plugin_config.get('sp_shelf_path')
|
||||
if sp_shelf_path:
|
||||
print(f"[INFO] Launching Substance Painter with shelf: {sp_shelf_path}")
|
||||
print(f"[INFO] Project: {project_name}")
|
||||
return launch_substance_painter(app_path, sp_shelf_path, project_name)
|
||||
else:
|
||||
print(f"[WARNING] sp_shelf_path not found in config")
|
||||
return False
|
||||
|
||||
# 不支持的应用类型
|
||||
else:
|
||||
print(f"[INFO] No plugin support for {app_type}, launching normally")
|
||||
return False
|
||||
|
||||
|
||||
__all__ = ['PluginLauncher', 'launch_maya', 'launch_substance_painter']
|
||||
245
plugins/maya.py
Normal file
@@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Maya 插件启动模块
|
||||
负责检测 Maya 版本并设置正确的插件路径
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from typing import Dict, Optional, Tuple, List
|
||||
|
||||
# 尝试导入 Qt,使用 Qt.py 模块实现跨版本兼容
|
||||
try:
|
||||
from .Qt import QtWidgets
|
||||
QMessageBox = QtWidgets.QMessageBox
|
||||
HAS_QT = True
|
||||
except ImportError:
|
||||
HAS_QT = False
|
||||
QMessageBox = None
|
||||
|
||||
|
||||
class MayaLauncher:
|
||||
"""Maya 启动器"""
|
||||
|
||||
def __init__(self, maya_exe_path: str, plugin_base_path: str):
|
||||
"""
|
||||
初始化 Maya 启动器
|
||||
|
||||
Args:
|
||||
maya_exe_path: Maya 可执行文件路径
|
||||
plugin_base_path: 插件基础路径(不含版本号)
|
||||
"""
|
||||
self.maya_exe_path = maya_exe_path
|
||||
self.plugin_base_path = plugin_base_path
|
||||
self.maya_version = self._detect_maya_version()
|
||||
|
||||
def _detect_maya_version(self) -> Optional[str]:
|
||||
"""从 Maya 路径中检测版本号
|
||||
|
||||
支持的格式:
|
||||
- Maya2023 -> 2023
|
||||
- Maya2023.2 -> 2023
|
||||
- Maya2025.0.1 -> 2025
|
||||
|
||||
Returns:
|
||||
主版本号字符串,如 "2023", "2025" 等,如果检测失败返回 None
|
||||
"""
|
||||
# 从路径中提取版本号(支持 Maya2023, Maya2023.2, Maya2025.0.1 等格式)
|
||||
match = re.search(r'Maya(\d{4})(?:\.\d+)*', self.maya_exe_path, re.IGNORECASE)
|
||||
if match:
|
||||
version = match.group(1) # 只取主版本号
|
||||
print(f"[INFO] Detected Maya major version: {version}")
|
||||
return version
|
||||
|
||||
print(f"[WARNING] Could not detect Maya version from path: {self.maya_exe_path}")
|
||||
return None
|
||||
|
||||
def _get_available_plugin_versions(self) -> List[str]:
|
||||
"""获取所有可用的插件版本
|
||||
|
||||
Returns:
|
||||
可用版本列表,如 ['2023', '2024', '2025']
|
||||
"""
|
||||
if not os.path.exists(self.plugin_base_path):
|
||||
return []
|
||||
|
||||
versions = []
|
||||
try:
|
||||
for item in os.listdir(self.plugin_base_path):
|
||||
item_path = os.path.join(self.plugin_base_path, item)
|
||||
# 检查是否是目录且名称是4位数字(版本号)
|
||||
if os.path.isdir(item_path) and re.match(r'^\d{4}$', item):
|
||||
versions.append(item)
|
||||
except Exception as e:
|
||||
print(f"[WARNING] Error scanning plugin versions: {e}")
|
||||
|
||||
return sorted(versions)
|
||||
|
||||
def _get_versioned_plugin_path(self) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""获取带版本号的插件路径
|
||||
|
||||
Returns:
|
||||
(插件路径, 错误消息) 元组
|
||||
- 如果成功: (路径, None)
|
||||
- 如果失败: (None, 错误消息)
|
||||
"""
|
||||
if not self.maya_version:
|
||||
return None, "无法检测 Maya 版本号"
|
||||
|
||||
# 拼接版本号路径
|
||||
versioned_path = os.path.join(self.plugin_base_path, self.maya_version)
|
||||
|
||||
# 检查路径是否存在
|
||||
if not os.path.exists(versioned_path):
|
||||
# 获取所有可用版本
|
||||
available_versions = self._get_available_plugin_versions()
|
||||
|
||||
if not available_versions:
|
||||
error_msg = (
|
||||
f"插件目录不存在:\n{self.plugin_base_path}\n\n"
|
||||
f"未找到任何可用的插件版本。"
|
||||
)
|
||||
else:
|
||||
error_msg = (
|
||||
f"Maya 版本: {self.maya_version}\n"
|
||||
f"插件版本不匹配!\n\n"
|
||||
f"当前 Maya 版本: {self.maya_version}\n"
|
||||
f"可用插件版本: {', '.join(available_versions)}\n\n"
|
||||
f"插件将不会被加载。\n"
|
||||
f"请检查 Maya 软件版本或安装对应版本的插件。"
|
||||
)
|
||||
|
||||
return None, error_msg
|
||||
|
||||
print(f"[INFO] Using plugin path: {versioned_path}")
|
||||
return versioned_path, None
|
||||
|
||||
def _setup_environment(self) -> Tuple[Dict[str, str], Optional[str]]:
|
||||
"""设置 Maya 环境变量
|
||||
|
||||
Returns:
|
||||
(环境变量字典, 错误消息) 元组
|
||||
"""
|
||||
env = os.environ.copy()
|
||||
|
||||
# 获取带版本号的插件路径
|
||||
plugin_path, error_msg = self._get_versioned_plugin_path()
|
||||
|
||||
if not plugin_path:
|
||||
return env, error_msg
|
||||
|
||||
# 设置 Maya 环境变量
|
||||
shelves_path = os.path.join(plugin_path, "shelves")
|
||||
scripts_path = os.path.join(plugin_path, "scripts")
|
||||
plugins_path = os.path.join(plugin_path, "plug-ins")
|
||||
icons_path = os.path.join(plugin_path, "icons")
|
||||
|
||||
# MAYA_SHELF_PATH - 工具架路径(追加到现有路径)
|
||||
if os.path.exists(shelves_path):
|
||||
existing_shelf_path = env.get("MAYA_SHELF_PATH", "")
|
||||
if existing_shelf_path:
|
||||
env["MAYA_SHELF_PATH"] = f"{shelves_path};{existing_shelf_path}"
|
||||
else:
|
||||
env["MAYA_SHELF_PATH"] = shelves_path
|
||||
print(f"[OK] Set MAYA_SHELF_PATH: {shelves_path}")
|
||||
|
||||
# MAYA_SCRIPT_PATH - MEL/Python 脚本路径(追加到现有路径)
|
||||
if os.path.exists(scripts_path):
|
||||
existing_script_path = env.get("MAYA_SCRIPT_PATH", "")
|
||||
if existing_script_path:
|
||||
env["MAYA_SCRIPT_PATH"] = f"{scripts_path};{existing_script_path}"
|
||||
else:
|
||||
env["MAYA_SCRIPT_PATH"] = scripts_path
|
||||
print(f"[OK] Set MAYA_SCRIPT_PATH: {scripts_path}")
|
||||
|
||||
# PYTHONPATH - Python 模块路径
|
||||
if os.path.exists(scripts_path):
|
||||
existing_pythonpath = env.get("PYTHONPATH", "")
|
||||
if existing_pythonpath:
|
||||
env["PYTHONPATH"] = f"{scripts_path};{existing_pythonpath}"
|
||||
else:
|
||||
env["PYTHONPATH"] = scripts_path
|
||||
print(f"[OK] Set PYTHONPATH: {scripts_path}")
|
||||
|
||||
# MAYA_PLUG_IN_PATH - 插件路径(追加到现有路径)
|
||||
if os.path.exists(plugins_path):
|
||||
existing_plugin_path = env.get("MAYA_PLUG_IN_PATH", "")
|
||||
if existing_plugin_path:
|
||||
env["MAYA_PLUG_IN_PATH"] = f"{plugins_path};{existing_plugin_path}"
|
||||
else:
|
||||
env["MAYA_PLUG_IN_PATH"] = plugins_path
|
||||
print(f"[OK] Set MAYA_PLUG_IN_PATH: {plugins_path}")
|
||||
|
||||
# XBMLANGPATH - 图标路径(追加到现有路径)
|
||||
if os.path.exists(icons_path):
|
||||
existing_icon_path = env.get("XBMLANGPATH", "")
|
||||
if existing_icon_path:
|
||||
env["XBMLANGPATH"] = f"{icons_path};{existing_icon_path}"
|
||||
else:
|
||||
env["XBMLANGPATH"] = icons_path
|
||||
print(f"[OK] Set XBMLANGPATH: {icons_path}")
|
||||
|
||||
return env, None
|
||||
|
||||
def launch(self, show_error_dialog: bool = True) -> bool:
|
||||
"""启动 Maya
|
||||
|
||||
Args:
|
||||
show_error_dialog: 是否显示错误对话框
|
||||
|
||||
Returns:
|
||||
是否成功启动
|
||||
"""
|
||||
try:
|
||||
# 检查 Maya 可执行文件是否存在
|
||||
if not os.path.exists(self.maya_exe_path):
|
||||
error_msg = f"Maya 可执行文件不存在:\n{self.maya_exe_path}"
|
||||
print(f"[ERROR] {error_msg}")
|
||||
if show_error_dialog and HAS_QT:
|
||||
QMessageBox.critical(None, "Maya 启动失败", error_msg)
|
||||
return False
|
||||
|
||||
# 设置环境变量
|
||||
env, error_msg = self._setup_environment()
|
||||
|
||||
# 如果有错误消息,显示警告但继续启动(不加载插件)
|
||||
if error_msg:
|
||||
print(f"[WARNING] {error_msg}")
|
||||
if show_error_dialog and HAS_QT:
|
||||
QMessageBox.warning(
|
||||
None,
|
||||
"插件版本不匹配",
|
||||
error_msg + "\n\nMaya 将在不加载插件的情况下启动。"
|
||||
)
|
||||
|
||||
# 启动 Maya
|
||||
print(f"[INFO] Launching Maya: {self.maya_exe_path}")
|
||||
subprocess.Popen([self.maya_exe_path], env=env)
|
||||
|
||||
if error_msg:
|
||||
print(f"[OK] Maya launched without plugins")
|
||||
else:
|
||||
print(f"[OK] Maya launched successfully with plugins")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"启动 Maya 时发生错误:\n{str(e)}"
|
||||
print(f"[ERROR] {error_msg}")
|
||||
if show_error_dialog and HAS_QT:
|
||||
QMessageBox.critical(None, "Maya 启动失败", error_msg)
|
||||
return False
|
||||
|
||||
|
||||
def launch_maya(maya_exe_path: str, plugin_base_path: str) -> bool:
|
||||
"""启动 Maya 的便捷函数
|
||||
|
||||
Args:
|
||||
maya_exe_path: Maya 可执行文件路径
|
||||
plugin_base_path: 插件基础路径(不含版本号)
|
||||
|
||||
Returns:
|
||||
是否成功启动
|
||||
"""
|
||||
launcher = MayaLauncher(maya_exe_path, plugin_base_path)
|
||||
return launcher.launch()
|
||||
9
plugins/substancepainter/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Substance Painter 插件模块
|
||||
"""
|
||||
|
||||
from .launcher import SubstancePainterLauncher, launch_substance_painter
|
||||
|
||||
__all__ = ['SubstancePainterLauncher', 'launch_substance_painter']
|
||||
435
plugins/substancepainter/launcher.py
Normal file
@@ -0,0 +1,435 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Substance Painter 启动器
|
||||
负责设置 SP 库路径和自动配置
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import subprocess
|
||||
import shutil
|
||||
import tempfile
|
||||
import time
|
||||
import psutil
|
||||
from typing import Dict, Optional
|
||||
from pathlib import Path
|
||||
from .registry_manager import SPRegistryManager
|
||||
|
||||
|
||||
class SubstancePainterLauncher:
|
||||
"""Substance Painter 启动器"""
|
||||
|
||||
def __init__(self, sp_exe_path: str, shelf_path: str, project_name: str = "NexusLauncher"):
|
||||
"""
|
||||
初始化 Substance Painter 启动器
|
||||
|
||||
Args:
|
||||
sp_exe_path: Substance Painter 可执行文件路径
|
||||
shelf_path: 库路径(sp_shelf_path)
|
||||
project_name: 项目名称,用作库名称
|
||||
"""
|
||||
self.sp_exe_path = sp_exe_path
|
||||
self.shelf_path = shelf_path
|
||||
self.project_name = project_name
|
||||
|
||||
# SP 配置文件路径
|
||||
self.sp_config_dir = self._get_sp_config_dir()
|
||||
|
||||
def _get_sp_config_dir(self) -> Optional[Path]:
|
||||
"""获取 Substance Painter 配置目录
|
||||
|
||||
Returns:
|
||||
配置目录路径,如果找不到返回 None
|
||||
"""
|
||||
# 方法1: 使用 USERPROFILE
|
||||
userprofile = os.environ.get('USERPROFILE', '')
|
||||
if userprofile:
|
||||
documents = Path(userprofile) / 'Documents'
|
||||
else:
|
||||
documents = Path.home() / 'Documents'
|
||||
|
||||
# 方法2: 使用注册表获取文档路径
|
||||
try:
|
||||
import winreg
|
||||
key = winreg.OpenKey(
|
||||
winreg.HKEY_CURRENT_USER,
|
||||
r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
|
||||
)
|
||||
documents_path, _ = winreg.QueryValueEx(key, "Personal")
|
||||
winreg.CloseKey(key)
|
||||
documents = Path(documents_path)
|
||||
print(f"[SubstancePainter] Documents folder (from registry): {documents}")
|
||||
except Exception as e:
|
||||
print(f"[SubstancePainter] Could not get documents from registry: {e}")
|
||||
|
||||
# 尝试查找 Substance Painter 配置目录
|
||||
sp_dirs = [
|
||||
# SP 2023+
|
||||
documents / 'Adobe' / 'Adobe Substance 3D Painter',
|
||||
# 旧版本
|
||||
documents / 'Allegorithmic' / 'Substance Painter',
|
||||
# 备用路径
|
||||
Path(userprofile) / 'AppData' / 'Local' / 'Adobe' / 'Adobe Substance 3D Painter',
|
||||
Path(userprofile) / 'AppData' / 'Roaming' / 'Adobe' / 'Adobe Substance 3D Painter',
|
||||
]
|
||||
|
||||
print(f"[SubstancePainter] Searching for config directory...")
|
||||
for sp_dir in sp_dirs:
|
||||
print(f"[SubstancePainter] Checking: {sp_dir}")
|
||||
if sp_dir.exists():
|
||||
print(f"[SubstancePainter] ✓ Found config directory: {sp_dir}")
|
||||
|
||||
# 列出配置目录中的文件
|
||||
try:
|
||||
config_files = list(sp_dir.glob('*.json'))
|
||||
if config_files:
|
||||
print(f"[SubstancePainter] Config files found:")
|
||||
for f in config_files:
|
||||
print(f"[SubstancePainter] - {f.name}")
|
||||
except Exception as e:
|
||||
print(f"[SubstancePainter] Could not list config files: {e}")
|
||||
|
||||
return sp_dir
|
||||
|
||||
# 如果找不到,尝试创建默认目录
|
||||
default_dir = documents / 'Adobe' / 'Adobe Substance 3D Painter'
|
||||
print(f"[SubstancePainter] Config directory not found")
|
||||
print(f"[SubstancePainter] Will create default directory: {default_dir}")
|
||||
|
||||
try:
|
||||
default_dir.mkdir(parents=True, exist_ok=True)
|
||||
print(f"[SubstancePainter] ✓ Created config directory")
|
||||
return default_dir
|
||||
except Exception as e:
|
||||
print(f"[SubstancePainter] ✗ Failed to create config directory: {e}")
|
||||
return None
|
||||
|
||||
def _get_shelf_config_path(self) -> Optional[Path]:
|
||||
"""获取库配置文件路径
|
||||
|
||||
Returns:
|
||||
库配置文件路径,如果找不到返回 None
|
||||
"""
|
||||
if not self.sp_config_dir:
|
||||
return None
|
||||
|
||||
# SP 库配置文件可能的位置
|
||||
possible_configs = [
|
||||
self.sp_config_dir / 'shelf.json',
|
||||
self.sp_config_dir / 'shelves.json',
|
||||
self.sp_config_dir / 'assets.json',
|
||||
self.sp_config_dir / 'preferences.json',
|
||||
]
|
||||
|
||||
# 先检查是否已存在配置文件
|
||||
for config_path in possible_configs:
|
||||
if config_path.exists():
|
||||
print(f"[SubstancePainter] Found existing config: {config_path.name}")
|
||||
return config_path
|
||||
|
||||
# 如果都不存在,使用默认的 shelf.json
|
||||
shelf_config = self.sp_config_dir / 'shelf.json'
|
||||
print(f"[SubstancePainter] Will create new config: {shelf_config.name}")
|
||||
|
||||
return shelf_config
|
||||
|
||||
def _create_default_shelf_config(self) -> dict:
|
||||
"""创建默认的库配置
|
||||
|
||||
Returns:
|
||||
默认库配置字典
|
||||
"""
|
||||
# SP 的库配置格式(基于官方文档)
|
||||
# 库会自动创建标准文件夹结构:
|
||||
# alphas, colorluts, effects, environments, generators, materials, etc.
|
||||
|
||||
# SP 库名称规则:只能包含小写字母、数字、下划线和连字符
|
||||
import re
|
||||
safe_name = self.project_name.lower().replace(" ", "_")
|
||||
safe_name = re.sub(r'[^a-z0-9_-]', '', safe_name)
|
||||
|
||||
return {
|
||||
"libraries": [
|
||||
{
|
||||
"name": safe_name,
|
||||
"path": self.shelf_path.replace("\\", "/"),
|
||||
"default": True
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
def _update_shelf_config(self) -> bool:
|
||||
"""更新 Substance Painter 库配置
|
||||
|
||||
Returns:
|
||||
是否成功更新
|
||||
"""
|
||||
try:
|
||||
shelf_config_path = self._get_shelf_config_path()
|
||||
|
||||
if not shelf_config_path:
|
||||
print(f"[SubstancePainter] Warning: Cannot update shelf config (config path not found)")
|
||||
print(f"[SubstancePainter] SP will start with default library settings")
|
||||
return False # 返回 False 但不影响启动
|
||||
|
||||
# 读取现有配置(如果存在)
|
||||
if shelf_config_path.exists():
|
||||
try:
|
||||
with open(shelf_config_path, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
print(f"[SubstancePainter] Loaded existing config from: {shelf_config_path.name}")
|
||||
except Exception as e:
|
||||
print(f"[SubstancePainter] Failed to load existing config: {e}")
|
||||
config = self._create_default_shelf_config()
|
||||
else:
|
||||
print(f"[SubstancePainter] Creating new config")
|
||||
config = self._create_default_shelf_config()
|
||||
|
||||
# 确保有 libraries 列表(SP 使用 "libraries" 而不是 "shelves")
|
||||
if "libraries" not in config:
|
||||
config["libraries"] = []
|
||||
|
||||
# 清理项目名称(移除空格和特殊字符,转为小写)
|
||||
# SP 库名称规则:只能包含小写字母、数字、下划线和连字符
|
||||
import re
|
||||
clean_project_name = self.project_name.lower().replace(" ", "_")
|
||||
clean_project_name = re.sub(r'[^a-z0-9_-]', '', clean_project_name)
|
||||
|
||||
# 查找是否已存在同名库
|
||||
existing_library = None
|
||||
for lib in config["libraries"]:
|
||||
if lib.get("name") == clean_project_name:
|
||||
existing_library = lib
|
||||
break
|
||||
|
||||
if existing_library:
|
||||
# 更新现有库
|
||||
print(f"[SubstancePainter] Updating existing library: {clean_project_name}")
|
||||
existing_library["path"] = self.shelf_path.replace("\\", "/")
|
||||
existing_library["default"] = True
|
||||
else:
|
||||
# 添加新库
|
||||
print(f"[SubstancePainter] Adding new library: {clean_project_name}")
|
||||
config["libraries"].append({
|
||||
"name": clean_project_name,
|
||||
"path": self.shelf_path.replace("\\", "/"),
|
||||
"default": True
|
||||
})
|
||||
|
||||
# 将其他库设置为非默认
|
||||
for lib in config["libraries"]:
|
||||
if lib.get("name") != clean_project_name:
|
||||
lib["default"] = False
|
||||
|
||||
# 保存配置
|
||||
shelf_config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(shelf_config_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(config, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"[SubstancePainter] ✓ Shelf config updated successfully")
|
||||
print(f"[SubstancePainter] Library: {self.project_name}")
|
||||
print(f"[SubstancePainter] Path: {self.shelf_path}")
|
||||
print(f"[SubstancePainter] Default: True")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"[SubstancePainter] Error updating shelf config: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def _create_library_structure(self) -> bool:
|
||||
"""创建 SP 库的标准文件夹结构
|
||||
|
||||
Returns:
|
||||
是否成功创建
|
||||
"""
|
||||
try:
|
||||
# SP 库的标准文件夹结构
|
||||
standard_folders = [
|
||||
'alphas', # Alpha 贴图
|
||||
'brushes', # 笔刷预设
|
||||
'colorluts', # 颜色查找表
|
||||
'effects', # 滤镜/效果
|
||||
'environments', # 环境贴图
|
||||
'generators', # 生成器
|
||||
'materials', # 材质
|
||||
'particles', # 粒子预设
|
||||
'presets', # 预设
|
||||
'shaders', # 着色器
|
||||
'smart-materials', # 智能材质
|
||||
'smart-masks', # 智能蒙版
|
||||
'textures', # 纹理
|
||||
'export-presets', # 导出预设
|
||||
]
|
||||
|
||||
# 确保使用绝对路径
|
||||
base_path = Path(self.shelf_path).resolve()
|
||||
print(f"[SubstancePainter] Library base path: {base_path}")
|
||||
|
||||
# 创建基础目录
|
||||
if not base_path.exists():
|
||||
print(f"[SubstancePainter] Creating library directory...")
|
||||
base_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 创建标准文件夹
|
||||
created_folders = []
|
||||
for folder in standard_folders:
|
||||
folder_path = base_path / folder
|
||||
if not folder_path.exists():
|
||||
folder_path.mkdir(exist_ok=True)
|
||||
created_folders.append(folder)
|
||||
|
||||
if created_folders:
|
||||
print(f"[SubstancePainter] Created {len(created_folders)} standard folders")
|
||||
print(f"[SubstancePainter] ✓ Library structure ready")
|
||||
else:
|
||||
print(f"[SubstancePainter] ✓ Library structure already exists")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"[SubstancePainter] Error creating library structure: {e}")
|
||||
return False
|
||||
|
||||
def _setup_environment(self) -> Dict[str, str]:
|
||||
"""设置 Substance Painter 环境变量
|
||||
|
||||
Returns:
|
||||
包含环境变量的字典
|
||||
"""
|
||||
env = os.environ.copy()
|
||||
|
||||
# 创建库文件夹结构
|
||||
self._create_library_structure()
|
||||
|
||||
# 设置环境变量供 SP Python 插件使用
|
||||
# SP 库名称规则:只能包含小写字母、数字、下划线和连字符
|
||||
import re
|
||||
clean_project_name = self.project_name.lower().replace(" ", "_")
|
||||
clean_project_name = re.sub(r'[^a-z0-9_-]', '', clean_project_name)
|
||||
env["NEXUS_SP_LIBRARY_NAME"] = clean_project_name
|
||||
env["NEXUS_SP_LIBRARY_PATH"] = self.shelf_path
|
||||
print(f"[SubstancePainter] Set NEXUS_SP_LIBRARY_NAME: {clean_project_name}")
|
||||
print(f"[SubstancePainter] Set NEXUS_SP_LIBRARY_PATH: {self.shelf_path}")
|
||||
|
||||
# 创建临时插件目录并复制插件
|
||||
try:
|
||||
# 创建临时目录
|
||||
temp_plugin_dir = Path(tempfile.gettempdir()) / "NexusLauncher_SP_Plugins"
|
||||
temp_plugin_dir.mkdir(exist_ok=True)
|
||||
|
||||
# 复制插件文件
|
||||
plugin_source = Path(__file__).parent / "sp_api_plugin.py"
|
||||
plugin_dest = temp_plugin_dir / "sp_api_plugin.py"
|
||||
|
||||
if plugin_source.exists():
|
||||
shutil.copy2(plugin_source, plugin_dest)
|
||||
print(f"[SubstancePainter] Copied plugin to: {plugin_dest}")
|
||||
|
||||
# 设置 SP 插件路径
|
||||
env["SUBSTANCE_PAINTER_PLUGINS_PATH"] = str(temp_plugin_dir)
|
||||
print(f"[SubstancePainter] Set SUBSTANCE_PAINTER_PLUGINS_PATH: {temp_plugin_dir}")
|
||||
else:
|
||||
print(f"[SubstancePainter] Warning: Plugin source not found: {plugin_source}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[SubstancePainter] Warning: Failed to setup plugin: {e}")
|
||||
|
||||
return env
|
||||
|
||||
def _is_sp_running(self) -> bool:
|
||||
"""检查 SP 是否正在运行"""
|
||||
for proc in psutil.process_iter(['name']):
|
||||
try:
|
||||
if 'Adobe Substance 3D Painter' in proc.info['name']:
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
return False
|
||||
|
||||
def _kill_sp(self):
|
||||
"""关闭所有 SP 进程"""
|
||||
print(f"[SubstancePainter] Closing existing SP instances...")
|
||||
for proc in psutil.process_iter(['name', 'pid']):
|
||||
try:
|
||||
if 'Adobe Substance 3D Painter' in proc.info['name']:
|
||||
proc.kill()
|
||||
print(f"[SubstancePainter] Killed process: {proc.info['pid']}")
|
||||
except:
|
||||
pass
|
||||
time.sleep(2) # 等待进程完全关闭
|
||||
|
||||
def launch(self) -> bool:
|
||||
"""启动 Substance Painter
|
||||
|
||||
Returns:
|
||||
是否成功启动
|
||||
"""
|
||||
try:
|
||||
# 检查 SP 可执行文件是否存在
|
||||
if not os.path.exists(self.sp_exe_path):
|
||||
print(f"[SubstancePainter] Error: Executable not found: {self.sp_exe_path}")
|
||||
return False
|
||||
|
||||
print(f"[SubstancePainter] ========================================")
|
||||
print(f"[SubstancePainter] Launching Substance Painter")
|
||||
print(f"[SubstancePainter] ========================================")
|
||||
print(f"[SubstancePainter] Project: {self.project_name}")
|
||||
print(f"[SubstancePainter] Shelf Path: {self.shelf_path}")
|
||||
print(f"")
|
||||
|
||||
# 步骤 1: 清理旧的项目库
|
||||
print(f"[SubstancePainter] Step 1: Cleaning old project libraries...")
|
||||
SPRegistryManager.remove_project_libraries()
|
||||
print(f"")
|
||||
|
||||
# 步骤 2: 添加当前项目库
|
||||
print(f"[SubstancePainter] Step 2: Adding current project library...")
|
||||
self._create_library_structure() # 确保库文件夹存在
|
||||
success = SPRegistryManager.add_project_library(self.project_name, self.shelf_path, set_as_default=True)
|
||||
if not success:
|
||||
print(f"[SubstancePainter] Warning: Failed to add library to registry")
|
||||
print(f"")
|
||||
|
||||
# 步骤 3: 检查是否需要重启 SP
|
||||
if self._is_sp_running():
|
||||
print(f"[SubstancePainter] Step 3: SP is running, restarting to apply changes...")
|
||||
self._kill_sp()
|
||||
time.sleep(2)
|
||||
else:
|
||||
print(f"[SubstancePainter] Step 3: No restart needed")
|
||||
print(f"")
|
||||
|
||||
# 步骤 4: 设置环境变量
|
||||
env = self._setup_environment()
|
||||
|
||||
# 步骤 5: 启动 Substance Painter
|
||||
print(f"[SubstancePainter] Step 4: Starting application...")
|
||||
subprocess.Popen([self.sp_exe_path], env=env)
|
||||
|
||||
print(f"[SubstancePainter] ✓ Substance Painter launched successfully")
|
||||
print(f"[SubstancePainter] ========================================")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"[SubstancePainter] Error: Failed to launch: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def launch_substance_painter(sp_exe_path: str, shelf_path: str, project_name: str = "NexusLauncher") -> bool:
|
||||
"""启动 Substance Painter 的便捷函数
|
||||
|
||||
Args:
|
||||
sp_exe_path: Substance Painter 可执行文件路径
|
||||
shelf_path: 库路径(sp_shelf_path)
|
||||
project_name: 项目名称,用作库名称
|
||||
|
||||
Returns:
|
||||
是否成功启动
|
||||
"""
|
||||
launcher = SubstancePainterLauncher(sp_exe_path, shelf_path, project_name)
|
||||
return launcher.launch()
|
||||
252
plugins/substancepainter/registry_manager.py
Normal file
@@ -0,0 +1,252 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Substance Painter 注册表管理器
|
||||
用于在启动前配置库,关闭后清理库
|
||||
"""
|
||||
|
||||
import winreg
|
||||
import re
|
||||
import os
|
||||
|
||||
|
||||
class SPRegistryManager:
|
||||
"""SP 注册表管理器"""
|
||||
|
||||
REGISTRY_KEY = r"SOFTWARE\Adobe\Adobe Substance 3D Painter\Shelf\pathInfos"
|
||||
SHELF_KEY = r"SOFTWARE\Adobe\Adobe Substance 3D Painter\Shelf"
|
||||
|
||||
@staticmethod
|
||||
def normalize_library_name(name):
|
||||
"""规范化库名称(小写、下划线、连字符)"""
|
||||
safe_name = name.lower().replace(' ', '_')
|
||||
safe_name = re.sub(r'[^a-z0-9_-]', '', safe_name)
|
||||
return safe_name
|
||||
|
||||
@staticmethod
|
||||
def get_all_libraries():
|
||||
"""获取所有库配置
|
||||
|
||||
Returns:
|
||||
list: [(id, name, path, disabled), ...]
|
||||
"""
|
||||
try:
|
||||
reg_conn = winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER)
|
||||
key = winreg.OpenKey(reg_conn, SPRegistryManager.REGISTRY_KEY, 0, winreg.KEY_READ)
|
||||
|
||||
libraries = []
|
||||
sub_key_count = winreg.QueryInfoKey(key)[0]
|
||||
|
||||
for i in range(sub_key_count):
|
||||
sub_key_name = winreg.EnumKey(key, i)
|
||||
sub_key = winreg.OpenKey(reg_conn, f"{SPRegistryManager.REGISTRY_KEY}\\{sub_key_name}", 0, winreg.KEY_READ)
|
||||
|
||||
try:
|
||||
name = winreg.QueryValueEx(sub_key, "name")[0]
|
||||
path = winreg.QueryValueEx(sub_key, "path")[0]
|
||||
disabled = winreg.QueryValueEx(sub_key, "disabled")[0]
|
||||
libraries.append((int(sub_key_name), name, path, disabled))
|
||||
except:
|
||||
pass
|
||||
finally:
|
||||
winreg.CloseKey(sub_key)
|
||||
|
||||
winreg.CloseKey(key)
|
||||
return libraries
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Registry] Error reading libraries: {e}")
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def remove_project_libraries():
|
||||
"""删除所有项目库(保留系统库)
|
||||
|
||||
系统库:your_assets, starter_assets, system_fonts, user_fonts
|
||||
"""
|
||||
system_libs = ['your_assets', 'starter_assets', 'system_fonts', 'user_fonts']
|
||||
|
||||
try:
|
||||
libraries = SPRegistryManager.get_all_libraries()
|
||||
to_remove = []
|
||||
|
||||
for lib_id, name, path, disabled in libraries:
|
||||
if name not in system_libs:
|
||||
to_remove.append(lib_id)
|
||||
print(f"[Registry] Will remove: {name} (ID: {lib_id})")
|
||||
|
||||
if not to_remove:
|
||||
print(f"[Registry] No project libraries to remove")
|
||||
return True
|
||||
|
||||
# 删除项目库
|
||||
reg_conn = winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER)
|
||||
for lib_id in to_remove:
|
||||
try:
|
||||
winreg.DeleteKey(winreg.HKEY_CURRENT_USER, f"{SPRegistryManager.REGISTRY_KEY}\\{lib_id}")
|
||||
print(f"[Registry] Removed library ID: {lib_id}")
|
||||
except Exception as e:
|
||||
print(f"[Registry] Error removing {lib_id}: {e}")
|
||||
|
||||
# 重新编号剩余的库(确保连续)
|
||||
SPRegistryManager._reindex_libraries()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Registry] Error removing libraries: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _reindex_libraries():
|
||||
"""重新编号库,确保 ID 连续"""
|
||||
try:
|
||||
libraries = SPRegistryManager.get_all_libraries()
|
||||
libraries.sort(key=lambda x: x[0]) # 按 ID 排序
|
||||
|
||||
reg_conn = winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER)
|
||||
|
||||
# 如果已经连续,不需要重新编号
|
||||
expected_ids = list(range(1, len(libraries) + 1))
|
||||
actual_ids = [lib[0] for lib in libraries]
|
||||
|
||||
if expected_ids == actual_ids:
|
||||
print(f"[Registry] Library IDs are already continuous")
|
||||
return
|
||||
|
||||
print(f"[Registry] Reindexing libraries...")
|
||||
|
||||
# 先将所有库移到临时 ID
|
||||
temp_offset = 1000
|
||||
for lib_id, name, path, disabled in libraries:
|
||||
old_key_path = f"{SPRegistryManager.REGISTRY_KEY}\\{lib_id}"
|
||||
new_key_path = f"{SPRegistryManager.REGISTRY_KEY}\\{lib_id + temp_offset}"
|
||||
|
||||
# 读取旧值
|
||||
old_key = winreg.OpenKey(reg_conn, old_key_path, 0, winreg.KEY_READ)
|
||||
name_val = winreg.QueryValueEx(old_key, "name")[0]
|
||||
path_val = winreg.QueryValueEx(old_key, "path")[0]
|
||||
disabled_val = winreg.QueryValueEx(old_key, "disabled")[0]
|
||||
winreg.CloseKey(old_key)
|
||||
|
||||
# 创建新键
|
||||
new_key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, new_key_path)
|
||||
winreg.SetValueEx(new_key, "name", 0, winreg.REG_SZ, name_val)
|
||||
winreg.SetValueEx(new_key, "path", 0, winreg.REG_SZ, path_val)
|
||||
winreg.SetValueEx(new_key, "disabled", 0, winreg.REG_SZ, disabled_val)
|
||||
winreg.CloseKey(new_key)
|
||||
|
||||
# 删除旧键
|
||||
winreg.DeleteKey(winreg.HKEY_CURRENT_USER, old_key_path)
|
||||
|
||||
# 再将临时 ID 移到正确的连续 ID
|
||||
for new_id, (old_id, name, path, disabled) in enumerate(libraries, start=1):
|
||||
temp_id = old_id + temp_offset
|
||||
temp_key_path = f"{SPRegistryManager.REGISTRY_KEY}\\{temp_id}"
|
||||
final_key_path = f"{SPRegistryManager.REGISTRY_KEY}\\{new_id}"
|
||||
|
||||
# 读取临时键
|
||||
temp_key = winreg.OpenKey(reg_conn, temp_key_path, 0, winreg.KEY_READ)
|
||||
name_val = winreg.QueryValueEx(temp_key, "name")[0]
|
||||
path_val = winreg.QueryValueEx(temp_key, "path")[0]
|
||||
disabled_val = winreg.QueryValueEx(temp_key, "disabled")[0]
|
||||
winreg.CloseKey(temp_key)
|
||||
|
||||
# 创建最终键
|
||||
final_key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, final_key_path)
|
||||
winreg.SetValueEx(final_key, "name", 0, winreg.REG_SZ, name_val)
|
||||
winreg.SetValueEx(final_key, "path", 0, winreg.REG_SZ, path_val)
|
||||
winreg.SetValueEx(final_key, "disabled", 0, winreg.REG_SZ, disabled_val)
|
||||
winreg.CloseKey(final_key)
|
||||
|
||||
# 删除临时键
|
||||
winreg.DeleteKey(winreg.HKEY_CURRENT_USER, temp_key_path)
|
||||
|
||||
# 更新 size
|
||||
key = winreg.OpenKeyEx(reg_conn, SPRegistryManager.REGISTRY_KEY, 0, winreg.KEY_SET_VALUE)
|
||||
winreg.SetValueEx(key, "size", 0, winreg.REG_DWORD, len(libraries))
|
||||
winreg.CloseKey(key)
|
||||
|
||||
print(f"[Registry] Reindexing complete, new size: {len(libraries)}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Registry] Error reindexing: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
@staticmethod
|
||||
def add_project_library(library_name, library_path, set_as_default=True):
|
||||
"""添加项目库
|
||||
|
||||
Args:
|
||||
library_name: 库名称
|
||||
library_path: 库路径
|
||||
set_as_default: 是否设置为默认
|
||||
|
||||
Returns:
|
||||
bool: 是否成功
|
||||
"""
|
||||
try:
|
||||
safe_name = SPRegistryManager.normalize_library_name(library_name)
|
||||
normalized_path = library_path.replace('\\', '/')
|
||||
|
||||
print(f"[Registry] Adding library: {safe_name}")
|
||||
print(f"[Registry] Path: {normalized_path}")
|
||||
|
||||
# 检查是否已存在
|
||||
libraries = SPRegistryManager.get_all_libraries()
|
||||
for lib_id, name, path, disabled in libraries:
|
||||
if name == safe_name:
|
||||
print(f"[Registry] Library already exists: {name}")
|
||||
return True
|
||||
|
||||
# 找到下一个 ID
|
||||
if libraries:
|
||||
next_id = max(lib[0] for lib in libraries) + 1
|
||||
else:
|
||||
next_id = 1
|
||||
|
||||
# 创建新库
|
||||
reg_conn = winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER)
|
||||
new_key_path = f"{SPRegistryManager.REGISTRY_KEY}\\{next_id}"
|
||||
new_key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, new_key_path)
|
||||
winreg.SetValueEx(new_key, "name", 0, winreg.REG_SZ, safe_name)
|
||||
winreg.SetValueEx(new_key, "path", 0, winreg.REG_SZ, normalized_path)
|
||||
winreg.SetValueEx(new_key, "disabled", 0, winreg.REG_SZ, "false")
|
||||
winreg.CloseKey(new_key)
|
||||
|
||||
# 更新 size
|
||||
key = winreg.OpenKeyEx(reg_conn, SPRegistryManager.REGISTRY_KEY, 0, winreg.KEY_SET_VALUE)
|
||||
winreg.SetValueEx(key, "size", 0, winreg.REG_DWORD, len(libraries) + 1)
|
||||
winreg.CloseKey(key)
|
||||
|
||||
print(f"[Registry] Library added with ID: {next_id}")
|
||||
|
||||
# 设置为默认库
|
||||
if set_as_default:
|
||||
shelf_key = winreg.OpenKeyEx(reg_conn, SPRegistryManager.SHELF_KEY, 0, winreg.KEY_SET_VALUE)
|
||||
winreg.SetValueEx(shelf_key, "writableShelf", 0, winreg.REG_SZ, safe_name)
|
||||
winreg.CloseKey(shelf_key)
|
||||
print(f"[Registry] Set as default library")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Registry] Error adding library: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def needs_restart():
|
||||
"""检查是否需要重启 SP 以应用更改
|
||||
|
||||
Returns:
|
||||
bool: 是否需要重启
|
||||
"""
|
||||
# 如果 SP 正在运行,注册表更改需要重启才能生效
|
||||
import psutil
|
||||
for proc in psutil.process_iter(['name']):
|
||||
if 'Adobe Substance 3D Painter' in proc.info['name']:
|
||||
return True
|
||||
return False
|
||||
318
plugins/substancepainter/sp_api_plugin.py
Normal file
@@ -0,0 +1,318 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Substance Painter API 插件
|
||||
通过 Python API 自动添加项目库
|
||||
|
||||
此文件会被复制到临时目录,在 SP 启动时自动执行
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def setup_project_library_via_registry(library_name, library_path):
|
||||
"""通过注册表设置项目库(Windows)
|
||||
|
||||
Args:
|
||||
library_name: 库名称
|
||||
library_path: 库路径
|
||||
|
||||
Returns:
|
||||
bool: 是否成功
|
||||
"""
|
||||
try:
|
||||
import winreg
|
||||
import re
|
||||
|
||||
# SP 库名称规则:只能包含小写字母、数字、下划线和连字符
|
||||
safe_library_name = library_name.lower().replace(' ', '_')
|
||||
safe_library_name = re.sub(r'[^a-z0-9_-]', '', safe_library_name)
|
||||
|
||||
print(f"[NexusLauncher] Setting up library via registry: {library_name}")
|
||||
if safe_library_name != library_name:
|
||||
print(f"[NexusLauncher] Normalized name: {safe_library_name}")
|
||||
print(f"[NexusLauncher] Path: {library_path}")
|
||||
|
||||
# 检查路径是否存在
|
||||
if not os.path.exists(library_path):
|
||||
print(f"[NexusLauncher] Error: Library path does not exist: {library_path}")
|
||||
return False
|
||||
|
||||
# 规范化路径
|
||||
normalized_path = library_path.replace('\\', '/')
|
||||
|
||||
# 注册表路径
|
||||
registry_key_name = r"SOFTWARE\Adobe\Adobe Substance 3D Painter\Shelf\pathInfos"
|
||||
|
||||
# 连接到注册表
|
||||
reg_connection = winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER)
|
||||
|
||||
try:
|
||||
# 打开父键
|
||||
key = winreg.OpenKey(reg_connection, registry_key_name, 0, winreg.KEY_READ)
|
||||
except FileNotFoundError:
|
||||
print(f"[NexusLauncher] Registry key not found. Please open SP settings once to create it.")
|
||||
return False
|
||||
|
||||
# 检查是否已存在同名库
|
||||
sub_key_count = winreg.QueryInfoKey(key)[0]
|
||||
shelf_exists = False
|
||||
shelf_number = 0
|
||||
|
||||
for x in range(sub_key_count):
|
||||
sub_key_name = winreg.EnumKey(key, x)
|
||||
shelf_number = max(shelf_number, int(sub_key_name))
|
||||
|
||||
sub_key = winreg.OpenKey(reg_connection, registry_key_name + "\\" + sub_key_name, 0, winreg.KEY_READ)
|
||||
try:
|
||||
existing_name = winreg.QueryValueEx(sub_key, "name")[0]
|
||||
existing_path = winreg.QueryValueEx(sub_key, "path")[0]
|
||||
|
||||
if existing_name == safe_library_name:
|
||||
print(f"[NexusLauncher] Library already exists: {existing_name} at {existing_path}")
|
||||
shelf_exists = True
|
||||
winreg.CloseKey(sub_key)
|
||||
break
|
||||
finally:
|
||||
winreg.CloseKey(sub_key)
|
||||
|
||||
if not shelf_exists:
|
||||
# 添加新库
|
||||
shelf_number += 1
|
||||
print(f"[NexusLauncher] Adding new library with ID: {shelf_number}")
|
||||
|
||||
# 创建新键
|
||||
new_key = winreg.CreateKey(key, str(shelf_number))
|
||||
winreg.SetValueEx(new_key, "disabled", 0, winreg.REG_SZ, "false")
|
||||
winreg.SetValueEx(new_key, "name", 0, winreg.REG_SZ, safe_library_name)
|
||||
winreg.SetValueEx(new_key, "path", 0, winreg.REG_SZ, normalized_path)
|
||||
winreg.CloseKey(new_key)
|
||||
|
||||
# 更新计数
|
||||
try:
|
||||
count = winreg.QueryValueEx(key, "size")[0]
|
||||
new_count = count + 1
|
||||
except:
|
||||
# 如果读取失败,使用当前最大的 shelf_number + 1
|
||||
new_count = shelf_number + 1
|
||||
|
||||
winreg.CloseKey(key)
|
||||
|
||||
key = winreg.OpenKeyEx(reg_connection, registry_key_name, 0, winreg.KEY_SET_VALUE)
|
||||
winreg.SetValueEx(key, "size", 0, winreg.REG_DWORD, new_count)
|
||||
print(f"[NexusLauncher] Updated size to: {new_count}")
|
||||
|
||||
print(f"[NexusLauncher] ✓ Library added to registry successfully")
|
||||
|
||||
# 设置为默认库(writableShelf)
|
||||
try:
|
||||
shelf_key = r"SOFTWARE\Adobe\Adobe Substance 3D Painter\Shelf"
|
||||
default_key = winreg.OpenKeyEx(reg_connection, shelf_key, 0, winreg.KEY_SET_VALUE)
|
||||
winreg.SetValueEx(default_key, "writableShelf", 0, winreg.REG_SZ, safe_library_name)
|
||||
winreg.CloseKey(default_key)
|
||||
print(f"[NexusLauncher] ✓ Set as default library (writableShelf)")
|
||||
except Exception as e:
|
||||
print(f"[NexusLauncher] Warning: Could not set as default: {e}")
|
||||
|
||||
winreg.CloseKey(key)
|
||||
else:
|
||||
winreg.CloseKey(key)
|
||||
|
||||
print(f"[NexusLauncher] ========================================")
|
||||
print(f"[NexusLauncher] ✓ Library setup complete")
|
||||
print(f"[NexusLauncher] Note: Restart SP to see the changes")
|
||||
print(f"[NexusLauncher] ========================================")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"[NexusLauncher] Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def setup_project_library(library_name, library_path, set_as_default=True):
|
||||
"""设置项目库
|
||||
|
||||
Args:
|
||||
library_name: 库名称
|
||||
library_path: 库路径
|
||||
set_as_default: 是否设置为默认库
|
||||
|
||||
Returns:
|
||||
bool: 是否成功
|
||||
"""
|
||||
try:
|
||||
# 延迟导入,因为只有在 SP 中才能导入
|
||||
import substance_painter.resource
|
||||
|
||||
# SP 库名称规则:只能包含小写字母、数字、下划线和连字符
|
||||
# 将名称转换为小写并替换无效字符
|
||||
safe_library_name = library_name.lower().replace(' ', '_')
|
||||
# 移除其他无效字符
|
||||
import re
|
||||
safe_library_name = re.sub(r'[^a-z0-9_-]', '', safe_library_name)
|
||||
|
||||
print(f"[NexusLauncher] Setting up library: {library_name}")
|
||||
if safe_library_name != library_name:
|
||||
print(f"[NexusLauncher] Normalized name: {safe_library_name} (SP naming rules)")
|
||||
print(f"[NexusLauncher] Path: {library_path}")
|
||||
|
||||
# 检查路径是否存在
|
||||
if not os.path.exists(library_path):
|
||||
print(f"[NexusLauncher] Error: Library path does not exist: {library_path}")
|
||||
return False
|
||||
|
||||
# 获取所有现有的 Shelf
|
||||
existing_shelves = substance_painter.resource.Shelves.all()
|
||||
print(f"[NexusLauncher] Found {len(existing_shelves)} existing shelves")
|
||||
|
||||
# 检查是否已存在同名或同路径的库
|
||||
shelf_exists = False
|
||||
for shelf in existing_shelves:
|
||||
shelf_name = shelf.name()
|
||||
shelf_path = shelf.path()
|
||||
print(f"[NexusLauncher] Existing shelf: {shelf_name} -> {shelf_path}")
|
||||
|
||||
# 规范化路径进行比较
|
||||
from os.path import normpath
|
||||
normalized_shelf_path = normpath(shelf_path).lower()
|
||||
normalized_library_path = normpath(library_path).lower()
|
||||
|
||||
if shelf_name == safe_library_name or normalized_shelf_path == normalized_library_path:
|
||||
print(f"[NexusLauncher] Library already exists: {shelf_name} at {shelf_path}")
|
||||
shelf_exists = True
|
||||
existing_shelf = shelf
|
||||
break
|
||||
|
||||
if not shelf_exists:
|
||||
# 添加新库
|
||||
print(f"[NexusLauncher] Adding new library...")
|
||||
try:
|
||||
# 探索可用的 API 方法
|
||||
print(f"[NexusLauncher] Available Shelf methods: {[m for m in dir(substance_painter.resource.Shelf) if not m.startswith('_')]}")
|
||||
print(f"[NexusLauncher] Available Shelves methods: {[m for m in dir(substance_painter.resource.Shelves) if not m.startswith('_')]}")
|
||||
|
||||
# 检查是否有项目打开(添加库需要关闭项目)
|
||||
try:
|
||||
import substance_painter.project
|
||||
if substance_painter.project.is_open():
|
||||
print(f"[NexusLauncher] ERROR: A project is currently open!")
|
||||
print(f"[NexusLauncher] According to SP API docs, no project should be open when adding shelves")
|
||||
print(f"[NexusLauncher] Please close the project and try again")
|
||||
print(f"[NexusLauncher] Or add the library manually: Edit -> Settings -> Libraries")
|
||||
return False
|
||||
else:
|
||||
print(f"[NexusLauncher] ✓ No project is open, safe to add shelf")
|
||||
except Exception as e:
|
||||
print(f"[NexusLauncher] Warning: Could not check project status: {e}")
|
||||
|
||||
# 规范化路径为正斜杠格式(SP 可能需要这个)
|
||||
normalized_path = library_path.replace('\\', '/')
|
||||
print(f"[NexusLauncher] Normalized path: {normalized_path}")
|
||||
|
||||
# 使用 Shelves.add 方法(使用规范化的名称)
|
||||
print(f"[NexusLauncher] Calling Shelves.add('{safe_library_name}', '{normalized_path}')")
|
||||
new_shelf = substance_painter.resource.Shelves.add(safe_library_name, normalized_path)
|
||||
print(f"[NexusLauncher] ✓ Library added successfully")
|
||||
|
||||
# 刷新资源
|
||||
substance_painter.resource.Shelves.refresh_all()
|
||||
print(f"[NexusLauncher] ✓ Resources refreshed")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[NexusLauncher] Error adding library: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
else:
|
||||
print(f"[NexusLauncher] Library already configured")
|
||||
|
||||
# 设置为默认库(如果需要)
|
||||
if set_as_default:
|
||||
# 注意:SP API 可能没有直接设置默认库的方法
|
||||
# 这需要通过修改配置文件来实现
|
||||
print(f"[NexusLauncher] Note: Default library setting may require manual configuration")
|
||||
|
||||
print(f"[NexusLauncher] ========================================")
|
||||
print(f"[NexusLauncher] ✓ Library setup complete")
|
||||
print(f"[NexusLauncher] ========================================")
|
||||
|
||||
return True
|
||||
|
||||
except ImportError as e:
|
||||
print(f"[NexusLauncher] Error: Cannot import substance_painter module")
|
||||
print(f"[NexusLauncher] This script must be run inside Substance Painter")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"[NexusLauncher] Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def get_library_info_from_env():
|
||||
"""从环境变量获取库信息
|
||||
|
||||
Returns:
|
||||
tuple: (library_name, library_path) 或 (None, None)
|
||||
"""
|
||||
# NexusLauncher 会设置这些环境变量
|
||||
library_name = os.environ.get('NEXUS_SP_LIBRARY_NAME')
|
||||
library_path = os.environ.get('NEXUS_SP_LIBRARY_PATH')
|
||||
|
||||
if library_name and library_path:
|
||||
return library_name, library_path
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
print(f"[NexusLauncher] ========================================")
|
||||
print(f"[NexusLauncher] Substance Painter Library Setup Plugin")
|
||||
print(f"[NexusLauncher] ========================================")
|
||||
print(f"[NexusLauncher] Plugin loaded from: {__file__}")
|
||||
print(f"[NexusLauncher] Python version: {sys.version}")
|
||||
|
||||
# 检查环境变量
|
||||
print(f"[NexusLauncher] Checking environment variables...")
|
||||
library_name = os.environ.get('NEXUS_SP_LIBRARY_NAME')
|
||||
library_path = os.environ.get('NEXUS_SP_LIBRARY_PATH')
|
||||
print(f"[NexusLauncher] NEXUS_SP_LIBRARY_NAME: {library_name}")
|
||||
print(f"[NexusLauncher] NEXUS_SP_LIBRARY_PATH: {library_path}")
|
||||
|
||||
if not library_name or not library_path:
|
||||
print(f"[NexusLauncher] Error: Library info not found in environment variables")
|
||||
print(f"[NexusLauncher] Please launch SP through NexusLauncher")
|
||||
return False
|
||||
|
||||
# 优先使用注册表方法(更可靠)
|
||||
print(f"[NexusLauncher] Method 1: Using Windows Registry...")
|
||||
success = setup_project_library_via_registry(library_name, library_path)
|
||||
|
||||
if not success:
|
||||
print(f"[NexusLauncher] Method 2: Using Python API...")
|
||||
success = setup_project_library(library_name, library_path, set_as_default=True)
|
||||
|
||||
return success
|
||||
|
||||
|
||||
# SP 插件入口点
|
||||
def start_plugin():
|
||||
"""SP 插件启动入口点"""
|
||||
main()
|
||||
|
||||
def close_plugin():
|
||||
"""SP 插件关闭入口点"""
|
||||
pass
|
||||
|
||||
# 如果作为插件运行
|
||||
if __name__ == "__plugin__":
|
||||
start_plugin()
|
||||
|
||||
# 如果作为脚本运行
|
||||
elif __name__ == "__main__":
|
||||
main()
|
||||
5
requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
customtkinter>=5.2.0
|
||||
Pillow>=10.0.0
|
||||
pystray>=0.19.0
|
||||
pyinstaller>=6.0.0
|
||||
pywin32>=306
|
||||
51
ui/__init__.py
Normal file
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
UI Module
|
||||
---------
|
||||
用户界面相关模块
|
||||
"""
|
||||
|
||||
from .utilities import (
|
||||
MessageDialog,
|
||||
InputDialog,
|
||||
show_info,
|
||||
show_warning,
|
||||
show_error,
|
||||
ask_yes_no,
|
||||
ask_string,
|
||||
BaseDialog,
|
||||
IconManager,
|
||||
get_icon_path,
|
||||
set_window_icon,
|
||||
get_icons_dir,
|
||||
setup_dialog_icon,
|
||||
setup_dialog_window,
|
||||
set_dark_title_bar,
|
||||
darken_color
|
||||
)
|
||||
from .utilities import custom_dialogs
|
||||
from .settings_window import SettingsWindow
|
||||
from .splash_screen import SplashScreen
|
||||
|
||||
__all__ = [
|
||||
'BaseDialog',
|
||||
'MessageDialog',
|
||||
'InputDialog',
|
||||
'show_info',
|
||||
'show_warning',
|
||||
'show_error',
|
||||
'ask_yes_no',
|
||||
'ask_string',
|
||||
'SettingsWindow',
|
||||
'SplashScreen',
|
||||
'IconManager',
|
||||
'get_icon_path',
|
||||
'set_window_icon',
|
||||
'get_icons_dir',
|
||||
'setup_dialog_icon',
|
||||
'setup_dialog_window',
|
||||
'set_dark_title_bar',
|
||||
'darken_color',
|
||||
'custom_dialogs'
|
||||
]
|
||||
10
ui/project/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Project package for NexusLauncher.
|
||||
|
||||
Exports the project management components.
|
||||
"""
|
||||
|
||||
from .project_panel import ProjectPanel
|
||||
|
||||
__all__ = ["ProjectPanel"]
|
||||
533
ui/project/project_panel.py
Normal file
@@ -0,0 +1,533 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Project panel for NexusLauncher.
|
||||
|
||||
This module contains the ProjectPanel class which manages the display
|
||||
and interaction with project applications.
|
||||
"""
|
||||
|
||||
import os
|
||||
import traceback
|
||||
import customtkinter as ctk
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from config.constants import (
|
||||
COLOR_TRANSPARENT, TEXT_COLOR_WHITE, TEXT_COLOR_PRIMARY, TEXT_COLOR_SECONDARY,
|
||||
SCROLLBAR_COLOR, SCROLLBAR_HOVER_COLOR, PRESET_COLORS
|
||||
)
|
||||
from ui.utilities.color_utils import darken_color
|
||||
from ui.utilities import custom_dialogs
|
||||
from ui.utilities.ui_helpers import UIHelpers
|
||||
|
||||
|
||||
class ProjectPanel(ctk.CTkFrame):
|
||||
"""Project panel for displaying and managing project applications."""
|
||||
|
||||
def __init__(self, parent, config_manager, icon_manager, log_callback=None, **kwargs):
|
||||
"""Initialize the project panel.
|
||||
|
||||
Args:
|
||||
parent: Parent widget
|
||||
config_manager: Configuration manager instance
|
||||
icon_manager: Icon manager instance
|
||||
log_callback: Optional callback for logging messages
|
||||
**kwargs: Additional keyword arguments for CTkFrame
|
||||
"""
|
||||
super().__init__(parent, **kwargs)
|
||||
|
||||
self.config_manager = config_manager
|
||||
self.icon_manager = icon_manager
|
||||
self.log_callback = log_callback
|
||||
self.icon_size = 80 # Default icon size
|
||||
self.debug_mode = False # 调试模式,可通过配置控制
|
||||
|
||||
self._setup_ui()
|
||||
self._init_background_color()
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Setup the user interface."""
|
||||
self.grid_columnconfigure(0, weight=1)
|
||||
self.grid_rowconfigure(0, weight=1)
|
||||
|
||||
# Create scrollable frame for apps
|
||||
self.apps_outer_frame = ctk.CTkScrollableFrame(
|
||||
self,
|
||||
corner_radius=10,
|
||||
fg_color=COLOR_TRANSPARENT,
|
||||
scrollbar_fg_color=COLOR_TRANSPARENT,
|
||||
scrollbar_button_color=SCROLLBAR_COLOR,
|
||||
scrollbar_button_hover_color=SCROLLBAR_HOVER_COLOR
|
||||
)
|
||||
self.apps_outer_frame.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
|
||||
|
||||
# Hide scrollbar after creation (with longer delay to ensure it's fully created)
|
||||
self.after(100, lambda: UIHelpers.hide_scrollbar(self.apps_outer_frame))
|
||||
|
||||
# Create inner frame for app buttons
|
||||
self.apps_frame = ctk.CTkFrame(self.apps_outer_frame, fg_color=COLOR_TRANSPARENT)
|
||||
self.apps_frame.pack(fill="both", expand=True, padx=5, pady=5)
|
||||
|
||||
def _init_background_color(self):
|
||||
"""初始化背景色,避免首次显示时的蓝色"""
|
||||
try:
|
||||
# 获取当前项目的颜色
|
||||
current_project = self.config_manager.get_current_project()
|
||||
project_color = self.config_manager.get_project_color(current_project)
|
||||
|
||||
if project_color:
|
||||
# 如果有项目颜色,使用项目颜色
|
||||
self.apps_outer_frame.configure(fg_color=project_color)
|
||||
self._log(f"Initialized background color: {project_color}", "DEBUG")
|
||||
else:
|
||||
# 否则使用透明色
|
||||
self.apps_outer_frame.configure(fg_color=COLOR_TRANSPARENT)
|
||||
self._log("Initialized background color: transparent", "DEBUG")
|
||||
except Exception as e:
|
||||
self._log(f"Failed to initialize background color: {e}", "ERROR")
|
||||
|
||||
def _ensure_ready_then(self, func, delay: int = 100):
|
||||
"""在 apps_frame 就绪后执行回调,否则延迟重试。
|
||||
Args:
|
||||
func: 可调用对象,在就绪后执行
|
||||
delay: 未就绪时再次检查的延迟毫秒
|
||||
"""
|
||||
try:
|
||||
if not hasattr(self, 'apps_frame') or not self.apps_frame.winfo_exists():
|
||||
self._log("Frame not exists, delaying execution", "DEBUG")
|
||||
self.after(delay, lambda: self._ensure_ready_then(func, delay))
|
||||
return
|
||||
frame_width = self.apps_frame.winfo_width()
|
||||
if frame_width <= 1:
|
||||
self._log(f"Frame not ready (width={frame_width}), delaying execution", "DEBUG")
|
||||
self.after(delay, lambda: self._ensure_ready_then(func, delay))
|
||||
return
|
||||
# 就绪,执行回调
|
||||
func()
|
||||
except Exception as e:
|
||||
self._log(f"Ready check failed: {e}", "ERROR")
|
||||
traceback.print_exc()
|
||||
self.after(delay, lambda: self._ensure_ready_then(func, delay))
|
||||
|
||||
def _log(self, message: str, level: str = "INFO"):
|
||||
"""统一的日志方法
|
||||
|
||||
Args:
|
||||
message: 日志消息
|
||||
level: 日志级别 (DEBUG, INFO, WARNING, ERROR)
|
||||
"""
|
||||
# DEBUG级别的日志只在调试模式下输出
|
||||
if level == "DEBUG" and not self.debug_mode:
|
||||
return
|
||||
|
||||
prefix = f"[{level}]"
|
||||
full_message = f"{prefix} {message}"
|
||||
|
||||
if self.log_callback:
|
||||
self.log_callback(full_message)
|
||||
else:
|
||||
print(full_message)
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh the project panel."""
|
||||
# 确保框架已经完全初始化
|
||||
try:
|
||||
# 防重入,避免多次排队刷新
|
||||
if getattr(self, '_refreshing', False):
|
||||
return
|
||||
self._refreshing = True
|
||||
# 使用就绪助手,保持行为一致
|
||||
def _do_load():
|
||||
self._log(f"Frame ready (width={self.apps_frame.winfo_width()}), loading apps", "DEBUG")
|
||||
# 强制更新布局,确保所有pending的更新完成
|
||||
self.update_idletasks()
|
||||
# 延迟加载,确保UI完全稳定
|
||||
self.after(50, self._load_apps)
|
||||
self._ensure_ready_then(_do_load, delay=100)
|
||||
except Exception as e:
|
||||
self._log(f"Refresh failed: {e}", "ERROR")
|
||||
traceback.print_exc()
|
||||
# 如果出错,延迟重试
|
||||
self.after(100, self.refresh)
|
||||
finally:
|
||||
self._refreshing = False
|
||||
|
||||
def _load_apps(self):
|
||||
"""加载并显示应用按钮(带错误处理)"""
|
||||
try:
|
||||
# Clear icon cache to ensure icons are updated
|
||||
self.icon_manager.clear_cache()
|
||||
|
||||
# Clear existing buttons
|
||||
for widget in self.apps_frame.winfo_children():
|
||||
widget.destroy()
|
||||
|
||||
# Get current project's applications
|
||||
apps = self.config_manager.get_apps()
|
||||
|
||||
self._log(f"Loading apps, count: {len(apps) if apps else 0}", "DEBUG")
|
||||
|
||||
if not apps:
|
||||
self._show_empty_state()
|
||||
return
|
||||
|
||||
# Calculate columns per row (adaptive)
|
||||
cols_per_row = self._calculate_columns_per_row()
|
||||
|
||||
# Create application buttons (adaptive layout)
|
||||
for idx, app in enumerate(apps):
|
||||
row = idx // cols_per_row
|
||||
col = idx % cols_per_row
|
||||
self._create_app_button(app, row, col)
|
||||
|
||||
# Hide scrollbar after loading (with longer delay)
|
||||
self.after(100, lambda: UIHelpers.hide_scrollbar(self.apps_outer_frame))
|
||||
|
||||
except Exception as e:
|
||||
self._log(f"Load apps failed: {e}", "ERROR")
|
||||
traceback.print_exc()
|
||||
|
||||
def _show_empty_state(self):
|
||||
"""显示空状态提示"""
|
||||
empty_label = ctk.CTkLabel(
|
||||
self.apps_frame,
|
||||
text="暂无应用\n请点击右上角的设置按钮添加应用",
|
||||
font=ctk.CTkFont(size=16, weight="bold"),
|
||||
text_color=TEXT_COLOR_WHITE,
|
||||
justify="center"
|
||||
)
|
||||
empty_label.pack(expand=True)
|
||||
|
||||
def _calculate_columns_per_row(self) -> int:
|
||||
"""Calculate number of columns per row based on frame width."""
|
||||
self.update_idletasks()
|
||||
self.apps_frame.update_idletasks()
|
||||
|
||||
# Get available width
|
||||
frame_width = self.apps_frame.winfo_width()
|
||||
if frame_width <= 1:
|
||||
# Fallback to estimated width
|
||||
window_width = self.winfo_toplevel().winfo_width() or 800
|
||||
frame_width = window_width - 30 # Subtract padding
|
||||
|
||||
# Calculate columns: (width - safety margin) / (button size + spacing)
|
||||
cols = (frame_width - 10) // (self.icon_size + 20)
|
||||
|
||||
# Ensure at least 1 column, maximum 8 columns
|
||||
return max(1, min(cols, 8))
|
||||
|
||||
def _get_default_button_color(self) -> tuple:
|
||||
"""Get default button color and hover color."""
|
||||
# Use first color from preset colors as default
|
||||
color = PRESET_COLORS[0] # Blue-gray
|
||||
|
||||
# Generate corresponding hover color (slightly darker)
|
||||
hover_color = darken_color(color, 0.2)
|
||||
|
||||
return color, hover_color
|
||||
|
||||
def _create_app_button(self, app: Dict, row: int, col: int):
|
||||
"""Create an application button.
|
||||
|
||||
Args:
|
||||
app: Application dictionary containing name, path, version, etc.
|
||||
row: Grid row position
|
||||
col: Grid column position
|
||||
"""
|
||||
# Button container
|
||||
btn_container = ctk.CTkFrame(self.apps_frame, fg_color=COLOR_TRANSPARENT)
|
||||
btn_container.grid(row=row, column=col, padx=10, pady=10)
|
||||
|
||||
# Get icon and color
|
||||
icon_image = self.icon_manager.get_app_icon(app['path'], self.config_manager)
|
||||
custom_color = self.config_manager.get_app_color(app['path'])
|
||||
fg_color, hover_color = (
|
||||
(custom_color, darken_color(custom_color, 0.2))
|
||||
if custom_color
|
||||
else self._get_default_button_color()
|
||||
)
|
||||
|
||||
# Truncate application name
|
||||
app_name = app['name']
|
||||
max_chars = max(3, self.icon_size // 10)
|
||||
if len(app_name) > max_chars:
|
||||
app_name = app_name[:max_chars-1] + ".."
|
||||
|
||||
# Square button
|
||||
app_button = ctk.CTkButton(
|
||||
btn_container,
|
||||
text=app_name,
|
||||
image=icon_image,
|
||||
command=lambda: self._launch_app(app),
|
||||
width=self.icon_size,
|
||||
height=self.icon_size,
|
||||
corner_radius=15,
|
||||
font=ctk.CTkFont(size=8, weight="bold"),
|
||||
fg_color=fg_color,
|
||||
hover_color=hover_color,
|
||||
compound="top",
|
||||
border_spacing=3,
|
||||
text_color=TEXT_COLOR_PRIMARY
|
||||
)
|
||||
app_button.pack(pady=(0, 3))
|
||||
|
||||
# Version label
|
||||
version_label = ctk.CTkLabel(
|
||||
btn_container,
|
||||
text=f"v{app['version']}",
|
||||
font=ctk.CTkFont(size=10),
|
||||
text_color=TEXT_COLOR_WHITE
|
||||
)
|
||||
version_label.pack()
|
||||
|
||||
def _launch_app(self, app: Dict):
|
||||
"""Launch an application with plugin support.
|
||||
|
||||
Args:
|
||||
app: Application dictionary containing path and name
|
||||
"""
|
||||
app_path = app['path']
|
||||
app_name = app['name']
|
||||
|
||||
try:
|
||||
# Check if path exists
|
||||
if not os.path.exists(app_path):
|
||||
self._log(f"Launch failed: {app_name} - Path does not exist", "ERROR")
|
||||
custom_dialogs.show_error(
|
||||
self,
|
||||
"错误",
|
||||
f"应用路径不存在:\n{app_path}\n\n请检查配置是否正确。"
|
||||
)
|
||||
return
|
||||
|
||||
# Try to launch with plugins
|
||||
current_project = self.config_manager.get_current_project()
|
||||
plugin_config = self.config_manager.get_task_settings(current_project)
|
||||
|
||||
# Import plugin launcher
|
||||
from plugins import PluginLauncher
|
||||
|
||||
# Try plugin launch
|
||||
plugin_launched = False
|
||||
if plugin_config:
|
||||
self._log(f"Attempting to launch {app_name} with plugins...", "DEBUG")
|
||||
plugin_launched = PluginLauncher.launch_with_plugins(
|
||||
app_name,
|
||||
app_path,
|
||||
plugin_config,
|
||||
current_project # 传递项目名称
|
||||
)
|
||||
|
||||
# Fallback to normal launch if plugin launch failed or not configured
|
||||
if not plugin_launched:
|
||||
self._log(f"Launching {app_name} normally (no plugins)", "INFO")
|
||||
if os.path.isfile(app_path):
|
||||
os.startfile(app_path)
|
||||
else:
|
||||
os.startfile(app_path)
|
||||
|
||||
self._log(f"Launched application: {app_name}", "INFO")
|
||||
|
||||
except PermissionError:
|
||||
self._log(f"Launch failed: {app_name} - Permission denied", "ERROR")
|
||||
custom_dialogs.show_error(
|
||||
self,
|
||||
"权限错误",
|
||||
f"没有权限启动应用 '{app_name}'\n请以管理员身份运行或检查文件权限。"
|
||||
)
|
||||
except OSError as e:
|
||||
self._log(f"Launch failed: {app_name} - OS error: {e}", "ERROR")
|
||||
custom_dialogs.show_error(
|
||||
self,
|
||||
"系统错误",
|
||||
f"无法启动应用 '{app_name}':\n{str(e)}"
|
||||
)
|
||||
except Exception as e:
|
||||
self._log(f"Launch failed: {app_name} - {str(e)}", "ERROR")
|
||||
custom_dialogs.show_error(
|
||||
self,
|
||||
"启动失败",
|
||||
f"无法启动应用 '{app_name}'\n请检查应用路径是否正确。"
|
||||
)
|
||||
|
||||
def set_icon_size(self, size: int):
|
||||
"""Set the icon size and refresh the display.
|
||||
|
||||
Args:
|
||||
size: New icon size in pixels
|
||||
"""
|
||||
self.icon_size = size
|
||||
self._load_apps()
|
||||
|
||||
def update_background_color(self, color: str):
|
||||
"""Update the background color of the panel and its components.
|
||||
|
||||
Args:
|
||||
color: Background color in hex format
|
||||
"""
|
||||
try:
|
||||
# Update the outer scrollable frame background
|
||||
if hasattr(self, 'apps_outer_frame'):
|
||||
self.apps_outer_frame.configure(fg_color=color)
|
||||
|
||||
self._log(f"Updated background color to: {color}", "DEBUG")
|
||||
except Exception as e:
|
||||
self._log(f"Failed to update background color: {e}", "ERROR")
|
||||
|
||||
def on_window_resize(self):
|
||||
"""Handle window resize events."""
|
||||
# Check if we need to recalculate layout
|
||||
try:
|
||||
if not hasattr(self, 'apps_frame'):
|
||||
return
|
||||
|
||||
# Clear existing buttons
|
||||
if hasattr(self, 'apps_frame'):
|
||||
for widget in self.apps_frame.winfo_children():
|
||||
widget.destroy()
|
||||
|
||||
# Calculate new column count
|
||||
cols_per_row = self._calculate_columns_per_row()
|
||||
|
||||
# Recreate buttons with new layout
|
||||
apps = self.config_manager.get_apps()
|
||||
for idx, app in enumerate(apps):
|
||||
row = idx // cols_per_row
|
||||
col = idx % cols_per_row
|
||||
self._create_app_button(app, row, col)
|
||||
|
||||
except Exception as e:
|
||||
self._log(f"Error during resize: {e}", "WARNING")
|
||||
traceback.print_exc()
|
||||
|
||||
def handle_zoom(self, event, min_icon_size=50, max_icon_size=150):
|
||||
"""处理 Ctrl + 鼠标滚轮缩放事件
|
||||
|
||||
Args:
|
||||
event: 鼠标滚轮事件
|
||||
min_icon_size: 最小图标大小
|
||||
max_icon_size: 最大图标大小
|
||||
|
||||
Returns:
|
||||
str: "break" 阻止事件继续传播
|
||||
"""
|
||||
try:
|
||||
self._log(f"Zoom event triggered - delta: {event.delta}, current size: {self.icon_size}", "DEBUG")
|
||||
|
||||
# 获取滚轮方向(正数为向上,负数为向下)
|
||||
delta = event.delta
|
||||
|
||||
# 计算新的图标大小
|
||||
if delta > 0:
|
||||
# 放大
|
||||
new_size = min(self.icon_size + 10, max_icon_size)
|
||||
else:
|
||||
# 缩小
|
||||
new_size = max(self.icon_size - 10, min_icon_size)
|
||||
|
||||
self._log(f"New size: {new_size}", "DEBUG")
|
||||
|
||||
# 如果大小有变化,则更新
|
||||
if new_size != self.icon_size:
|
||||
self.icon_size = new_size
|
||||
# 清空图标缓存,强制重新加载新尺寸的图标
|
||||
self.icon_manager.clear_cache()
|
||||
# 使用防抖动机制,避免频繁刷新
|
||||
if hasattr(self, '_zoom_timer'):
|
||||
self.after_cancel(self._zoom_timer)
|
||||
# 延迟50ms执行,如果连续滚动则只执行最后一次
|
||||
self._zoom_timer = self.after(50, lambda: self._apply_zoom(new_size))
|
||||
self._log("Zoom triggered", "DEBUG")
|
||||
else:
|
||||
self._log("Size unchanged, limit reached", "DEBUG")
|
||||
|
||||
# 阻止事件继续传播,避免触发滚动
|
||||
return "break"
|
||||
except Exception as e:
|
||||
self._log(f"Zoom error: {e}", "ERROR")
|
||||
return "break"
|
||||
|
||||
def _apply_zoom(self, new_size):
|
||||
"""应用缩放(防抖动后执行)
|
||||
|
||||
Args:
|
||||
new_size: 新的图标大小
|
||||
"""
|
||||
try:
|
||||
# 保存到配置
|
||||
self.config_manager.save_icon_size(new_size)
|
||||
# 更新图标管理器的图标大小
|
||||
self.icon_manager.update_icon_size(new_size)
|
||||
# 重新加载应用按钮
|
||||
self._load_apps()
|
||||
except Exception as e:
|
||||
self._log(f"Scaling application failed: {e}", "ERROR")
|
||||
|
||||
def update_tab_icon(self, tabview, project_name):
|
||||
"""更新Project标签页的图标
|
||||
|
||||
Args:
|
||||
tabview: 标签页视图对象
|
||||
project_name: 项目名称
|
||||
"""
|
||||
try:
|
||||
from PIL import Image
|
||||
import customtkinter as ctk
|
||||
|
||||
# 获取项目图标路径
|
||||
project_icon_path = self.config_manager.get_project_icon(project_name)
|
||||
|
||||
# 获取标签页按钮
|
||||
project_button = tabview._segmented_button._buttons_dict.get("Project")
|
||||
task_button = tabview._segmented_button._buttons_dict.get("Task")
|
||||
|
||||
# 确保两个按钮高度一致
|
||||
if project_button:
|
||||
project_button.configure(height=40)
|
||||
if task_button:
|
||||
task_button.configure(height=40)
|
||||
|
||||
if project_button:
|
||||
self._log(f"Project icon path: {project_icon_path}", "DEBUG")
|
||||
self._log(f"File exists: {os.path.exists(project_icon_path) if project_icon_path else False}", "DEBUG")
|
||||
|
||||
if project_icon_path and os.path.exists(project_icon_path):
|
||||
# 加载项目图标
|
||||
try:
|
||||
icon_image = Image.open(project_icon_path)
|
||||
icon_image = icon_image.resize((20, 20), Image.Resampling.LANCZOS)
|
||||
ctk_icon = ctk.CTkImage(light_image=icon_image, dark_image=icon_image, size=(20, 20))
|
||||
project_button.configure(image=ctk_icon, compound="left", text="Project")
|
||||
self._log(f"Successfully loaded project icon: {project_icon_path}", "DEBUG")
|
||||
except Exception as e:
|
||||
self._log(f"Failed to load project icon: {e}", "WARNING")
|
||||
# 加载失败时使用空的CTkImage避免引用错误
|
||||
empty_icon = self._create_empty_icon()
|
||||
project_button.configure(image=empty_icon, compound="left", text="Project")
|
||||
self._log("Set empty icon due to load failure", "DEBUG")
|
||||
else:
|
||||
# 如果没有图标,使用空的CTkImage避免引用错误
|
||||
self._log("No icon path or file doesn't exist, creating empty icon", "DEBUG")
|
||||
empty_icon = self._create_empty_icon()
|
||||
project_button.configure(image=empty_icon, compound="left", text="Project")
|
||||
self._log("Set empty icon for project", "DEBUG")
|
||||
|
||||
# 强制更新按钮显示
|
||||
project_button.update_idletasks()
|
||||
if task_button:
|
||||
task_button.update_idletasks()
|
||||
|
||||
except Exception as e:
|
||||
self._log(f"Failed to update tab icon: {e}", "ERROR")
|
||||
traceback.print_exc()
|
||||
|
||||
def _create_empty_icon(self):
|
||||
"""创建空的CTkImage,避免图像引用错误
|
||||
|
||||
Returns:
|
||||
空的CTkImage对象
|
||||
"""
|
||||
from PIL import Image
|
||||
import customtkinter as ctk
|
||||
|
||||
empty_image = Image.new('RGBA', (1, 1), (0, 0, 0, 0))
|
||||
return ctk.CTkImage(light_image=empty_image, dark_image=empty_image, size=(1, 1))
|
||||
2121
ui/settings_window.py
Normal file
138
ui/splash_screen.py
Normal file
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
启动画面 - 使用customtkinter实现,带淡入淡出动画
|
||||
"""
|
||||
import customtkinter as ctk
|
||||
|
||||
|
||||
class SplashScreen(ctk.CTkToplevel):
|
||||
"""启动画面 - 扁平化设计,带动画特效"""
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
|
||||
# 无边框窗口
|
||||
self.overrideredirect(True)
|
||||
|
||||
# 窗口大小和位置(更扁平)
|
||||
width = 500
|
||||
height = 160
|
||||
screen_width = self.winfo_screenwidth()
|
||||
screen_height = self.winfo_screenheight()
|
||||
x = (screen_width - width) // 2
|
||||
y = (screen_height - height) // 2
|
||||
self.geometry(f"{width}x{height}+{x}+{y}")
|
||||
|
||||
# 背景色 - 深色
|
||||
bg_color = "#0d1117"
|
||||
border_color = "#58a6ff"
|
||||
|
||||
# 设置窗口背景为边框颜色,创造边框效果
|
||||
self.configure(fg_color=border_color)
|
||||
|
||||
# 主框架(带圆角,通过padding创造边框效果)
|
||||
main_frame = ctk.CTkFrame(
|
||||
self,
|
||||
fg_color=bg_color,
|
||||
corner_radius=10
|
||||
)
|
||||
# 增大padding,让圆角更明显
|
||||
main_frame.pack(expand=True, fill="both", padx=2.5, pady=2.5)
|
||||
|
||||
# 内容容器
|
||||
content_frame = ctk.CTkFrame(main_frame, fg_color="transparent")
|
||||
content_frame.pack(expand=True, fill="both", padx=20, pady=15)
|
||||
|
||||
# 标题 - 使用更酷的字体和渐变色效果
|
||||
title_frame = ctk.CTkFrame(content_frame, fg_color="transparent")
|
||||
title_frame.pack(pady=(10, 5))
|
||||
|
||||
self.title_label = ctk.CTkLabel(
|
||||
title_frame,
|
||||
text="⚡ NEXUS LAUNCHER",
|
||||
font=ctk.CTkFont(size=32, weight="bold", family="Consolas"),
|
||||
text_color="#58a6ff"
|
||||
)
|
||||
self.title_label.pack()
|
||||
|
||||
# 副标题 - 科技感
|
||||
self.subtitle_label = ctk.CTkLabel(
|
||||
content_frame,
|
||||
text="[ INITIALIZING SYSTEM ]",
|
||||
font=ctk.CTkFont(size=11, family="Consolas"),
|
||||
text_color="#8b949e"
|
||||
)
|
||||
self.subtitle_label.pack(pady=(0, 15))
|
||||
|
||||
# 进度条容器
|
||||
progress_container = ctk.CTkFrame(content_frame, fg_color="transparent")
|
||||
progress_container.pack(fill="x", pady=(0, 10))
|
||||
|
||||
# 进度条 - 更粗,带发光效果
|
||||
self.progress = ctk.CTkProgressBar(
|
||||
progress_container,
|
||||
width=440,
|
||||
height=12,
|
||||
mode='indeterminate',
|
||||
progress_color="#58a6ff",
|
||||
fg_color="#161b22",
|
||||
border_width=0,
|
||||
corner_radius=6
|
||||
)
|
||||
self.progress.pack()
|
||||
self.progress.start()
|
||||
|
||||
# 状态文本 - 动画效果
|
||||
self.status_label = ctk.CTkLabel(
|
||||
content_frame,
|
||||
text="Loading modules...",
|
||||
font=ctk.CTkFont(size=9, family="Consolas"),
|
||||
text_color="#6e7681"
|
||||
)
|
||||
self.status_label.pack()
|
||||
|
||||
# 置顶
|
||||
self.attributes('-topmost', True)
|
||||
|
||||
# 初始透明度动画
|
||||
self.alpha = 0.0
|
||||
self.attributes('-alpha', self.alpha)
|
||||
self._fade_in()
|
||||
|
||||
self.update()
|
||||
|
||||
def _fade_in(self):
|
||||
"""淡入动画"""
|
||||
if self.alpha < 1.0:
|
||||
self.alpha += 0.1
|
||||
self.attributes('-alpha', self.alpha)
|
||||
self.after(20, self._fade_in)
|
||||
|
||||
def _fade_out(self, callback):
|
||||
"""淡出动画"""
|
||||
if self.alpha > 0.0:
|
||||
self.alpha -= 0.15
|
||||
self.attributes('-alpha', self.alpha)
|
||||
self.after(20, lambda: self._fade_out(callback))
|
||||
else:
|
||||
callback()
|
||||
|
||||
def update_status(self, message):
|
||||
"""更新状态信息"""
|
||||
try:
|
||||
self.status_label.configure(text=message)
|
||||
self.update()
|
||||
except:
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
"""关闭启动画面(带淡出动画)"""
|
||||
try:
|
||||
self.progress.stop()
|
||||
self._fade_out(self.destroy)
|
||||
except:
|
||||
try:
|
||||
self.destroy()
|
||||
except:
|
||||
pass
|
||||
12
ui/task/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Task package for NexusLauncher.
|
||||
|
||||
Exports the core task-related classes.
|
||||
"""
|
||||
|
||||
from .node import Node
|
||||
from .subfolder_editor import NodeEditor
|
||||
from .task_panel import TaskPanel
|
||||
|
||||
__all__ = ["Node", "NodeEditor", "TaskPanel"]
|
||||
138
ui/task/node.py
Normal file
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Node Module
|
||||
-----------
|
||||
Defines the Node class for representing folder nodes in the structure.
|
||||
"""
|
||||
import uuid
|
||||
from typing import List, Optional, Dict, Any
|
||||
|
||||
|
||||
class Node:
|
||||
"""Represents a folder node in the structure with parent-child relationships.
|
||||
|
||||
Attributes:
|
||||
id (str): Unique identifier for the node
|
||||
name (str): Display name of the node
|
||||
parent_id (Optional[str]): ID of the parent node, None for root nodes
|
||||
children (List[Node]): List of child nodes
|
||||
x (float): X-coordinate for display
|
||||
y (float): Y-coordinate for display
|
||||
width (float): Width of the node in pixels
|
||||
height (float): Height of the node in pixels
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, node_id: Optional[str] = None, parent_id: Optional[str] = None):
|
||||
"""Initialize a new node.
|
||||
|
||||
Args:
|
||||
name: The display name of the node
|
||||
node_id: Optional unique identifier. If not provided, a UUID will be generated.
|
||||
parent_id: Optional ID of the parent node.
|
||||
"""
|
||||
self.id = node_id or str(uuid.uuid4())
|
||||
self.name = name
|
||||
self.parent_id = parent_id
|
||||
self.children: List['Node'] = []
|
||||
self.x = 0.0
|
||||
self.y = 0.0
|
||||
self.width = 120.0
|
||||
self.height = 60.0
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert the node and its children to a dictionary.
|
||||
|
||||
Returns:
|
||||
A dictionary representation of the node and its children.
|
||||
"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'parent_id': self.parent_id,
|
||||
'x': self.x,
|
||||
'y': self.y,
|
||||
'width': self.width,
|
||||
'height': self.height,
|
||||
'children': [child.to_dict() for child in self.children]
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'Node':
|
||||
"""Create a Node instance from a dictionary.
|
||||
|
||||
Args:
|
||||
data: Dictionary containing node data
|
||||
|
||||
Returns:
|
||||
A new Node instance with the given data
|
||||
"""
|
||||
node = cls(
|
||||
name=data['name'],
|
||||
node_id=data['id'],
|
||||
parent_id=data.get('parent_id')
|
||||
)
|
||||
node.x = data.get('x', 0)
|
||||
node.y = data.get('y', 0)
|
||||
node.width = data.get('width', 120)
|
||||
node.height = data.get('height', 60)
|
||||
|
||||
# Recursively create child nodes
|
||||
for child_data in data.get('children', []):
|
||||
child_node = cls.from_dict(child_data)
|
||||
child_node.parent_id = node.id
|
||||
node.children.append(child_node)
|
||||
|
||||
return node
|
||||
|
||||
def add_child(self, name: str) -> 'Node':
|
||||
"""Add a new child node.
|
||||
|
||||
Args:
|
||||
name: Name for the new child node
|
||||
|
||||
Returns:
|
||||
The newly created child node
|
||||
"""
|
||||
child = Node(name, parent_id=self.id)
|
||||
self.children.append(child)
|
||||
return child
|
||||
|
||||
def remove_child(self, node_id: str) -> bool:
|
||||
"""Remove a child node by ID.
|
||||
|
||||
Args:
|
||||
node_id: ID of the node to remove
|
||||
|
||||
Returns:
|
||||
True if the node was found and removed, False otherwise
|
||||
"""
|
||||
for i, child in enumerate(self.children):
|
||||
if child.id == node_id:
|
||||
self.children.pop(i)
|
||||
return True
|
||||
if child.remove_child(node_id):
|
||||
return True
|
||||
return False
|
||||
|
||||
def find_node(self, node_id: str) -> Optional['Node']:
|
||||
"""Find a node by ID in the subtree.
|
||||
|
||||
Args:
|
||||
node_id: ID of the node to find
|
||||
|
||||
Returns:
|
||||
The found node, or None if not found
|
||||
"""
|
||||
if self.id == node_id:
|
||||
return self
|
||||
|
||||
for child in self.children:
|
||||
found = child.find_node(node_id)
|
||||
if found:
|
||||
return found
|
||||
|
||||
return None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Node id='{self.id}' name='{self.name}'>"
|
||||
1695
ui/task/subfolder_editor.py
Normal file
2094
ui/task/task_panel.py
Normal file
82
ui/utilities/__init__.py
Normal file
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
UI Utilities Package
|
||||
-------------------
|
||||
UI工具包,包含对话框、图标管理、工具函数等
|
||||
"""
|
||||
|
||||
from .custom_dialogs import (
|
||||
MessageDialog,
|
||||
InputDialog,
|
||||
show_info,
|
||||
show_warning,
|
||||
show_error,
|
||||
ask_yes_no,
|
||||
ask_string
|
||||
)
|
||||
|
||||
from .base_dialog import BaseDialog
|
||||
|
||||
# 图标工具
|
||||
from .icon_utils import (
|
||||
get_icons_dir,
|
||||
get_icon_path,
|
||||
set_window_icon,
|
||||
setup_dialog_icon
|
||||
)
|
||||
|
||||
# 窗口工具
|
||||
from .window_utils import (
|
||||
set_dark_title_bar,
|
||||
setup_dialog_window
|
||||
)
|
||||
|
||||
# 颜色工具
|
||||
from .color_utils import (
|
||||
darken_color,
|
||||
lighten_color,
|
||||
hex_to_rgb,
|
||||
rgb_to_hex
|
||||
)
|
||||
|
||||
# 向后兼容已迁移到具体模块,不再需要utils.py
|
||||
|
||||
from .icon_manager import IconManager
|
||||
from .window_manager import WindowManager
|
||||
from .ui_helpers import UIHelpers
|
||||
|
||||
__all__ = [
|
||||
# 对话框类
|
||||
'MessageDialog',
|
||||
'InputDialog',
|
||||
'BaseDialog',
|
||||
|
||||
# 对话框便捷函数
|
||||
'show_info',
|
||||
'show_warning',
|
||||
'show_error',
|
||||
'ask_yes_no',
|
||||
'ask_string',
|
||||
|
||||
# 图标工具函数
|
||||
'get_icons_dir',
|
||||
'get_icon_path',
|
||||
'set_window_icon',
|
||||
'setup_dialog_icon',
|
||||
|
||||
# 窗口工具函数
|
||||
'set_dark_title_bar',
|
||||
'setup_dialog_window',
|
||||
|
||||
# 颜色工具函数
|
||||
'darken_color',
|
||||
'lighten_color',
|
||||
'hex_to_rgb',
|
||||
'rgb_to_hex',
|
||||
|
||||
# 管理器类
|
||||
'IconManager',
|
||||
'WindowManager',
|
||||
'UIHelpers'
|
||||
]
|
||||
67
ui/utilities/base_dialog.py
Normal file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
对话框基类
|
||||
"""
|
||||
import tkinter as tk
|
||||
from .icon_utils import get_icon_path
|
||||
from config.constants import BG_COLOR_DARK
|
||||
import os
|
||||
|
||||
|
||||
class BaseDialog(tk.Toplevel):
|
||||
"""对话框基类,提供通用的初始化和居中功能"""
|
||||
|
||||
def __init__(self, parent, title: str, width: int, height: int):
|
||||
"""
|
||||
初始化对话框
|
||||
|
||||
Args:
|
||||
parent: 父窗口
|
||||
title: 窗口标题
|
||||
width: 窗口宽度
|
||||
height: 窗口高度
|
||||
"""
|
||||
super().__init__(parent)
|
||||
|
||||
# 先隐藏窗口
|
||||
self.withdraw()
|
||||
|
||||
# 设置窗口属性
|
||||
self.title(title)
|
||||
self.geometry(f"{width}x{height}")
|
||||
self.resizable(False, False)
|
||||
|
||||
# 设置图标
|
||||
icon_path = get_icon_path()
|
||||
if os.path.exists(icon_path):
|
||||
self.iconbitmap(icon_path)
|
||||
|
||||
# 设置深色主题背景
|
||||
self.configure(bg=BG_COLOR_DARK)
|
||||
|
||||
# 设置为模态窗口
|
||||
self.transient(parent)
|
||||
|
||||
# 居中显示
|
||||
self._center_window()
|
||||
|
||||
def _center_window(self):
|
||||
"""将窗口居中显示"""
|
||||
self.update_idletasks()
|
||||
|
||||
screen_width = self.winfo_screenwidth()
|
||||
screen_height = self.winfo_screenheight()
|
||||
window_width = self.winfo_width()
|
||||
window_height = self.winfo_height()
|
||||
|
||||
x = (screen_width - window_width) // 2
|
||||
y = (screen_height - window_height) // 2
|
||||
|
||||
self.geometry(f"+{x}+{y}")
|
||||
|
||||
def show(self):
|
||||
"""显示对话框"""
|
||||
self.deiconify()
|
||||
self.grab_set()
|
||||
self.wait_window()
|
||||
87
ui/utilities/color_utils.py
Normal file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
颜色工具模块
|
||||
负责颜色处理和转换
|
||||
"""
|
||||
|
||||
|
||||
def darken_color(hex_color: str, factor: float = 0.2) -> str:
|
||||
"""使颜色变暗
|
||||
|
||||
Args:
|
||||
hex_color: 十六进制颜色代码
|
||||
factor: 变暗系数,0-1 之间
|
||||
|
||||
Returns:
|
||||
变暗后的十六进制颜色代码
|
||||
"""
|
||||
# 去除#前缀
|
||||
hex_color = hex_color.lstrip('#')
|
||||
|
||||
# 转换为RGB
|
||||
r = int(hex_color[0:2], 16)
|
||||
g = int(hex_color[2:4], 16)
|
||||
b = int(hex_color[4:6], 16)
|
||||
|
||||
# 降低亮度
|
||||
r = max(0, int(r * (1 - factor)))
|
||||
g = max(0, int(g * (1 - factor)))
|
||||
b = max(0, int(b * (1 - factor)))
|
||||
|
||||
# 转回十六进制
|
||||
return f'#{r:02x}{g:02x}{b:02x}'
|
||||
|
||||
|
||||
def lighten_color(hex_color: str, factor: float = 0.2) -> str:
|
||||
"""使颜色变亮
|
||||
|
||||
Args:
|
||||
hex_color: 十六进制颜色代码
|
||||
factor: 变亮系数,0-1 之间
|
||||
|
||||
Returns:
|
||||
变亮后的十六进制颜色代码
|
||||
"""
|
||||
# 去除#前缀
|
||||
hex_color = hex_color.lstrip('#')
|
||||
|
||||
# 转换为RGB
|
||||
r = int(hex_color[0:2], 16)
|
||||
g = int(hex_color[2:4], 16)
|
||||
b = int(hex_color[4:6], 16)
|
||||
|
||||
# 提高亮度
|
||||
r = min(255, int(r + (255 - r) * factor))
|
||||
g = min(255, int(g + (255 - g) * factor))
|
||||
b = min(255, int(b + (255 - b) * factor))
|
||||
|
||||
# 转回十六进制
|
||||
return f'#{r:02x}{g:02x}{b:02x}'
|
||||
|
||||
|
||||
def hex_to_rgb(hex_color: str) -> tuple:
|
||||
"""将十六进制颜色转换为RGB元组
|
||||
|
||||
Args:
|
||||
hex_color: 十六进制颜色代码
|
||||
|
||||
Returns:
|
||||
RGB颜色元组 (r, g, b)
|
||||
"""
|
||||
hex_color = hex_color.lstrip('#')
|
||||
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
||||
|
||||
|
||||
def rgb_to_hex(r: int, g: int, b: int) -> str:
|
||||
"""将RGB颜色转换为十六进制
|
||||
|
||||
Args:
|
||||
r: 红色分量 (0-255)
|
||||
g: 绿色分量 (0-255)
|
||||
b: 蓝色分量 (0-255)
|
||||
|
||||
Returns:
|
||||
十六进制颜色代码
|
||||
"""
|
||||
return f'#{r:02x}{g:02x}{b:02x}'
|
||||
296
ui/utilities/custom_dialogs.py
Normal file
@@ -0,0 +1,296 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
自定义对话框
|
||||
提供与应用主题统一的消息对话框
|
||||
"""
|
||||
import customtkinter as ctk
|
||||
from typing import Optional
|
||||
from .base_dialog import BaseDialog
|
||||
from config.constants import (
|
||||
BG_COLOR_DARK,
|
||||
BUTTON_WIDTH_MEDIUM,
|
||||
BUTTON_HEIGHT_SMALL,
|
||||
FONT_SIZE_TINY,
|
||||
FONT_SIZE_MEDIUM,
|
||||
FONT_SIZE_LARGE
|
||||
)
|
||||
|
||||
|
||||
class MessageDialog(BaseDialog):
|
||||
"""自定义消息对话框"""
|
||||
|
||||
def __init__(self, parent, title: str, message: str, dialog_type: str = "info", **kwargs):
|
||||
"""
|
||||
初始化消息对话框
|
||||
|
||||
Args:
|
||||
parent: 父窗口
|
||||
title: 对话框标题
|
||||
message: 消息内容
|
||||
dialog_type: 对话框类型 (info, warning, error, question)
|
||||
"""
|
||||
super().__init__(parent, title, 400, 200)
|
||||
|
||||
self.result = None
|
||||
self.dialog_type = dialog_type
|
||||
|
||||
# 创建界面
|
||||
self._create_widgets(message)
|
||||
|
||||
# 显示对话框
|
||||
self.show()
|
||||
|
||||
def _create_widgets(self, message: str):
|
||||
"""创建界面组件"""
|
||||
# 主容器
|
||||
main_frame = ctk.CTkFrame(self, fg_color=BG_COLOR_DARK)
|
||||
main_frame.pack(fill="both", expand=True, padx=20, pady=20)
|
||||
|
||||
# 图标和消息容器
|
||||
content_frame = ctk.CTkFrame(main_frame, fg_color="transparent")
|
||||
content_frame.pack(fill="both", expand=True)
|
||||
|
||||
# 图标
|
||||
icon_text = self._get_icon_text()
|
||||
icon_color = self._get_icon_color()
|
||||
|
||||
icon_label = ctk.CTkLabel(
|
||||
content_frame,
|
||||
text=icon_text,
|
||||
font=ctk.CTkFont(size=40),
|
||||
text_color=icon_color,
|
||||
width=60
|
||||
)
|
||||
icon_label.pack(side="left", padx=(0, 15))
|
||||
|
||||
# 消息文本
|
||||
message_label = ctk.CTkLabel(
|
||||
content_frame,
|
||||
text=message,
|
||||
font=ctk.CTkFont(size=FONT_SIZE_LARGE),
|
||||
wraplength=280,
|
||||
justify="left"
|
||||
)
|
||||
message_label.pack(side="left", fill="both", expand=True)
|
||||
|
||||
# 按钮容器
|
||||
button_frame = ctk.CTkFrame(main_frame, fg_color="transparent")
|
||||
button_frame.pack(side="bottom", pady=(20, 0))
|
||||
|
||||
# 根据对话框类型创建按钮
|
||||
if self.dialog_type == "question":
|
||||
# 是/否按钮
|
||||
no_btn = ctk.CTkButton(
|
||||
button_frame,
|
||||
text="否",
|
||||
command=self._on_no,
|
||||
width=BUTTON_WIDTH_MEDIUM,
|
||||
height=BUTTON_HEIGHT_SMALL,
|
||||
font=ctk.CTkFont(size=FONT_SIZE_MEDIUM),
|
||||
fg_color="#666666",
|
||||
hover_color="#555555"
|
||||
)
|
||||
no_btn.pack(side="left", padx=5)
|
||||
|
||||
yes_btn = ctk.CTkButton(
|
||||
button_frame,
|
||||
text="是",
|
||||
command=self._on_yes,
|
||||
width=BUTTON_WIDTH_MEDIUM,
|
||||
height=BUTTON_HEIGHT_SMALL,
|
||||
font=ctk.CTkFont(size=FONT_SIZE_MEDIUM)
|
||||
)
|
||||
yes_btn.pack(side="left", padx=5)
|
||||
yes_btn.focus_set()
|
||||
else:
|
||||
# 确定按钮
|
||||
ok_btn = ctk.CTkButton(
|
||||
button_frame,
|
||||
text="确定",
|
||||
command=self._on_ok,
|
||||
width=BUTTON_WIDTH_MEDIUM,
|
||||
height=BUTTON_HEIGHT_SMALL,
|
||||
font=ctk.CTkFont(size=FONT_SIZE_MEDIUM)
|
||||
)
|
||||
ok_btn.pack()
|
||||
ok_btn.focus_set()
|
||||
|
||||
# 绑定 Enter 键
|
||||
self.bind("<Return>", lambda e: self._on_ok() if self.dialog_type != "question" else self._on_yes())
|
||||
self.bind("<Escape>", lambda e: self._on_no() if self.dialog_type == "question" else self._on_ok())
|
||||
|
||||
def _get_icon_text(self) -> str:
|
||||
"""获取图标文本"""
|
||||
icons = {
|
||||
"info": "i",
|
||||
"warning": "!",
|
||||
"error": "X",
|
||||
"question": "?"
|
||||
}
|
||||
return icons.get(self.dialog_type, "i")
|
||||
|
||||
def _get_icon_color(self) -> str:
|
||||
"""获取图标颜色"""
|
||||
colors = {
|
||||
"info": "#3584e4",
|
||||
"warning": "#ff9800",
|
||||
"error": "#f44336",
|
||||
"question": "#3584e4"
|
||||
}
|
||||
return colors.get(self.dialog_type, "#3584e4")
|
||||
|
||||
def destroy(self):
|
||||
"""销毁对话框前解除事件绑定"""
|
||||
try:
|
||||
self.unbind("<Return>")
|
||||
self.unbind("<Escape>")
|
||||
except Exception:
|
||||
pass
|
||||
return super().destroy()
|
||||
|
||||
def _on_ok(self):
|
||||
"""确定按钮点击"""
|
||||
self.result = True
|
||||
self.destroy()
|
||||
|
||||
def _on_yes(self):
|
||||
"""是按钮点击"""
|
||||
self.result = True
|
||||
self.destroy()
|
||||
|
||||
def _on_no(self):
|
||||
"""否按钮点击"""
|
||||
self.result = False
|
||||
self.destroy()
|
||||
|
||||
|
||||
class InputDialog(BaseDialog):
|
||||
"""自定义输入对话框"""
|
||||
|
||||
def __init__(self, parent, title: str, prompt: str, initial_value: str = "", **kwargs):
|
||||
"""
|
||||
初始化输入对话框
|
||||
|
||||
Args:
|
||||
parent: 父窗口
|
||||
title: 对话框标题
|
||||
prompt: 提示文本
|
||||
initial_value: 初始值
|
||||
"""
|
||||
super().__init__(parent, title, 400, 180)
|
||||
|
||||
self.result = None
|
||||
|
||||
# 创建界面
|
||||
self._create_widgets(prompt, initial_value)
|
||||
|
||||
# 显示对话框
|
||||
self.show()
|
||||
|
||||
def _create_widgets(self, prompt: str, initial_value: str):
|
||||
"""创建界面组件"""
|
||||
# 主容器
|
||||
main_frame = ctk.CTkFrame(self, fg_color="transparent")
|
||||
main_frame.pack(fill="both", expand=True, padx=20, pady=20)
|
||||
|
||||
# 提示文本
|
||||
prompt_label = ctk.CTkLabel(
|
||||
main_frame,
|
||||
text=prompt,
|
||||
font=ctk.CTkFont(size=FONT_SIZE_LARGE),
|
||||
wraplength=360,
|
||||
justify="left"
|
||||
)
|
||||
prompt_label.pack(pady=(0, 15))
|
||||
|
||||
# 输入框
|
||||
self.entry = ctk.CTkEntry(
|
||||
main_frame,
|
||||
height=BUTTON_HEIGHT_SMALL,
|
||||
font=ctk.CTkFont(size=FONT_SIZE_MEDIUM)
|
||||
)
|
||||
self.entry.pack(fill="x", pady=(0, 20))
|
||||
self.entry.insert(0, initial_value)
|
||||
self.entry.select_range(0, "end")
|
||||
self.entry.focus_set()
|
||||
|
||||
# 按钮容器
|
||||
button_frame = ctk.CTkFrame(main_frame, fg_color="transparent")
|
||||
button_frame.pack(side="bottom")
|
||||
|
||||
# 取消按钮
|
||||
cancel_btn = ctk.CTkButton(
|
||||
button_frame,
|
||||
text="取消",
|
||||
command=self._on_cancel,
|
||||
width=BUTTON_WIDTH_MEDIUM,
|
||||
height=BUTTON_HEIGHT_SMALL,
|
||||
font=ctk.CTkFont(size=FONT_SIZE_MEDIUM),
|
||||
fg_color="#666666",
|
||||
hover_color="#555555"
|
||||
)
|
||||
cancel_btn.pack(side="left", padx=5)
|
||||
|
||||
# 确定按钮
|
||||
ok_btn = ctk.CTkButton(
|
||||
button_frame,
|
||||
text="确定",
|
||||
command=self._on_ok,
|
||||
width=BUTTON_WIDTH_MEDIUM,
|
||||
height=BUTTON_HEIGHT_SMALL,
|
||||
font=ctk.CTkFont(size=FONT_SIZE_MEDIUM)
|
||||
)
|
||||
ok_btn.pack(side="left", padx=5)
|
||||
|
||||
# 绑定键盘事件
|
||||
self.entry.bind("<Return>", lambda e: self._on_ok())
|
||||
self.bind("<Escape>", lambda e: self._on_cancel())
|
||||
|
||||
def destroy(self):
|
||||
"""销毁对话框前解除事件绑定"""
|
||||
try:
|
||||
if hasattr(self, 'entry'):
|
||||
self.entry.unbind("<Return>")
|
||||
self.unbind("<Escape>")
|
||||
except Exception:
|
||||
pass
|
||||
return super().destroy()
|
||||
|
||||
def _on_ok(self):
|
||||
"""确定按钮点击"""
|
||||
self.result = self.entry.get().strip()
|
||||
self.destroy()
|
||||
|
||||
def _on_cancel(self):
|
||||
"""取消按钮点击"""
|
||||
self.result = None
|
||||
self.destroy()
|
||||
|
||||
|
||||
# 便捷函数
|
||||
def show_info(parent, title: str, message: str):
|
||||
"""显示信息对话框"""
|
||||
MessageDialog(parent, title, message, "info")
|
||||
|
||||
|
||||
def show_warning(parent, title: str, message: str):
|
||||
"""显示警告对话框"""
|
||||
MessageDialog(parent, title, message, "warning")
|
||||
|
||||
|
||||
def show_error(parent, title: str, message: str):
|
||||
"""显示错误对话框"""
|
||||
MessageDialog(parent, title, message, "error")
|
||||
|
||||
|
||||
def ask_yes_no(parent, title: str, message: str) -> bool:
|
||||
"""显示是/否对话框"""
|
||||
dialog = MessageDialog(parent, title, message, "question")
|
||||
return dialog.result if dialog.result is not None else False
|
||||
|
||||
|
||||
def ask_string(parent, title: str, prompt: str, initial_value: str = "") -> Optional[str]:
|
||||
"""显示输入对话框"""
|
||||
dialog = InputDialog(parent, title, prompt, initial_value)
|
||||
return dialog.result
|
||||
202
ui/utilities/icon_manager.py
Normal file
@@ -0,0 +1,202 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
图标管理器
|
||||
负责图标的加载、缓存和管理
|
||||
"""
|
||||
import os
|
||||
import glob
|
||||
import customtkinter as ctk
|
||||
from PIL import Image
|
||||
from functools import lru_cache
|
||||
from collections import OrderedDict
|
||||
from config.constants import APP_ICON_MAPPING
|
||||
|
||||
|
||||
class IconManager:
|
||||
"""图标管理器,负责图标的加载、缓存和管理"""
|
||||
|
||||
def __init__(self, icons_dir: str, icon_size: int):
|
||||
"""
|
||||
初始化图标管理器
|
||||
|
||||
Args:
|
||||
icons_dir: 图标目录路径
|
||||
icon_size: 图标大小
|
||||
"""
|
||||
self.icons_dir = icons_dir
|
||||
self.icon_size = icon_size
|
||||
# 使用有界缓存控制内存占用
|
||||
self._max_cache_size = 128
|
||||
self.cache: OrderedDict[str, ctk.CTkImage] = OrderedDict()
|
||||
|
||||
def get_app_icon(self, app_path: str, config_manager) -> ctk.CTkImage:
|
||||
"""
|
||||
获取应用图标
|
||||
|
||||
Args:
|
||||
app_path: 应用路径
|
||||
config_manager: 配置管理器
|
||||
|
||||
Returns:
|
||||
CTkImage 对象,如果失败则返回 None
|
||||
"""
|
||||
# 检查缓存
|
||||
if app_path in self.cache:
|
||||
self.cache.move_to_end(app_path)
|
||||
return self.cache[app_path]
|
||||
|
||||
try:
|
||||
# 查找图标路径
|
||||
icon_path = self._find_icon_path(app_path, config_manager)
|
||||
if not icon_path:
|
||||
return self._get_fallback_icon(app_path)
|
||||
|
||||
# 创建并缓存图标
|
||||
ctk_image = self._create_ctk_icon(icon_path)
|
||||
self.cache[app_path] = ctk_image
|
||||
# 控制缓存大小
|
||||
if len(self.cache) > self._max_cache_size:
|
||||
self.cache.popitem(last=False)
|
||||
return ctk_image
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to load icon ({app_path}): {e}")
|
||||
return self._get_fallback_icon(app_path)
|
||||
|
||||
def _find_icon_path(self, app_path: str, config_manager) -> str:
|
||||
"""
|
||||
查找应用图标路径
|
||||
|
||||
查找优先级:
|
||||
1. 自定义图标
|
||||
2. 预设图标映射
|
||||
3. 应用名称匹配
|
||||
4. 默认图标
|
||||
5. 任意可用图标
|
||||
|
||||
Args:
|
||||
app_path: 应用路径
|
||||
config_manager: 配置管理器
|
||||
|
||||
Returns:
|
||||
图标文件路径,如果未找到则返回 None
|
||||
"""
|
||||
app_name = os.path.splitext(os.path.basename(app_path))[0]
|
||||
|
||||
# 1. 检查自定义图标
|
||||
custom_icon = config_manager.get_app_icon(app_path)
|
||||
if custom_icon and os.path.exists(custom_icon):
|
||||
return custom_icon
|
||||
|
||||
# 2. 匹配预设图标
|
||||
app_name_lower = app_name.lower()
|
||||
for key, icon_name in APP_ICON_MAPPING.items():
|
||||
if key in app_name_lower:
|
||||
icon_path = os.path.join(self.icons_dir, f"{icon_name}.png")
|
||||
if os.path.exists(icon_path):
|
||||
return icon_path
|
||||
|
||||
# 3. 使用应用名称
|
||||
icon_path = os.path.join(self.icons_dir, f"{app_name}.png")
|
||||
if os.path.exists(icon_path):
|
||||
return icon_path
|
||||
|
||||
# 4. 使用默认图标
|
||||
default_icon = os.path.join(self.icons_dir, "NexusLauncher.ico")
|
||||
if os.path.exists(default_icon):
|
||||
return default_icon
|
||||
|
||||
# 5. 使用任意可用图标
|
||||
icons = glob.glob(os.path.join(self.icons_dir, "*.png"))
|
||||
icons.extend(glob.glob(os.path.join(self.icons_dir, "*.ico")))
|
||||
return icons[0] if icons else None
|
||||
|
||||
@lru_cache(maxsize=128)
|
||||
def _load_pil_image(self, icon_path: str) -> Image.Image:
|
||||
"""
|
||||
加载 PIL 图像(带 LRU 缓存)
|
||||
|
||||
使用 LRU 缓存可以避免重复加载相同的图标文件,
|
||||
提升性能并减少磁盘 I/O
|
||||
|
||||
Args:
|
||||
icon_path: 图标文件路径
|
||||
|
||||
Returns:
|
||||
PIL Image 对象
|
||||
"""
|
||||
return Image.open(icon_path)
|
||||
|
||||
def _create_ctk_icon(self, icon_path: str) -> ctk.CTkImage:
|
||||
"""
|
||||
创建 CTk 图标对象
|
||||
|
||||
Args:
|
||||
icon_path: 图标文件路径
|
||||
|
||||
Returns:
|
||||
CTkImage 对象
|
||||
"""
|
||||
pil_image = self._load_pil_image(icon_path)
|
||||
icon_display_size = int(self.icon_size * 0.6)
|
||||
return ctk.CTkImage(
|
||||
light_image=pil_image,
|
||||
dark_image=pil_image,
|
||||
size=(icon_display_size, icon_display_size)
|
||||
)
|
||||
|
||||
def _get_fallback_icon(self, app_path: str) -> ctk.CTkImage:
|
||||
"""
|
||||
获取降级图标
|
||||
|
||||
当无法加载指定图标时,尝试使用任意可用图标
|
||||
|
||||
Args:
|
||||
app_path: 应用路径
|
||||
|
||||
Returns:
|
||||
CTkImage 对象,如果失败则返回 None
|
||||
"""
|
||||
try:
|
||||
icons = glob.glob(os.path.join(self.icons_dir, "*.png"))
|
||||
if icons:
|
||||
ctk_image = self._create_ctk_icon(icons[0])
|
||||
self.cache[app_path] = ctk_image
|
||||
if len(self.cache) > self._max_cache_size:
|
||||
self.cache.popitem(last=False)
|
||||
return ctk_image
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
|
||||
def clear_cache(self):
|
||||
"""清空所有缓存"""
|
||||
self.cache.clear()
|
||||
self._load_pil_image.cache_clear()
|
||||
|
||||
def update_icon_size(self, new_size: int):
|
||||
"""
|
||||
更新图标大小
|
||||
|
||||
更新图标大小后会清空缓存,
|
||||
下次获取图标时会使用新的尺寸重新创建
|
||||
|
||||
Args:
|
||||
new_size: 新的图标大小
|
||||
"""
|
||||
self.icon_size = new_size
|
||||
self.clear_cache()
|
||||
|
||||
def get_cache_info(self) -> dict:
|
||||
"""
|
||||
获取缓存信息
|
||||
|
||||
Returns:
|
||||
包含缓存统计信息的字典
|
||||
"""
|
||||
return {
|
||||
'ctk_cache_size': len(self.cache),
|
||||
'pil_cache_info': self._load_pil_image.cache_info()._asdict()
|
||||
}
|
||||
91
ui/utilities/icon_utils.py
Normal file
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
图标工具模块
|
||||
负责图标路径获取和窗口图标设置
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def get_icons_dir() -> str:
|
||||
"""获取 icons 目录的绝对路径
|
||||
|
||||
Returns:
|
||||
icons 目录的绝对路径
|
||||
"""
|
||||
# 获取当前文件所在目录(ui/utilities/)
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# 往上两级到项目根目录
|
||||
project_root = os.path.dirname(os.path.dirname(current_dir))
|
||||
|
||||
# 拼接 icons 文件夹
|
||||
icons_dir = os.path.join(project_root, "icons")
|
||||
|
||||
# 如果是打包后的应用,使用 sys._MEIPASS
|
||||
if getattr(sys, 'frozen', False):
|
||||
# 打包后的应用
|
||||
icons_dir = os.path.join(sys._MEIPASS, "icons")
|
||||
|
||||
return icons_dir
|
||||
|
||||
|
||||
def get_icon_path(icon_name: str = "NexusLauncher.ico") -> str:
|
||||
"""获取图标文件的绝对路径
|
||||
|
||||
Args:
|
||||
icon_name: 图标文件名,默认为 "NexusLauncher.ico"
|
||||
|
||||
Returns:
|
||||
图标文件的绝对路径
|
||||
"""
|
||||
icons_dir = get_icons_dir()
|
||||
icon_path = os.path.join(icons_dir, icon_name)
|
||||
return icon_path
|
||||
|
||||
|
||||
def set_window_icon(window, icon_name: str = "NexusLauncher.ico"):
|
||||
"""为窗口设置图标
|
||||
|
||||
Args:
|
||||
window: Tkinter 窗口对象
|
||||
icon_name: 图标文件名,默认为 "NexusLauncher.ico"
|
||||
"""
|
||||
icon_path = get_icon_path(icon_name)
|
||||
|
||||
if os.path.exists(icon_path):
|
||||
try:
|
||||
window.iconbitmap(icon_path)
|
||||
window.wm_iconbitmap(icon_path)
|
||||
except Exception as e:
|
||||
print(f"Failed to set window icon: {e}")
|
||||
else:
|
||||
print(f"Icon file not found: {icon_path}")
|
||||
|
||||
|
||||
def setup_dialog_icon(dialog, icon_name: str = "NexusLauncher.ico"):
|
||||
"""为对话框设置图标(包含延迟设置以确保生效)
|
||||
|
||||
Args:
|
||||
dialog: 对话框窗口对象
|
||||
icon_name: 图标文件名,默认为 "NexusLauncher.ico"
|
||||
"""
|
||||
icon_path = get_icon_path(icon_name)
|
||||
|
||||
if not os.path.exists(icon_path):
|
||||
return
|
||||
|
||||
def set_icon():
|
||||
try:
|
||||
dialog.iconbitmap(icon_path)
|
||||
dialog.wm_iconbitmap(icon_path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 立即设置
|
||||
set_icon()
|
||||
|
||||
# 延迟设置(确保生效)
|
||||
dialog.after(50, set_icon)
|
||||
dialog.after(200, set_icon)
|
||||
126
ui/utilities/ui_helpers.py
Normal file
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
UI辅助工具
|
||||
负责标签按钮、下拉框等UI组件的辅助功能
|
||||
"""
|
||||
import customtkinter as ctk
|
||||
from config.constants import COLOR_TRANSPARENT
|
||||
|
||||
|
||||
class UIHelpers:
|
||||
"""UI辅助工具类,提供各种UI组件的辅助功能"""
|
||||
|
||||
@staticmethod
|
||||
def adjust_tab_button_width(tabview):
|
||||
"""调整标签按钮宽度,让它们平分整个宽度"""
|
||||
try:
|
||||
# 获取tabview的宽度
|
||||
tabview_width = tabview.winfo_width()
|
||||
|
||||
if tabview_width > 1:
|
||||
# 获取标签按钮
|
||||
segmented_button = tabview._segmented_button
|
||||
buttons_dict = segmented_button._buttons_dict
|
||||
|
||||
# 计算每个按钮的宽度(平分)
|
||||
button_count = len(buttons_dict)
|
||||
if button_count > 0:
|
||||
button_width = tabview_width // button_count
|
||||
|
||||
# 设置每个按钮的宽度
|
||||
for button in buttons_dict.values():
|
||||
try:
|
||||
button.configure(width=button_width)
|
||||
except:
|
||||
pass
|
||||
|
||||
print(f"Adjusted tab button width: {button_width}px (total width: {tabview_width}px)")
|
||||
except Exception as e:
|
||||
print(f"Failed to adjust tab button width: {e}")
|
||||
|
||||
@staticmethod
|
||||
def fix_dropdown_width(combo_box):
|
||||
"""修复下拉菜单宽度,使其与下拉框一致"""
|
||||
try:
|
||||
# 获取下拉框的实际宽度
|
||||
combo_width = combo_box.winfo_width()
|
||||
|
||||
if combo_width > 1:
|
||||
# 方法1: 直接设置 CTkComboBox 的内部属性
|
||||
if hasattr(combo_box, '_dropdown_menu'):
|
||||
dropdown_menu = combo_box._dropdown_menu
|
||||
|
||||
if dropdown_menu is not None:
|
||||
# 设置下拉菜单的宽度
|
||||
try:
|
||||
dropdown_menu.configure(width=combo_width)
|
||||
except:
|
||||
pass
|
||||
|
||||
# 尝试设置内部 frame 的宽度
|
||||
try:
|
||||
if hasattr(dropdown_menu, 'winfo_children'):
|
||||
for child in dropdown_menu.winfo_children():
|
||||
child.configure(width=combo_width)
|
||||
except:
|
||||
pass
|
||||
|
||||
# 方法2: 重写 _open_dropdown_menu 方法
|
||||
if not hasattr(combo_box, '_original_open_dropdown'):
|
||||
combo_box._original_open_dropdown = combo_box._open_dropdown_menu
|
||||
|
||||
def custom_open_dropdown():
|
||||
combo_box._original_open_dropdown()
|
||||
# 延迟设置宽度
|
||||
if hasattr(combo_box, 'after'):
|
||||
combo_box.after(1, lambda: UIHelpers._set_dropdown_width(combo_box, combo_width))
|
||||
|
||||
combo_box._open_dropdown_menu = custom_open_dropdown
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to set dropdown menu width: {e}")
|
||||
|
||||
@staticmethod
|
||||
def _set_dropdown_width(combo_box, width):
|
||||
"""设置下拉菜单宽度的辅助方法"""
|
||||
try:
|
||||
if hasattr(combo_box, '_dropdown_menu') and combo_box._dropdown_menu is not None:
|
||||
dropdown = combo_box._dropdown_menu
|
||||
dropdown.configure(width=width)
|
||||
|
||||
# 遍历所有子组件并设置宽度
|
||||
for widget in dropdown.winfo_children():
|
||||
try:
|
||||
widget.configure(width=width)
|
||||
except:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def configure_tab_transparency(project_tab, task_tab):
|
||||
"""配置标签页透明度"""
|
||||
try:
|
||||
# 尝试将标签页背景设置为透明,让圆角容器可见
|
||||
project_tab.configure(fg_color=COLOR_TRANSPARENT)
|
||||
task_tab.configure(fg_color=COLOR_TRANSPARENT)
|
||||
print("[OK] Set tab background to transparent")
|
||||
except Exception as e:
|
||||
print(f"Failed to set tab background transparent: {e}")
|
||||
|
||||
@staticmethod
|
||||
def hide_scrollbar(scrollable_frame):
|
||||
"""隐藏 CTkScrollableFrame 的滚动条(仍保持滚动功能)"""
|
||||
try:
|
||||
scrollbar = getattr(scrollable_frame, "_scrollbar", None)
|
||||
if scrollbar:
|
||||
bgcolor = scrollable_frame.cget("fg_color") if hasattr(scrollable_frame, "cget") else COLOR_TRANSPARENT
|
||||
scrollbar.configure(fg_color=COLOR_TRANSPARENT, button_color=bgcolor, button_hover_color=bgcolor)
|
||||
scrollbar.configure(width=0)
|
||||
try:
|
||||
scrollbar.grid_remove()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
327
ui/utilities/window_manager.py
Normal file
@@ -0,0 +1,327 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
窗口管理器
|
||||
负责窗口定位、托盘图标、日志窗口等通用窗口功能
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import ctypes
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from PIL import Image
|
||||
import pystray
|
||||
from pystray import MenuItem as item
|
||||
import customtkinter as ctk
|
||||
from config.constants import CONSOLE_WINDOW_SIZE
|
||||
from .icon_utils import get_icon_path
|
||||
|
||||
|
||||
class ConsoleRedirector:
|
||||
"""重定向 stdout 到控制台窗口"""
|
||||
|
||||
def __init__(self, log_callback, original_stdout):
|
||||
self.log_callback = log_callback
|
||||
self.original_stdout = original_stdout
|
||||
self.buffer = ""
|
||||
|
||||
def write(self, message):
|
||||
"""写入消息到控制台窗口和原始 stdout"""
|
||||
# 同时输出到原始 stdout(命令行)和控制台窗口
|
||||
if self.original_stdout:
|
||||
self.original_stdout.write(message)
|
||||
self.original_stdout.flush()
|
||||
|
||||
# 输出到控制台窗口
|
||||
if message and message.strip(): # 只记录非空消息
|
||||
self.log_callback(message.rstrip())
|
||||
|
||||
def flush(self):
|
||||
"""刷新缓冲区"""
|
||||
if self.original_stdout:
|
||||
self.original_stdout.flush()
|
||||
|
||||
|
||||
class WindowManager:
|
||||
"""窗口管理器,处理窗口定位、托盘图标、日志窗口等功能"""
|
||||
|
||||
def __init__(self, main_window, config_manager):
|
||||
"""
|
||||
初始化窗口管理器
|
||||
|
||||
Args:
|
||||
main_window: 主窗口实例
|
||||
config_manager: 配置管理器
|
||||
"""
|
||||
self.main_window = main_window
|
||||
self.config_manager = config_manager
|
||||
|
||||
# 托盘图标相关
|
||||
self.tray_icon = None
|
||||
self.is_quitting = False
|
||||
|
||||
# 日志窗口相关
|
||||
self.console_visible = False
|
||||
self.console_window = None
|
||||
self.log_text = None
|
||||
|
||||
# stdout 重定向
|
||||
self.original_stdout = sys.stdout
|
||||
self.console_redirector = None
|
||||
|
||||
# 图标路径
|
||||
self.icon_path = get_icon_path()
|
||||
|
||||
def setup_window_appid(self):
|
||||
"""设置Windows AppUserModelID,确保任务栏图标正确显示"""
|
||||
try:
|
||||
myappid = 'NexusLauncher.App.1.0'
|
||||
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
|
||||
except:
|
||||
pass
|
||||
|
||||
def position_window_bottom_right(self, width, height):
|
||||
"""将窗口定位到屏幕右下角(任务栏上方)"""
|
||||
# 先设置窗口大小
|
||||
self.main_window.geometry(f"{width}x{height}")
|
||||
|
||||
# 更新窗口以获取准确的尺寸
|
||||
self.main_window.update_idletasks()
|
||||
|
||||
# 获取屏幕尺寸
|
||||
screen_width = self.main_window.winfo_screenwidth()
|
||||
screen_height = self.main_window.winfo_screenheight()
|
||||
|
||||
# 计算右下角位置(留出任务栏空间,确保不重叠)
|
||||
taskbar_height = 80 # 任务栏高度 + 额外间距,确保不重叠
|
||||
x = screen_width - width - 15 # 右边距15px
|
||||
y = screen_height - height - taskbar_height # 底部留出足够空间
|
||||
|
||||
# 设置窗口位置
|
||||
self.main_window.geometry(f"{width}x{height}+{x}+{y}")
|
||||
|
||||
def set_window_icon(self):
|
||||
"""设置窗口图标"""
|
||||
if os.path.exists(self.icon_path):
|
||||
self.main_window.iconbitmap(self.icon_path)
|
||||
|
||||
def setup_tray_icon(self):
|
||||
"""设置系统托盘图标"""
|
||||
try:
|
||||
# 加载图标
|
||||
if os.path.exists(self.icon_path):
|
||||
icon_image = Image.open(self.icon_path)
|
||||
else:
|
||||
# 如果图标不存在,创建一个简单的默认图标
|
||||
icon_image = Image.new('RGB', (64, 64), color='blue')
|
||||
|
||||
# 创建托盘菜单
|
||||
console_text = '隐藏日志' if self.console_visible else '显示日志'
|
||||
menu = pystray.Menu(
|
||||
item('显示主窗口', self._show_window, default=True),
|
||||
item('设置', self._show_settings),
|
||||
item(console_text, self._toggle_console),
|
||||
pystray.Menu.SEPARATOR,
|
||||
item('退出', self._quit_app)
|
||||
)
|
||||
|
||||
# 创建托盘图标
|
||||
self.tray_icon = pystray.Icon(
|
||||
"NexusLauncher",
|
||||
icon_image,
|
||||
"NexusLauncher",
|
||||
menu
|
||||
)
|
||||
|
||||
# 在单独的线程中运行托盘图标
|
||||
tray_thread = threading.Thread(target=self.tray_icon.run, daemon=True)
|
||||
tray_thread.start()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to create tray icon: {e}")
|
||||
|
||||
def create_log_window(self):
|
||||
"""创建自定义日志窗口"""
|
||||
self.console_window = ctk.CTkToplevel(self.main_window)
|
||||
self.console_window.title("NexusLauncher - 控制台")
|
||||
self.console_window.geometry(CONSOLE_WINDOW_SIZE)
|
||||
|
||||
# 设置图标 - 使用持续监控方式
|
||||
if os.path.exists(self.icon_path):
|
||||
self.console_window.iconbitmap(self.icon_path)
|
||||
# 持续设置图标,防止被 CustomTkinter 覆盖
|
||||
self._keep_console_icon_alive()
|
||||
|
||||
# 创建文本框显示日志
|
||||
self.log_text = ctk.CTkTextbox(
|
||||
self.console_window,
|
||||
wrap="word",
|
||||
font=ctk.CTkFont(family="Consolas", size=12)
|
||||
)
|
||||
self.log_text.pack(fill="both", expand=True, padx=10, pady=10)
|
||||
|
||||
# 添加欢迎信息
|
||||
self.log_text.insert("1.0", "NexusLauncher 控制台\n")
|
||||
self.log_text.insert("end", "=" * 50 + "\n")
|
||||
self.log_text.insert("end", "实时显示应用调试信息\n")
|
||||
self.log_text.insert("end", "关闭此窗口不会退出应用\n")
|
||||
self.log_text.insert("end", "=" * 50 + "\n\n")
|
||||
|
||||
# 绑定关闭事件 - 只隐藏不退出
|
||||
self.console_window.protocol("WM_DELETE_WINDOW", self._on_log_window_close)
|
||||
|
||||
# 重定向 stdout 到控制台窗口
|
||||
self._redirect_stdout()
|
||||
|
||||
def show_console(self):
|
||||
"""显示日志窗口"""
|
||||
if not self.console_window or not self.console_window.winfo_exists():
|
||||
self.create_log_window()
|
||||
else:
|
||||
self.console_window.deiconify()
|
||||
self.console_window.lift()
|
||||
self.console_visible = True
|
||||
# 更新托盘菜单
|
||||
if self.tray_icon:
|
||||
self._update_tray_menu()
|
||||
self.log_with_timestamp("[VIEW] 日志窗口已显示")
|
||||
|
||||
def hide_console(self):
|
||||
"""隐藏日志窗口"""
|
||||
if self.console_window and self.console_window.winfo_exists():
|
||||
self.console_window.withdraw()
|
||||
self.console_visible = False
|
||||
# 更新托盘菜单
|
||||
if self.tray_icon:
|
||||
self._update_tray_menu()
|
||||
print("[VIEW] Console window hidden")
|
||||
|
||||
def log(self, message: str):
|
||||
"""记录日志到控制台窗口(不带时间戳,用于 print 重定向)"""
|
||||
if self.log_text:
|
||||
try:
|
||||
self.log_text.insert("end", f"{message}\n")
|
||||
self.log_text.see("end") # 自动滚动到最新日志
|
||||
except:
|
||||
pass
|
||||
|
||||
def log_with_timestamp(self, message: str):
|
||||
"""记录带时间戳的日志到控制台窗口(用于重要事件)"""
|
||||
if self.log_text:
|
||||
try:
|
||||
timestamp = datetime.now().strftime("%H:%M:%S")
|
||||
self.log_text.insert("end", f"[{timestamp}] {message}\n")
|
||||
self.log_text.see("end") # 自动滚动到最新日志
|
||||
except:
|
||||
pass
|
||||
|
||||
def _redirect_stdout(self):
|
||||
"""重定向 stdout 到控制台窗口"""
|
||||
if not self.console_redirector:
|
||||
self.console_redirector = ConsoleRedirector(self.log, self.original_stdout)
|
||||
sys.stdout = self.console_redirector
|
||||
|
||||
def _restore_stdout(self):
|
||||
"""恢复原始 stdout"""
|
||||
if self.console_redirector:
|
||||
sys.stdout = self.original_stdout
|
||||
self.console_redirector = None
|
||||
|
||||
def hide_window(self):
|
||||
"""隐藏窗口到托盘"""
|
||||
self.main_window.withdraw()
|
||||
|
||||
def show_window(self):
|
||||
"""显示主窗口"""
|
||||
self.main_window.deiconify()
|
||||
self.main_window.lift()
|
||||
self.main_window.focus_force()
|
||||
|
||||
def quit_app(self):
|
||||
"""退出应用程序"""
|
||||
self.is_quitting = True
|
||||
|
||||
# 恢复原始 stdout
|
||||
self._restore_stdout()
|
||||
|
||||
# 关闭自定义日志窗口
|
||||
if self.console_window and self.console_window.winfo_exists():
|
||||
self.console_window.destroy()
|
||||
|
||||
# 停止托盘图标
|
||||
if self.tray_icon:
|
||||
self.tray_icon.stop()
|
||||
|
||||
# 保存窗口大小
|
||||
try:
|
||||
width = self.main_window.winfo_width()
|
||||
height = self.main_window.winfo_height()
|
||||
self.config_manager.save_window_size(width, height)
|
||||
except:
|
||||
pass
|
||||
|
||||
# 关闭应用
|
||||
self.main_window.after(0, self.main_window.quit)
|
||||
|
||||
def _keep_console_icon_alive(self):
|
||||
"""持续保持控制台图标不被覆盖"""
|
||||
try:
|
||||
if self.console_window and self.console_window.winfo_exists() and os.path.exists(self.icon_path):
|
||||
self.console_window.iconbitmap(self.icon_path)
|
||||
# 每 50ms 检查一次,持续 500ms
|
||||
if not hasattr(self, '_console_icon_check_count'):
|
||||
self._console_icon_check_count = 0
|
||||
|
||||
if self._console_icon_check_count < 10:
|
||||
self._console_icon_check_count += 1
|
||||
self.main_window.after(50, self._keep_console_icon_alive)
|
||||
except:
|
||||
pass
|
||||
|
||||
def _on_log_window_close(self):
|
||||
"""日志窗口关闭事件"""
|
||||
self.hide_console()
|
||||
|
||||
def _toggle_console(self, icon=None, item=None):
|
||||
"""切换控制台窗口显示状态"""
|
||||
if self.console_visible:
|
||||
self.main_window.after(0, self.hide_console)
|
||||
else:
|
||||
self.main_window.after(0, self.show_console)
|
||||
|
||||
def _update_tray_menu(self):
|
||||
"""更新托盘菜单"""
|
||||
try:
|
||||
console_text = '隐藏日志' if self.console_visible else '显示日志'
|
||||
menu = pystray.Menu(
|
||||
item('显示主窗口', self._show_window, default=True),
|
||||
item('设置', self._show_settings),
|
||||
item(console_text, self._toggle_console),
|
||||
pystray.Menu.SEPARATOR,
|
||||
item('退出', self._quit_app)
|
||||
)
|
||||
self.tray_icon.menu = menu
|
||||
except Exception as e:
|
||||
print(f"Failed to update tray menu: {e}")
|
||||
|
||||
def _show_window(self, icon=None, item=None):
|
||||
"""显示主窗口"""
|
||||
self.main_window.after(0, self.show_window)
|
||||
|
||||
def _show_settings(self, icon=None, item=None):
|
||||
"""显示设置窗口"""
|
||||
self.main_window.after(0, self._do_show_settings)
|
||||
|
||||
def _do_show_settings(self):
|
||||
"""在主线程中显示设置窗口"""
|
||||
# 如果窗口隐藏,先显示主窗口
|
||||
if not self.main_window.winfo_viewable():
|
||||
self.main_window.deiconify()
|
||||
|
||||
# 调用主窗口的设置方法
|
||||
if hasattr(self.main_window, '_open_settings'):
|
||||
self.main_window._open_settings()
|
||||
|
||||
def _quit_app(self, icon=None, item=None):
|
||||
"""退出应用程序"""
|
||||
self.quit_app()
|
||||
67
ui/utilities/window_utils.py
Normal file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
窗口工具模块
|
||||
负责窗口样式设置和对话框配置
|
||||
"""
|
||||
import ctypes
|
||||
from .icon_utils import setup_dialog_icon
|
||||
|
||||
|
||||
def set_dark_title_bar(window):
|
||||
"""设置窗口为深色标题栏(Windows 10/11)
|
||||
|
||||
Args:
|
||||
window: 窗口对象
|
||||
"""
|
||||
try:
|
||||
hwnd = ctypes.windll.user32.GetParent(window.winfo_id())
|
||||
DWMWA_USE_IMMERSIVE_DARK_MODE = 20
|
||||
value = ctypes.c_int(2)
|
||||
ctypes.windll.dwmapi.DwmSetWindowAttribute(
|
||||
hwnd,
|
||||
DWMWA_USE_IMMERSIVE_DARK_MODE,
|
||||
ctypes.byref(value),
|
||||
ctypes.sizeof(value)
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Failed to set dark title bar: {e}")
|
||||
|
||||
|
||||
def setup_dialog_window(dialog, title: str, width: int, height: int, parent=None, center: bool = True):
|
||||
"""统一配置对话框窗口
|
||||
|
||||
Args:
|
||||
dialog: 对话框对象
|
||||
title: 窗口标题
|
||||
width: 窗口宽度
|
||||
height: 窗口高度
|
||||
parent: 父窗口(可选)
|
||||
center: 是否居中显示,默认 True
|
||||
|
||||
Returns:
|
||||
配置好的对话框对象
|
||||
"""
|
||||
dialog.title(title)
|
||||
dialog.geometry(f"{width}x{height}")
|
||||
dialog.configure(fg_color="#2b2b2b")
|
||||
|
||||
# 设置图标
|
||||
setup_dialog_icon(dialog)
|
||||
|
||||
# 设置为模态
|
||||
if parent:
|
||||
dialog.transient(parent)
|
||||
dialog.grab_set()
|
||||
|
||||
# 设置深色标题栏
|
||||
dialog.after(10, lambda: set_dark_title_bar(dialog))
|
||||
|
||||
# 居中显示
|
||||
if center:
|
||||
dialog.update_idletasks()
|
||||
x = (dialog.winfo_screenwidth() // 2) - (width // 2)
|
||||
y = (dialog.winfo_screenheight() // 2) - (height // 2)
|
||||
dialog.geometry(f"{width}x{height}+{x}+{y}")
|
||||
|
||||
return dialog
|
||||