Update
This commit is contained in:
51
ui/__init__.py
Normal file
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
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
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
2121
ui/settings_window.py
Normal file
File diff suppressed because it is too large
Load Diff
138
ui/splash_screen.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
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
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
1695
ui/task/subfolder_editor.py
Normal file
File diff suppressed because it is too large
Load Diff
2094
ui/task/task_panel.py
Normal file
2094
ui/task/task_panel.py
Normal file
File diff suppressed because it is too large
Load Diff
82
ui/utilities/__init__.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
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
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
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
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
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
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
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
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
|
||||
Reference in New Issue
Block a user