2122 lines
82 KiB
Python
2122 lines
82 KiB
Python
#!/usr/bin/env python
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
设置窗口 - 提供现代化的配置界面
|
||
"""
|
||
import customtkinter as ctk
|
||
import os
|
||
import glob
|
||
from tkinter import filedialog
|
||
from typing import Callable
|
||
from PIL import Image
|
||
from config import ConfigManager
|
||
from config.constants import (
|
||
# 颜色常量
|
||
PRESET_COLORS, BORDER_COLOR, BG_COLOR_DARK, LINE_COLOR_GRAY,
|
||
BORDER_COLOR_WHITE, DIALOG_BG_COLOR, DIALOG_TEXT_COLOR,
|
||
|
||
# 按钮颜色
|
||
BUTTON_GRAY, BUTTON_GRAY_HOVER, BUTTON_RED, BUTTON_RED_HOVER,
|
||
BUTTON_BLUE, BUTTON_BLUE_HOVER, BUTTON_GREEN, BUTTON_GREEN_HOVER,
|
||
SAVE_BUTTON_COLOR, SAVE_BUTTON_HOVER,
|
||
|
||
# 状态颜色
|
||
COLOR_SUCCESS, COLOR_SUCCESS_HOVER,
|
||
|
||
# 拖拽和选择颜色
|
||
DRAG_HIGHLIGHT_COLOR, DRAG_HIGHLIGHT_BG, SELECTION_BORDER, SELECTION_BG,
|
||
|
||
# 滚动条颜色
|
||
SCROLLBAR_COLOR, SCROLLBAR_HOVER_COLOR,
|
||
|
||
# 窗口尺寸
|
||
SETTINGS_WINDOW_SIZE, DIALOG_INPUT_SIZE, DIALOG_CONFIRM_SIZE,
|
||
DIALOG_APP_EDIT_SIZE, DIALOG_ICON_SELECT_SIZE
|
||
)
|
||
from .utilities import custom_dialogs
|
||
from .utilities.icon_utils import get_icon_path, set_window_icon, get_icons_dir
|
||
import ctypes
|
||
|
||
class SettingsWindow(ctk.CTkToplevel):
|
||
"""设置窗口类"""
|
||
|
||
def __init__(self, parent, config_manager: ConfigManager, on_update: Callable):
|
||
super().__init__(parent)
|
||
|
||
self.config_manager = config_manager
|
||
self.on_update = on_update
|
||
self.debug_mode = False # 调试模式控制
|
||
|
||
# 拖放相关变量
|
||
self.drag_source = None
|
||
self.drag_target = None
|
||
self.drag_data = None
|
||
self.drag_widget = None
|
||
|
||
# 多选和复制粘贴相关变量
|
||
self.selected_items = [] # 存储选中的应用索引
|
||
self.clipboard_apps = [] # 剪贴板中的应用数据
|
||
self.last_selected_index = None # 上次选中的索引,用于Shift多选
|
||
|
||
# 性能优化相关变量
|
||
self._update_timer = None # 延迟更新定时器
|
||
self._cached_projects = None # 缓存的项目列表
|
||
self._last_project_update = 0 # 上次项目更新时间
|
||
|
||
# 窗口配置
|
||
self.title("NexusLauncher - Settings")
|
||
self.geometry(SETTINGS_WINDOW_SIZE)
|
||
self.resizable(True, True)
|
||
|
||
# 先隐藏窗口,避免加载时闪烁
|
||
self.withdraw()
|
||
|
||
# 设置窗口图标(在transient之前设置)
|
||
set_window_icon(self)
|
||
|
||
# 先设置为瞬态窗口
|
||
self.transient(parent)
|
||
|
||
# 等待窗口创建完成
|
||
self.update_idletasks()
|
||
|
||
# 再次尝试设置图标(确保生效)
|
||
def set_icon_delayed():
|
||
set_window_icon(self)
|
||
self.after(50, set_icon_delayed)
|
||
self.after(200, set_icon_delayed)
|
||
|
||
# 创建界面
|
||
self._create_widgets()
|
||
self._load_data()
|
||
|
||
# 绑定基本键盘快捷键
|
||
self.bind_all("<Delete>", self._handle_delete, "+")
|
||
self.bind_all("<Escape>", self._handle_escape, "+")
|
||
|
||
# 延迟绑定全局点击事件,确保所有控件都已创建
|
||
self.after(100, self._setup_global_click_binding)
|
||
|
||
# 所有内容加载完成后,居中显示窗口并抓取焦点
|
||
self._center_window()
|
||
self.deiconify()
|
||
self.grab_set()
|
||
self.focus_set()
|
||
|
||
# 设置深色标题栏
|
||
self.after(10, lambda: self._set_dark_title_bar(self))
|
||
|
||
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 _ensure_project_selected(self) -> str:
|
||
"""确保已选择项目,返回项目名称或None"""
|
||
current_project = self.project_combo.get()
|
||
if not current_project:
|
||
custom_dialogs.show_warning(self, "警告", "请先选择一个项目")
|
||
return None
|
||
return current_project
|
||
|
||
def _create_button(self, parent, text: str, command, width: int = 120, height: int = 35,
|
||
fg_color=None, hover_color=None, font_size: int = 12):
|
||
"""创建标准按钮的工厂方法"""
|
||
if fg_color is None:
|
||
fg_color = BUTTON_BLUE
|
||
if hover_color is None:
|
||
hover_color = BUTTON_BLUE_HOVER
|
||
|
||
return ctk.CTkButton(
|
||
parent,
|
||
text=text,
|
||
command=command,
|
||
width=width,
|
||
height=height,
|
||
font=ctk.CTkFont(size=font_size),
|
||
fg_color=fg_color,
|
||
hover_color=hover_color
|
||
)
|
||
|
||
def _batch_operation(self, operation_func, items, operation_name: str = "操作"):
|
||
"""批量操作的通用方法"""
|
||
if not items:
|
||
custom_dialogs.show_warning(self, "警告", f"没有选中的项目进行{operation_name}")
|
||
return False
|
||
|
||
success_count = 0
|
||
failed_count = 0
|
||
|
||
for item in items:
|
||
try:
|
||
if operation_func(item):
|
||
success_count += 1
|
||
else:
|
||
failed_count += 1
|
||
except Exception as e:
|
||
self._log(f"Batch operation error: {e}", "DEBUG")
|
||
failed_count += 1
|
||
|
||
# 显示结果
|
||
if failed_count == 0:
|
||
custom_dialogs.show_info(self, "成功", f"{operation_name}完成,共处理 {success_count} 项")
|
||
else:
|
||
custom_dialogs.show_warning(
|
||
self, "部分成功",
|
||
f"{operation_name}完成,成功 {success_count} 项,失败 {failed_count} 项"
|
||
)
|
||
|
||
return success_count > 0
|
||
|
||
def _center_window(self):
|
||
"""将窗口居中显示在屏幕中央"""
|
||
self.update_idletasks()
|
||
|
||
# 获取窗口大小
|
||
window_width = self.winfo_width()
|
||
window_height = self.winfo_height()
|
||
|
||
# 获取屏幕大小
|
||
screen_width = self.winfo_screenwidth()
|
||
screen_height = self.winfo_screenheight()
|
||
|
||
# 计算居中位置
|
||
x = (screen_width - window_width) // 2
|
||
y = (screen_height - window_height) // 2
|
||
|
||
# 设置窗口位置
|
||
self.geometry(f"{window_width}x{window_height}+{x}+{y}")
|
||
|
||
def _set_dark_title_bar(self, window):
|
||
"""设置窗口深色标题栏(Windows 10/11)"""
|
||
try:
|
||
window.update()
|
||
hwnd = ctypes.windll.user32.GetParent(window.winfo_id())
|
||
|
||
# DWMWA_USE_IMMERSIVE_DARK_MODE = 20 (Windows 11)
|
||
# DWMWA_USE_IMMERSIVE_DARK_MODE = 19 (Windows 10 1903+)
|
||
DWMWA_USE_IMMERSIVE_DARK_MODE = 20
|
||
value = ctypes.c_int(1) # 1 = 深色模式
|
||
|
||
ctypes.windll.dwmapi.DwmSetWindowAttribute(
|
||
hwnd,
|
||
DWMWA_USE_IMMERSIVE_DARK_MODE,
|
||
ctypes.byref(value),
|
||
ctypes.sizeof(value)
|
||
)
|
||
|
||
# 如果 Windows 11 方式失败,尝试 Windows 10 方式
|
||
if ctypes.windll.dwmapi.DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, ctypes.byref(value), ctypes.sizeof(value)) != 0:
|
||
DWMWA_USE_IMMERSIVE_DARK_MODE = 19
|
||
ctypes.windll.dwmapi.DwmSetWindowAttribute(
|
||
hwnd,
|
||
DWMWA_USE_IMMERSIVE_DARK_MODE,
|
||
ctypes.byref(value),
|
||
ctypes.sizeof(value)
|
||
)
|
||
except Exception as e:
|
||
self._log(f"Failed to set dark title bar: {e}", "DEBUG")
|
||
|
||
def destroy(self):
|
||
"""销毁设置窗口前解除全局绑定,避免潜在泄漏"""
|
||
try:
|
||
# 解除全局快捷键绑定
|
||
self.unbind_all("<Delete>")
|
||
self.unbind_all("<Escape>")
|
||
except Exception as e:
|
||
self._log(f"Failed to unbind global events on destroy: {e}", "DEBUG")
|
||
# 调用父类销毁
|
||
return super().destroy()
|
||
|
||
def _create_custom_input_dialog(self, title: str, text: str, default_value: str = "") -> str:
|
||
"""Create custom input dialog (with icon and dark title bar)"""
|
||
dialog = ctk.CTkToplevel(self)
|
||
dialog.title(title)
|
||
dialog.geometry(DIALOG_INPUT_SIZE)
|
||
dialog.resizable(False, False)
|
||
|
||
# 设置图标路径
|
||
icon_path = get_icon_path()
|
||
|
||
# 第一次设置图标
|
||
if os.path.exists(icon_path):
|
||
try:
|
||
dialog.iconbitmap(icon_path)
|
||
dialog.wm_iconbitmap(icon_path)
|
||
except:
|
||
pass
|
||
|
||
# 设置为模态对话框
|
||
dialog.transient(self)
|
||
dialog.grab_set()
|
||
|
||
# 居中显示
|
||
dialog.update_idletasks()
|
||
x = (dialog.winfo_screenwidth() // 2) - (400 // 2)
|
||
y = (dialog.winfo_screenheight() // 2) - (220 // 2)
|
||
dialog.geometry(f"{DIALOG_INPUT_SIZE}+{x}+{y}")
|
||
|
||
# 设置深色标题栏和图标的组合函数
|
||
def apply_title_bar_and_icon():
|
||
# 先设置深色标题栏
|
||
self._set_dark_title_bar(dialog)
|
||
# 再次设置图标(确保不被覆盖)
|
||
if os.path.exists(icon_path):
|
||
try:
|
||
dialog.iconbitmap(icon_path)
|
||
dialog.wm_iconbitmap(icon_path)
|
||
except:
|
||
pass
|
||
|
||
# 延迟执行
|
||
dialog.after(10, apply_title_bar_and_icon)
|
||
# 再次确保图标设置(多次尝试)
|
||
dialog.after(100, lambda: dialog.iconbitmap(icon_path) if os.path.exists(icon_path) else None)
|
||
dialog.after(200, lambda: dialog.iconbitmap(icon_path) if os.path.exists(icon_path) else None)
|
||
|
||
# 存储结果
|
||
result = {"value": None}
|
||
|
||
# 提示文本
|
||
label = ctk.CTkLabel(dialog, text=text, font=ctk.CTkFont(size=13))
|
||
label.pack(pady=(30, 10), padx=20)
|
||
|
||
# 输入框
|
||
entry = ctk.CTkEntry(dialog, width=350, height=35)
|
||
entry.pack(pady=10, padx=20)
|
||
if default_value:
|
||
entry.insert(0, default_value)
|
||
entry.focus_set()
|
||
|
||
# 按钮容器
|
||
btn_frame = ctk.CTkFrame(dialog, fg_color="transparent")
|
||
btn_frame.pack(pady=20)
|
||
|
||
def on_ok():
|
||
result["value"] = entry.get()
|
||
dialog.destroy()
|
||
|
||
def on_cancel():
|
||
result["value"] = None
|
||
dialog.destroy()
|
||
|
||
# OK 按钮
|
||
ok_btn = ctk.CTkButton(
|
||
btn_frame,
|
||
text="确定",
|
||
command=on_ok,
|
||
width=150,
|
||
height=50,
|
||
font=ctk.CTkFont(size=14)
|
||
)
|
||
ok_btn.pack(side="left", padx=10)
|
||
|
||
# Cancel 按钮
|
||
cancel_btn = ctk.CTkButton(
|
||
btn_frame,
|
||
text="取消",
|
||
command=on_cancel,
|
||
width=150,
|
||
height=50,
|
||
font=ctk.CTkFont(size=14),
|
||
fg_color=BUTTON_GRAY,
|
||
hover_color=BUTTON_GRAY_HOVER
|
||
)
|
||
cancel_btn.pack(side="left", padx=10)
|
||
|
||
# 绑定回车键
|
||
entry.bind("<Return>", lambda e: on_ok())
|
||
entry.bind("<Escape>", lambda e: on_cancel())
|
||
|
||
# 等待对话框关闭
|
||
dialog.wait_window()
|
||
|
||
return result["value"]
|
||
|
||
def _create_custom_confirm_dialog(self, title: str, message: str) -> bool:
|
||
"""创建自定义确认对话框(带图标和深色标题栏)"""
|
||
dialog = ctk.CTkToplevel(self)
|
||
dialog.title(title)
|
||
dialog.geometry(DIALOG_CONFIRM_SIZE)
|
||
dialog.resizable(False, False)
|
||
|
||
# 设置图标路径
|
||
icon_path = get_icon_path()
|
||
|
||
# 第一次设置图标
|
||
if os.path.exists(icon_path):
|
||
try:
|
||
dialog.iconbitmap(icon_path)
|
||
dialog.wm_iconbitmap(icon_path)
|
||
except:
|
||
pass
|
||
|
||
# 设置为模态对话框
|
||
dialog.transient(self)
|
||
dialog.grab_set()
|
||
|
||
# 居中显示
|
||
dialog.update_idletasks()
|
||
x = (dialog.winfo_screenwidth() // 2) - (450 // 2)
|
||
y = (dialog.winfo_screenheight() // 2) - (200 // 2)
|
||
dialog.geometry(f"{DIALOG_CONFIRM_SIZE}+{x}+{y}")
|
||
|
||
# 设置深色标题栏和图标
|
||
def apply_title_bar_and_icon():
|
||
self._set_dark_title_bar(dialog)
|
||
if os.path.exists(icon_path):
|
||
try:
|
||
dialog.iconbitmap(icon_path)
|
||
dialog.wm_iconbitmap(icon_path)
|
||
except:
|
||
pass
|
||
|
||
dialog.after(10, apply_title_bar_and_icon)
|
||
dialog.after(100, lambda: dialog.iconbitmap(icon_path) if os.path.exists(icon_path) else None)
|
||
dialog.after(200, lambda: dialog.iconbitmap(icon_path) if os.path.exists(icon_path) else None)
|
||
|
||
# 存储结果
|
||
result = {"value": False}
|
||
|
||
# 消息文本
|
||
label = ctk.CTkLabel(
|
||
dialog,
|
||
text=message,
|
||
font=ctk.CTkFont(size=13),
|
||
wraplength=400
|
||
)
|
||
label.pack(pady=(40, 20), padx=20)
|
||
|
||
# 按钮容器
|
||
btn_frame = ctk.CTkFrame(dialog, fg_color="transparent")
|
||
btn_frame.pack(pady=20)
|
||
|
||
def on_yes():
|
||
result["value"] = True
|
||
dialog.destroy()
|
||
|
||
def on_no():
|
||
result["value"] = False
|
||
dialog.destroy()
|
||
|
||
# 是 按钮
|
||
yes_btn = ctk.CTkButton(
|
||
btn_frame,
|
||
text="是(Y)",
|
||
command=on_yes,
|
||
width=150,
|
||
height=50,
|
||
font=ctk.CTkFont(size=14),
|
||
fg_color=BUTTON_RED,
|
||
hover_color=BUTTON_RED_HOVER
|
||
)
|
||
yes_btn.pack(side="left", padx=10)
|
||
|
||
# 否 按钮
|
||
no_btn = ctk.CTkButton(
|
||
btn_frame,
|
||
text="否(N)",
|
||
command=on_no,
|
||
width=150,
|
||
height=50,
|
||
font=ctk.CTkFont(size=14),
|
||
fg_color=BUTTON_GRAY,
|
||
hover_color=BUTTON_GRAY_HOVER
|
||
)
|
||
no_btn.pack(side="left", padx=10)
|
||
|
||
# 绑定键盘快捷键
|
||
dialog.bind("y", lambda e: on_yes())
|
||
dialog.bind("Y", lambda e: on_yes())
|
||
dialog.bind("n", lambda e: on_no())
|
||
dialog.bind("N", lambda e: on_no())
|
||
dialog.bind("<Escape>", lambda e: on_no())
|
||
|
||
# 等待对话框关闭
|
||
dialog.wait_window()
|
||
|
||
return result["value"]
|
||
|
||
def _create_widgets(self):
|
||
"""创建界面组件"""
|
||
# 主容器
|
||
self.grid_columnconfigure(0, weight=1)
|
||
self.grid_rowconfigure(1, weight=1)
|
||
|
||
# 标题
|
||
title_label = ctk.CTkLabel(
|
||
self,
|
||
text="配置管理",
|
||
font=ctk.CTkFont(size=20, weight="bold")
|
||
)
|
||
title_label.grid(row=0, column=0, padx=20, pady=(20, 10), sticky="w")
|
||
|
||
# 主内容区域
|
||
content_frame = ctk.CTkFrame(self)
|
||
content_frame.grid(row=1, column=0, padx=20, pady=(0, 20), sticky="nsew")
|
||
content_frame.grid_columnconfigure(0, weight=1)
|
||
content_frame.grid_rowconfigure(1, weight=1)
|
||
|
||
# 项目选择区域
|
||
project_frame = ctk.CTkFrame(content_frame)
|
||
project_frame.grid(row=0, column=0, padx=10, pady=(10, 0), sticky="ew")
|
||
project_frame.grid_columnconfigure(1, weight=1)
|
||
|
||
# 第一行:项目选择
|
||
ctk.CTkLabel(
|
||
project_frame,
|
||
text="选择项目:",
|
||
font=ctk.CTkFont(size=14, weight="bold")
|
||
).grid(row=0, column=0, padx=10, pady=(10, 5), sticky="w")
|
||
|
||
self.project_combo = ctk.CTkComboBox(
|
||
project_frame,
|
||
command=self._on_project_changed,
|
||
width=200,
|
||
state="readonly" # 设置为只读,禁止输入
|
||
)
|
||
self.project_combo.grid(row=0, column=1, padx=10, pady=(10, 5), sticky="ew")
|
||
|
||
# 第二行:项目管理按钮
|
||
project_buttons_frame = ctk.CTkFrame(project_frame, fg_color="transparent")
|
||
project_buttons_frame.grid(row=1, column=0, columnspan=2, padx=10, pady=(5, 5), sticky="ew")
|
||
|
||
# 项目管理按钮
|
||
project_buttons = [
|
||
("新建项目", self._add_project, BUTTON_BLUE, BUTTON_BLUE_HOVER),
|
||
("复制项目", self._copy_project, BUTTON_BLUE, BUTTON_BLUE_HOVER),
|
||
("重命名项目", self._rename_project, BUTTON_BLUE, BUTTON_BLUE_HOVER),
|
||
("删除项目", self._delete_project, BUTTON_RED, BUTTON_RED_HOVER)
|
||
]
|
||
|
||
for text, command, fg_color, hover_color in project_buttons:
|
||
self._create_button(
|
||
project_buttons_frame, text, command,
|
||
fg_color=fg_color, hover_color=hover_color
|
||
).pack(side="left", padx=5)
|
||
|
||
# 第三行:项目图标和颜色设置
|
||
settings_frame = ctk.CTkFrame(project_frame, fg_color="transparent")
|
||
settings_frame.grid(row=2, column=0, columnspan=2, padx=10, pady=(5, 10), sticky="ew")
|
||
|
||
# 项目设置按钮
|
||
settings_buttons = [
|
||
("设置图标", self._set_project_icon),
|
||
("设置背景颜色", self._set_project_color)
|
||
]
|
||
|
||
for text, command in settings_buttons:
|
||
self._create_button(settings_frame, text, command).pack(side="left", padx=5)
|
||
|
||
# 应用列表区域 - 直接在框架内显示标题
|
||
apps_frame = ctk.CTkFrame(content_frame, fg_color="transparent")
|
||
apps_frame.grid(row=1, column=0, padx=10, pady=(0, 0), sticky="nsew")
|
||
apps_frame.grid_columnconfigure(0, weight=1)
|
||
apps_frame.grid_rowconfigure(1, weight=1)
|
||
|
||
apps_label = ctk.CTkLabel(
|
||
apps_frame,
|
||
text="应用列表:",
|
||
font=ctk.CTkFont(size=14, weight="bold")
|
||
)
|
||
apps_label.grid(row=0, column=0, padx=0, pady=(0, 0), sticky="nw")
|
||
|
||
# 滚动框架 - 底部留出空间以显示拖动指示器
|
||
self.apps_scroll_frame = ctk.CTkScrollableFrame(apps_frame)
|
||
|
||
self.apps_scroll_frame.grid(row=1, column=0, padx=0, pady=(0, 20), sticky="nsew")
|
||
|
||
# 延迟设置滚动条颜色,确保滚动条已创建
|
||
def set_scrollbar_colors():
|
||
try:
|
||
# 尝试不同的滚动条属性名称
|
||
for attr_name in ['_scrollbar', '_parent_canvas', '_scrollbar_vertical']:
|
||
if hasattr(self.apps_scroll_frame, attr_name):
|
||
scrollbar = getattr(self.apps_scroll_frame, attr_name)
|
||
if scrollbar and hasattr(scrollbar, 'configure'):
|
||
scrollbar.configure(
|
||
button_color=SCROLLBAR_COLOR,
|
||
button_hover_color=SCROLLBAR_HOVER_COLOR,
|
||
fg_color=SCROLLBAR_COLOR
|
||
)
|
||
self._log(f"Successfully set scrollbar color via {attr_name}", "DEBUG")
|
||
break
|
||
except Exception as e:
|
||
self._log(f"Failed to set scrollbar color: {e}", "DEBUG")
|
||
|
||
# 延迟执行
|
||
self.after(100, set_scrollbar_colors)
|
||
|
||
# 在空白区域右键点击时显示粘贴菜单
|
||
self.apps_scroll_frame.bind("<Button-3>", self._show_empty_area_menu)
|
||
|
||
# 在空白区域左键点击时取消选择
|
||
self.apps_scroll_frame.bind("<Button-1>", self._handle_empty_area_click)
|
||
|
||
# 添加应用按钮
|
||
add_app_btn = ctk.CTkButton(
|
||
content_frame,
|
||
text="+ 添加应用",
|
||
command=self._add_app,
|
||
height=40,
|
||
font=ctk.CTkFont(size=14, weight="bold"),
|
||
fg_color=BUTTON_GREEN,
|
||
hover_color=BUTTON_GREEN_HOVER
|
||
)
|
||
add_app_btn.grid(row=2, column=0, padx=10, pady=(5, 10), sticky="ew")
|
||
|
||
def _load_data(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._load_apps()
|
||
else:
|
||
self.project_combo.configure(values=[""])
|
||
self.project_combo.set("")
|
||
|
||
def _on_project_changed(self, choice):
|
||
"""项目切换事件"""
|
||
self._load_apps()
|
||
|
||
def _load_apps(self):
|
||
"""加载应用列表"""
|
||
# 清空选择
|
||
self.selected_items = []
|
||
self.last_selected_index = None
|
||
|
||
# 清空现有应用
|
||
for widget in self.apps_scroll_frame.winfo_children():
|
||
widget.destroy()
|
||
|
||
current_project = self.project_combo.get()
|
||
if not current_project:
|
||
return
|
||
|
||
apps = self.config_manager.get_apps(current_project)
|
||
|
||
for idx, app in enumerate(apps):
|
||
self._create_app_item(idx, app)
|
||
|
||
def _create_app_item(self, index: int, app: dict):
|
||
"""创建应用项"""
|
||
item_frame = ctk.CTkFrame(
|
||
self.apps_scroll_frame,
|
||
corner_radius=12,
|
||
fg_color=SCROLLBAR_COLOR # 设置与滑块一致的背景色
|
||
)
|
||
item_frame.pack(fill="x", padx=5, pady=3)
|
||
item_frame.grid_columnconfigure(1, weight=1)
|
||
|
||
# 存储索引信息
|
||
item_frame.app_index = index
|
||
|
||
# 设置应用项边框
|
||
item_frame.configure(border_width=1, border_color=BORDER_COLOR)
|
||
|
||
# 创建一个容器框架来定位拖动手柄,覆盖整个卡片高度
|
||
handle_container = ctk.CTkFrame(item_frame, fg_color="transparent")
|
||
handle_container.grid(row=0, column=0, rowspan=4, padx=(5, 8), pady=8, sticky="ns") # 增加rowspan为4覆盖按钮行
|
||
handle_container.grid_rowconfigure(0, weight=1) # 上方留出空间
|
||
handle_container.grid_rowconfigure(1, weight=0) # 手柄行
|
||
handle_container.grid_rowconfigure(2, weight=1) # 下方留出空间
|
||
|
||
# 添加拖放手柄 - 使用更协调的设计并垂直居中
|
||
# 创建一个背景框作为手柄的容器
|
||
drag_handle_bg = ctk.CTkFrame(
|
||
handle_container,
|
||
width=30,
|
||
height=100,
|
||
fg_color="transparent", # 透明背景,使用Canvas绘制
|
||
corner_radius=15 # 增大圆角
|
||
)
|
||
drag_handle_bg.grid(row=1, column=0, sticky="ns")
|
||
drag_handle_bg.grid_propagate(False) # 防止内容影响容器大小
|
||
drag_handle_bg.grid_rowconfigure(0, weight=1)
|
||
drag_handle_bg.grid_columnconfigure(0, weight=1)
|
||
|
||
# 使用Canvas而不是Label来显示拖动符号,以解决偏移问题
|
||
drag_handle = ctk.CTkCanvas(
|
||
drag_handle_bg,
|
||
width=30,
|
||
height=100,
|
||
bg=SCROLLBAR_COLOR, # 与滚动条颜色统一
|
||
highlightthickness=0, # 移除边框
|
||
cursor="hand2"
|
||
)
|
||
drag_handle.grid(row=0, column=0, sticky="nsew")
|
||
|
||
# 在Canvas中心绘制三条水平线
|
||
line_color = LINE_COLOR_GRAY
|
||
line_width = 18 # 线条长度
|
||
line_height = 3 # 线条高度
|
||
line_spacing = 7 # 线条间距
|
||
|
||
# 计算中心位置
|
||
center_x = 15
|
||
center_y = 50
|
||
|
||
# 计算第一条线的位置(三条线的中心位置)
|
||
start_y = center_y - line_spacing
|
||
|
||
# 绘制三条线并保存线条ID
|
||
line_ids = []
|
||
for i in range(3):
|
||
y_pos = start_y + i * line_spacing
|
||
line_id = drag_handle.create_line(
|
||
center_x - line_width/2, y_pos,
|
||
center_x + line_width/2, y_pos,
|
||
fill=line_color,
|
||
width=line_height,
|
||
capstyle="round" # 圆形线条端点
|
||
)
|
||
line_ids.append(line_id)
|
||
|
||
# 在item_frame上保存拖动手柄的引用,用于后续更新颜色
|
||
item_frame.drag_handle = drag_handle
|
||
item_frame.drag_handle_line_ids = line_ids
|
||
|
||
# 绑定拖放事件到手柄文本
|
||
drag_handle.bind("<Button-1>", lambda e, idx=index, frame=item_frame: self._start_drag(e, idx, frame))
|
||
drag_handle.bind("<B1-Motion>", self._on_drag_motion)
|
||
drag_handle.bind("<ButtonRelease-1>", self._end_drag)
|
||
|
||
# 绑定左键点击和拖动事件到卡片本身
|
||
item_frame.bind("<Button-1>", lambda e, idx=index, frame=item_frame: self._on_item_click_or_drag_start(e, idx, frame))
|
||
item_frame.bind("<B1-Motion>", lambda e, idx=index, frame=item_frame: self._on_item_drag_motion(e, idx, frame))
|
||
item_frame.bind("<ButtonRelease-1>", self._on_item_drag_end)
|
||
|
||
# 绑定右键菜单(不阻止事件传播,这样空白区域也能触发)
|
||
item_frame.bind("<Button-3>", lambda e, idx=index: self._show_context_menu(e, idx), add="+")
|
||
|
||
# 绑定拖放事件到手柄背景框
|
||
drag_handle_bg.bind("<Button-1>", lambda e, idx=index, frame=item_frame: self._start_drag(e, idx, frame))
|
||
drag_handle_bg.bind("<B1-Motion>", self._on_drag_motion)
|
||
drag_handle_bg.bind("<ButtonRelease-1>", self._end_drag)
|
||
|
||
# 同时绑定拖放事件到手柄外层容器
|
||
handle_container.bind("<Button-1>", lambda e, idx=index, frame=item_frame: self._start_drag(e, idx, frame))
|
||
handle_container.bind("<B1-Motion>", self._on_drag_motion)
|
||
handle_container.bind("<ButtonRelease-1>", self._end_drag)
|
||
|
||
# 应用名称
|
||
name_label = ctk.CTkLabel(
|
||
item_frame,
|
||
text=f"名称: {app['name']}",
|
||
font=ctk.CTkFont(size=12, weight="bold")
|
||
)
|
||
name_label.grid(row=0, column=1, columnspan=3, padx=10, pady=(8, 2), sticky="w")
|
||
name_label.bind("<Button-3>", lambda e, idx=index: self._show_context_menu(e, idx))
|
||
|
||
# 路径
|
||
path_label = ctk.CTkLabel(
|
||
item_frame,
|
||
text=f"路径: {app['path']}",
|
||
font=ctk.CTkFont(size=11)
|
||
)
|
||
path_label.grid(row=1, column=1, columnspan=3, padx=10, pady=1, sticky="w")
|
||
path_label.bind("<Button-3>", lambda e, idx=index: self._show_context_menu(e, idx))
|
||
|
||
# 版本
|
||
version_label = ctk.CTkLabel(
|
||
item_frame,
|
||
text=f"版本: {app['version']}",
|
||
font=ctk.CTkFont(size=11)
|
||
)
|
||
version_label.grid(row=2, column=1, columnspan=3, padx=10, pady=(1, 6), sticky="w")
|
||
version_label.bind("<Button-3>", lambda e, idx=index: self._show_context_menu(e, idx))
|
||
|
||
# 按钮行容器
|
||
button_frame = ctk.CTkFrame(item_frame, fg_color="transparent")
|
||
button_frame.grid(row=3, column=1, columnspan=3, padx=10, pady=(0, 8), sticky="e")
|
||
|
||
# 编辑按钮
|
||
ctk.CTkButton(
|
||
button_frame,
|
||
text="编辑",
|
||
command=lambda: self._edit_app(index, app),
|
||
width=80,
|
||
font=ctk.CTkFont(size=12),
|
||
fg_color=BUTTON_BLUE,
|
||
hover_color=BUTTON_BLUE_HOVER
|
||
).pack(side="left", padx=5)
|
||
|
||
# 删除按钮
|
||
ctk.CTkButton(
|
||
button_frame,
|
||
text="删除",
|
||
command=lambda: self._delete_app(index),
|
||
width=80,
|
||
font=ctk.CTkFont(size=12),
|
||
fg_color=BUTTON_RED,
|
||
hover_color=BUTTON_RED_HOVER
|
||
).pack(side="left", padx=5)
|
||
|
||
def _add_project(self):
|
||
"""添加项目"""
|
||
# 使用自定义输入对话框
|
||
project_name = self._create_custom_input_dialog(
|
||
title="新建项目",
|
||
text="请输入项目名称:"
|
||
)
|
||
|
||
if project_name:
|
||
# 设置默认图标为 NexusLauncher.ico(只保存文件名,不保存完整路径)
|
||
default_icon = "NexusLauncher.ico"
|
||
if self.config_manager.add_project(project_name, default_icon):
|
||
# 先通知主窗口更新(刷新项目列表)
|
||
self.on_update()
|
||
# 再刷新设置窗口的数据
|
||
self._load_data()
|
||
# 自动切换到新建的项目
|
||
self.project_combo.set(project_name)
|
||
self._load_apps() # 刷新应用列表
|
||
|
||
# 检查新项目的应用数量
|
||
apps = self.config_manager.get_apps(project_name)
|
||
|
||
# 确保键盘事件在新项目创建后仍然有效
|
||
self.focus_set()
|
||
self.focus_force()
|
||
|
||
# 显示成功消息
|
||
custom_dialogs.show_info(self, "成功", f"项目 '{project_name}' 已创建")
|
||
else:
|
||
custom_dialogs.show_error(self, "错误", "项目已存在或创建失败")
|
||
|
||
def _delete_project(self):
|
||
"""删除项目"""
|
||
current_project = self._ensure_project_selected()
|
||
if not current_project:
|
||
return
|
||
|
||
# 使用自定义确认对话框
|
||
result = self._create_custom_confirm_dialog(
|
||
title="确认删除",
|
||
message=f"确定要删除项目 '{current_project}' 吗?\n这将删除该项目下的所有应用配置。"
|
||
)
|
||
|
||
if result:
|
||
if self.config_manager.delete_project(current_project):
|
||
custom_dialogs.show_info(self, "成功", "项目删除成功")
|
||
self._load_data()
|
||
self.on_update()
|
||
else:
|
||
custom_dialogs.show_error(self, "错误", "删除项目失败")
|
||
|
||
def _rename_project(self):
|
||
"""重命名项目"""
|
||
current_project = self._ensure_project_selected()
|
||
if not current_project:
|
||
return
|
||
|
||
# 使用自定义输入对话框
|
||
new_name = self._create_custom_input_dialog(
|
||
title="重命名项目",
|
||
text=f"请输入项目 '{current_project}' 的新名称:",
|
||
default_value=current_project
|
||
)
|
||
|
||
if new_name:
|
||
if self.config_manager.rename_project(current_project, new_name):
|
||
# 先通知主窗口更新(刷新项目列表)
|
||
self.on_update()
|
||
# 再刷新设置窗口的数据
|
||
self._load_data()
|
||
# 自动切换到重命名后的项目
|
||
self.project_combo.set(new_name)
|
||
self._load_apps() # 刷新应用列表
|
||
# 显示成功消息
|
||
custom_dialogs.show_info(self, "成功", f"项目重命名为 '{new_name}'")
|
||
else:
|
||
custom_dialogs.show_error(self, "错误", "重命名项目失败,可能名称重复")
|
||
elif new_name == current_project:
|
||
custom_dialogs.show_info(self, "成功", "新名称与原名称相同,无需修改")
|
||
|
||
def _copy_project(self):
|
||
"""复制项目"""
|
||
current_project = self._ensure_project_selected()
|
||
if not current_project:
|
||
return
|
||
|
||
# 生成默认新项目名称(自动添加 _01, _02 等后缀)
|
||
base_name = current_project
|
||
counter = 1
|
||
default_name = f"{base_name}_01"
|
||
|
||
# 检查名称是否已存在,如果存在则递增数字
|
||
existing_projects = self.config_manager.get_projects()
|
||
while default_name in existing_projects:
|
||
counter += 1
|
||
default_name = f"{base_name}_{counter:02d}"
|
||
|
||
# 弹出对话框让用户输入新项目名称
|
||
new_project_name = custom_dialogs.ask_string(
|
||
self,
|
||
"复制项目",
|
||
f"请输入新项目名称:",
|
||
default_name
|
||
)
|
||
|
||
if not new_project_name:
|
||
return # 用户取消
|
||
|
||
if new_project_name in existing_projects:
|
||
custom_dialogs.show_error(self, "错误", "项目名称已存在,请使用其他名称")
|
||
return
|
||
|
||
# 创建新项目(设置默认图标,只保存文件名)
|
||
default_icon = "NexusLauncher.ico"
|
||
if self.config_manager.add_project(new_project_name, default_icon):
|
||
# 复制源项目的图标和颜色(如果有)
|
||
source_icon = self.config_manager.get_project_icon(current_project)
|
||
if source_icon:
|
||
self.config_manager.set_project_icon(new_project_name, source_icon)
|
||
|
||
source_color = self.config_manager.get_project_color(current_project)
|
||
if source_color:
|
||
self.config_manager.set_project_color(new_project_name, source_color)
|
||
|
||
# 获取源项目的所有应用
|
||
source_apps = self.config_manager.get_apps(current_project)
|
||
|
||
# 复制所有应用到新项目
|
||
for app in source_apps:
|
||
# 添加应用
|
||
self.config_manager.add_app(
|
||
new_project_name,
|
||
app['name'],
|
||
app['path'],
|
||
app['version']
|
||
)
|
||
|
||
# 复制图标设置
|
||
icon = self.config_manager.get_app_icon(app['path'])
|
||
if icon:
|
||
self.config_manager.set_app_icon(app['path'], icon)
|
||
|
||
# 复制颜色设置
|
||
color = self.config_manager.get_app_color(app['path'])
|
||
if color:
|
||
self.config_manager.set_app_color(app['path'], color)
|
||
|
||
# 复制 task_settings(直接从 config_data 复制原始数据,避免格式转换)
|
||
if (current_project in self.config_manager.config_data.get("projects", {}) and
|
||
"task_settings" in self.config_manager.config_data["projects"][current_project]):
|
||
|
||
# 直接复制原始的 task_settings(保持 JSON 中的正斜杠格式)
|
||
source_task_settings = self.config_manager.config_data["projects"][current_project]["task_settings"]
|
||
|
||
# 确保新项目结构存在
|
||
if not self.config_manager.config_data.get("projects"):
|
||
self.config_manager.config_data["projects"] = {}
|
||
if new_project_name not in self.config_manager.config_data["projects"]:
|
||
self.config_manager.config_data["projects"][new_project_name] = {}
|
||
|
||
# 深拷贝 task_settings 到新项目
|
||
import copy
|
||
self.config_manager.config_data["projects"][new_project_name]["task_settings"] = copy.deepcopy(source_task_settings)
|
||
|
||
# 保存配置
|
||
self.config_manager.save_config()
|
||
|
||
# 先通知主窗口更新(刷新项目列表)
|
||
self.on_update()
|
||
# 再刷新设置窗口的数据
|
||
self._load_data()
|
||
# 自动切换到新复制的项目
|
||
self.project_combo.set(new_project_name)
|
||
self._load_apps() # 刷新应用列表
|
||
# 显示成功消息
|
||
custom_dialogs.show_info(
|
||
self,
|
||
"成功",
|
||
f"已复制项目 '{current_project}' 到 '{new_project_name}'\n共复制了 {len(source_apps)} 个应用"
|
||
)
|
||
else:
|
||
custom_dialogs.show_error(self, "错误", "复制项目失败")
|
||
|
||
def _add_app(self):
|
||
"""添加应用"""
|
||
current_project = self._ensure_project_selected()
|
||
if not current_project:
|
||
return
|
||
|
||
self._show_app_dialog(current_project)
|
||
|
||
def _edit_app(self, index: int, app: dict):
|
||
"""编辑应用"""
|
||
current_project = self.project_combo.get()
|
||
if not current_project:
|
||
return
|
||
|
||
self._show_app_dialog(current_project, index, app)
|
||
|
||
def _delete_app(self, index: int):
|
||
"""删除应用"""
|
||
current_project = self.project_combo.get()
|
||
if not current_project:
|
||
return
|
||
|
||
result = custom_dialogs.ask_yes_no(self, "确认删除", "确定要删除这个应用吗?")
|
||
|
||
if result:
|
||
if self.config_manager.delete_app(current_project, index):
|
||
custom_dialogs.show_info(self, "成功", "应用已删除")
|
||
self._load_apps()
|
||
self.on_update()
|
||
else:
|
||
custom_dialogs.show_error(self, "错误", "删除应用失败")
|
||
|
||
def _start_drag(self, event, index, frame):
|
||
"""开始拖放"""
|
||
self.drag_source = index
|
||
self.drag_widget = frame
|
||
|
||
# 记录鼠标相对于组件的位置
|
||
self.drag_data = (event.x_root, event.y_root, frame.winfo_y())
|
||
|
||
# 改变组件外观,表示正在拖动
|
||
frame.configure(border_width=2, border_color=DRAG_HIGHLIGHT_COLOR, fg_color=DRAG_HIGHLIGHT_BG)
|
||
|
||
# 更新拖动手柄的颜色
|
||
if hasattr(frame, 'drag_handle') and hasattr(frame, 'drag_handle_line_ids'):
|
||
# 更改Canvas背景色为与卡片一致的高亮色
|
||
frame.drag_handle.configure(bg=DRAG_HIGHLIGHT_BG)
|
||
# 更改线条颜色为高亮色
|
||
for line_id in frame.drag_handle_line_ids:
|
||
frame.drag_handle.itemconfig(line_id, fill=DRAG_HIGHLIGHT_COLOR)
|
||
|
||
# 创建拖动时的光标指示器
|
||
if not hasattr(self, 'insert_indicator') or not self.insert_indicator.winfo_exists():
|
||
self.insert_indicator = ctk.CTkFrame(self.apps_scroll_frame, height=4, fg_color=DRAG_HIGHLIGHT_COLOR, corner_radius=2)
|
||
|
||
# 阻止事件传播到item_frame,避免触发多选逻辑
|
||
return "break"
|
||
|
||
def _on_drag_motion(self, event):
|
||
"""拖放过程中"""
|
||
if not self.drag_data or not self.drag_widget:
|
||
return
|
||
|
||
# 计算移动距离
|
||
x_origin, y_origin, y_start = self.drag_data
|
||
y_offset = event.y_root - y_origin
|
||
new_y = y_start + y_offset
|
||
|
||
# 获取所有应用项,排除插入指示器
|
||
all_children = self.apps_scroll_frame.winfo_children()
|
||
app_frames = [f for f in all_children if f != getattr(self, 'insert_indicator', None)]
|
||
if not app_frames:
|
||
return
|
||
|
||
# 简化的拖动逻辑
|
||
target_index = self.drag_source
|
||
insert_y = 0
|
||
insert_at_end = False
|
||
found = False
|
||
|
||
# 遍历所有卡片,找到鼠标位置对应的目标
|
||
for i, frame in enumerate(app_frames):
|
||
if i == self.drag_source:
|
||
continue
|
||
|
||
frame_y = frame.winfo_y()
|
||
frame_h = frame.winfo_height()
|
||
frame_mid = frame_y + frame_h / 2
|
||
|
||
# 如果鼠标在这张卡片的上半部分,插入到它之前
|
||
if new_y < frame_mid:
|
||
target_index = i
|
||
# 确保指示器不会超出可视区域
|
||
insert_y = max(10, frame_y - 4)
|
||
found = True
|
||
break
|
||
|
||
# 如果没有找到插入位置,说明要插入到末尾
|
||
if not found:
|
||
# 找到最后一张非拖动卡片
|
||
last_frame = None
|
||
for i in range(len(app_frames) - 1, -1, -1):
|
||
if i != self.drag_source:
|
||
last_frame = app_frames[i]
|
||
break
|
||
|
||
if last_frame:
|
||
target_index = len(app_frames)
|
||
# 将指示器放在最后一张卡片内部的底部,而不是下方
|
||
frame_bottom = last_frame.winfo_y() + last_frame.winfo_height()
|
||
# 指示器显示在卡片底部边缘
|
||
insert_y = frame_bottom - 2
|
||
insert_at_end = True
|
||
|
||
if target_index != self.drag_target:
|
||
self.drag_target = target_index
|
||
|
||
# 重置所有应用项的外观
|
||
for i, frame in enumerate(app_frames):
|
||
if i != self.drag_source:
|
||
frame.configure(border_width=1, border_color=BORDER_COLOR)
|
||
|
||
# 显示插入指示器
|
||
try:
|
||
indicator_width = self.apps_scroll_frame.winfo_width() - 20
|
||
self.insert_indicator.configure(width=indicator_width)
|
||
self.insert_indicator.place(x=10, y=insert_y - 4)
|
||
self.insert_indicator.lift()
|
||
self.insert_indicator.configure(fg_color=DRAG_HIGHLIGHT_COLOR, border_width=1, border_color=BORDER_COLOR_WHITE)
|
||
|
||
except Exception:
|
||
pass
|
||
|
||
def _end_drag(self, event):
|
||
"""结束拖放"""
|
||
if self.drag_source is not None and self.drag_target is not None and self.drag_source != self.drag_target:
|
||
current_project = self.project_combo.get()
|
||
if current_project:
|
||
# 重新排序应用
|
||
if self.config_manager.reorder_apps(current_project, self.drag_source, self.drag_target):
|
||
self._load_apps()
|
||
self.on_update()
|
||
|
||
# 隐藏插入指示器
|
||
try:
|
||
if hasattr(self, 'insert_indicator') and self.insert_indicator.winfo_exists():
|
||
if self.insert_indicator.winfo_ismapped():
|
||
self.insert_indicator.place_forget()
|
||
except Exception as e:
|
||
self._log(f"Error hiding indicator on end drag: {e}", "DEBUG")
|
||
|
||
# 重置所有应用项的外观
|
||
for frame in self.apps_scroll_frame.winfo_children():
|
||
if frame != getattr(self, 'insert_indicator', None):
|
||
try:
|
||
frame.configure(border_width=1, border_color=BORDER_COLOR, fg_color="transparent")
|
||
# 恢复拖动手柄的颜色
|
||
if hasattr(frame, 'drag_handle') and hasattr(frame, 'drag_handle_line_ids'):
|
||
frame.drag_handle.configure(bg=SCROLLBAR_COLOR) # 与滚动条统一
|
||
for line_id in frame.drag_handle_line_ids:
|
||
frame.drag_handle.itemconfig(line_id, fill=LINE_COLOR_GRAY)
|
||
except Exception:
|
||
pass
|
||
|
||
# 重置拖放状态
|
||
self.drag_source = None
|
||
self.drag_target = None
|
||
self.drag_data = None
|
||
self.drag_widget = None
|
||
|
||
def _show_app_dialog(self, project_name: str, index: int = -1, app: dict = None):
|
||
"""显示应用编辑对话框"""
|
||
dialog = ctk.CTkToplevel(self)
|
||
dialog.title("编辑应用" if app else "添加应用")
|
||
dialog.geometry(DIALOG_APP_EDIT_SIZE)
|
||
|
||
# 设置对话框背景颜色(与设置窗口一致)
|
||
dialog.configure(fg_color=DIALOG_BG_COLOR)
|
||
|
||
# 设置窗口图标(在transient之前)
|
||
icon_path = get_icon_path()
|
||
if os.path.exists(icon_path):
|
||
try:
|
||
dialog.iconbitmap(icon_path)
|
||
dialog.wm_iconbitmap(icon_path)
|
||
except Exception:
|
||
pass
|
||
|
||
dialog.transient(self)
|
||
dialog.grab_set()
|
||
|
||
# 设置深色标题栏(Windows 10/11)
|
||
dialog.after(10, lambda: self._set_dark_title_bar(dialog))
|
||
|
||
# 居中显示
|
||
dialog.update_idletasks()
|
||
x = (dialog.winfo_screenwidth() // 2) - (650 // 2)
|
||
y = (dialog.winfo_screenheight() // 2) - (700 // 2)
|
||
dialog.geometry(f"{DIALOG_APP_EDIT_SIZE}+{x}+{y}")
|
||
|
||
# 再次尝试设置图标(确保生效)
|
||
if os.path.exists(icon_path):
|
||
def set_icon_delayed():
|
||
try:
|
||
dialog.iconbitmap(icon_path)
|
||
dialog.wm_iconbitmap(icon_path)
|
||
except Exception:
|
||
pass
|
||
dialog.after(50, set_icon_delayed)
|
||
dialog.after(200, set_icon_delayed)
|
||
|
||
# 表单
|
||
form_frame = ctk.CTkFrame(dialog)
|
||
form_frame.pack(fill="both", expand=True, padx=20, pady=20)
|
||
|
||
# 获取图标文件夹路径
|
||
icons_dir = get_icons_dir()
|
||
|
||
# 应用名称
|
||
ctk.CTkLabel(form_frame, text="应用名称:", font=ctk.CTkFont(size=12)).pack(anchor="w", pady=(10, 5))
|
||
name_entry = ctk.CTkEntry(form_frame, placeholder_text="例如: 记事本")
|
||
name_entry.pack(fill="x", pady=(0, 10))
|
||
if app:
|
||
name_entry.insert(0, app['name'])
|
||
|
||
# 应用路径
|
||
ctk.CTkLabel(form_frame, text="应用路径:", font=ctk.CTkFont(size=12)).pack(anchor="w", pady=(10, 5))
|
||
path_frame = ctk.CTkFrame(form_frame)
|
||
path_frame.pack(fill="x", pady=(0, 10))
|
||
|
||
path_entry = ctk.CTkEntry(path_frame, placeholder_text="例如: C:\\Program Files\\app.exe")
|
||
path_entry.pack(side="left", fill="x", expand=True, padx=(0, 10))
|
||
if app:
|
||
# 显示时将正斜杠转换为反斜杠(UI 显示格式)
|
||
display_path = app['path'].replace("/", "\\")
|
||
path_entry.insert(0, display_path)
|
||
|
||
def browse_file():
|
||
filename = filedialog.askopenfilename(
|
||
title="选择应用程序",
|
||
filetypes=[("可执行文件", "*.exe"), ("所有文件", "*.*")]
|
||
)
|
||
if filename:
|
||
# 将选择的路径标准化为反斜杠(UI 显示格式)
|
||
normalized_path = filename.replace("/", "\\")
|
||
path_entry.delete(0, "end")
|
||
path_entry.insert(0, normalized_path)
|
||
|
||
ctk.CTkButton(path_frame, text="浏览", command=browse_file, width=80).pack(side="right")
|
||
|
||
# 版本号
|
||
ctk.CTkLabel(form_frame, text="版本号:", font=ctk.CTkFont(size=12)).pack(anchor="w", pady=(10, 5))
|
||
version_entry = ctk.CTkEntry(form_frame, placeholder_text="例如: 1.0.0")
|
||
version_entry.pack(fill="x", pady=(0, 10))
|
||
if app:
|
||
version_entry.insert(0, app['version'])
|
||
|
||
# 图标选择
|
||
ctk.CTkLabel(form_frame, text="选择图标:", font=ctk.CTkFont(size=12)).pack(anchor="w", pady=(10, 5))
|
||
|
||
icon_frame = ctk.CTkFrame(form_frame)
|
||
icon_frame.pack(fill="x", pady=(0, 10))
|
||
|
||
# 默认图标路径(使用 NexusLauncher.ico 作为默认图标)
|
||
default_icon_path = get_icon_path()
|
||
selected_icon = ctk.StringVar(value=default_icon_path if os.path.exists(default_icon_path) else "")
|
||
|
||
# 如果已有图标设置,则加载
|
||
if app and app['path']:
|
||
custom_icon = self.config_manager.get_app_icon(app['path'])
|
||
if custom_icon and os.path.exists(custom_icon):
|
||
selected_icon.set(custom_icon)
|
||
|
||
# 显示当前选择的图标
|
||
icon_preview_frame = ctk.CTkFrame(icon_frame)
|
||
icon_preview_frame.pack(side="left", padx=(0, 10))
|
||
|
||
icon_preview_label = ctk.CTkLabel(icon_preview_frame, text="无图标", image=None)
|
||
icon_preview_label.pack(padx=10, pady=10)
|
||
|
||
# 更新图标预览
|
||
def update_icon_preview(icon_path=None):
|
||
if not icon_path and selected_icon.get():
|
||
icon_path = selected_icon.get()
|
||
|
||
if icon_path and os.path.exists(icon_path):
|
||
try:
|
||
# 加载图标并调整大小
|
||
img = Image.open(icon_path)
|
||
img = img.resize((48, 48), Image.Resampling.LANCZOS)
|
||
ctk_img = ctk.CTkImage(light_image=img, dark_image=img, size=(48, 48))
|
||
icon_preview_label.configure(image=ctk_img, text="") # 清除文字
|
||
icon_preview_label._image = ctk_img # 保持引用以防止垃圾回收
|
||
except Exception as e:
|
||
self._log(f"Failed to load icon preview: {e}", "DEBUG")
|
||
icon_preview_label.configure(image=None, text="Preview failed")
|
||
else:
|
||
icon_preview_label.configure(image=None, text="No icon")
|
||
|
||
# 初始化预览
|
||
update_icon_preview()
|
||
|
||
# 图标选择按钮
|
||
icon_buttons_frame = ctk.CTkFrame(icon_frame, fg_color="transparent")
|
||
icon_buttons_frame.pack(side="left", fill="x", expand=True)
|
||
|
||
def browse_icon():
|
||
filename = filedialog.askopenfilename(
|
||
title="选择图标文件",
|
||
filetypes=[("图标文件", "*.png;*.ico;*.jpg;*.jpeg"), ("所有文件", "*.*")]
|
||
)
|
||
if filename:
|
||
selected_icon.set(filename)
|
||
update_icon_preview(filename)
|
||
|
||
def select_preset_icon():
|
||
# 创建预设图标选择对话框
|
||
icon_dialog = ctk.CTkToplevel(dialog)
|
||
icon_dialog.title("选择预设图标")
|
||
icon_dialog.geometry(DIALOG_ICON_SELECT_SIZE)
|
||
|
||
# 设置窗口图标(在transient之前)
|
||
icon_path = get_icon_path()
|
||
if os.path.exists(icon_path):
|
||
try:
|
||
icon_dialog.iconbitmap(icon_path)
|
||
icon_dialog.wm_iconbitmap(icon_path)
|
||
except Exception:
|
||
pass
|
||
|
||
icon_dialog.transient(dialog)
|
||
icon_dialog.grab_set()
|
||
|
||
# 再次尝试设置图标(确保生效)
|
||
if os.path.exists(icon_path):
|
||
def set_icon_delayed():
|
||
try:
|
||
icon_dialog.iconbitmap(icon_path)
|
||
icon_dialog.wm_iconbitmap(icon_path)
|
||
except Exception:
|
||
pass
|
||
icon_dialog.after(50, set_icon_delayed)
|
||
icon_dialog.after(200, set_icon_delayed)
|
||
|
||
# 获取所有预设图标
|
||
preset_icons = glob.glob(os.path.join(icons_dir, "*.png"))
|
||
preset_icons.extend(glob.glob(os.path.join(icons_dir, "*.ico")))
|
||
|
||
# 创建滚动区域
|
||
scroll_frame = ctk.CTkScrollableFrame(
|
||
icon_dialog,
|
||
scrollbar_button_color=SCROLLBAR_COLOR,
|
||
scrollbar_button_hover_color=SCROLLBAR_HOVER_COLOR
|
||
)
|
||
scroll_frame.pack(fill="both", expand=True, padx=20, pady=20)
|
||
|
||
# 创建网格布局
|
||
row, col = 0, 0
|
||
max_cols = 5
|
||
|
||
for icon_path in preset_icons:
|
||
try:
|
||
# 创建图标项
|
||
icon_item = ctk.CTkFrame(scroll_frame)
|
||
icon_item.grid(row=row, column=col, padx=5, pady=5)
|
||
|
||
# 加载图标
|
||
img = Image.open(icon_path)
|
||
img = img.resize((48, 48), Image.Resampling.LANCZOS)
|
||
ctk_img = ctk.CTkImage(light_image=img, dark_image=img, size=(48, 48))
|
||
|
||
# 创建按钮
|
||
def make_select_cmd(path):
|
||
return lambda: select_icon(path)
|
||
|
||
icon_btn = ctk.CTkButton(
|
||
icon_item,
|
||
text="",
|
||
image=ctk_img,
|
||
command=make_select_cmd(icon_path),
|
||
width=60,
|
||
height=60
|
||
)
|
||
icon_btn.pack(padx=5, pady=5)
|
||
icon_btn._image = ctk_img # 保持引用
|
||
|
||
# 显示图标名称
|
||
icon_name = os.path.splitext(os.path.basename(icon_path))[0]
|
||
ctk.CTkLabel(
|
||
icon_item,
|
||
text=icon_name,
|
||
font=ctk.CTkFont(size=10)
|
||
).pack(pady=(0, 5))
|
||
|
||
# 更新行列位置
|
||
col += 1
|
||
if col >= max_cols:
|
||
col = 0
|
||
row += 1
|
||
except Exception as e:
|
||
self._log(f"Failed to load preset icon {icon_path}: {e}", "DEBUG")
|
||
|
||
def select_icon(path):
|
||
selected_icon.set(path)
|
||
update_icon_preview(path)
|
||
icon_dialog.destroy()
|
||
|
||
# 取消按钮
|
||
ctk.CTkButton(
|
||
icon_dialog,
|
||
text="取消",
|
||
command=icon_dialog.destroy,
|
||
width=100
|
||
).pack(pady=10)
|
||
|
||
# 图标选择按钮
|
||
ctk.CTkButton(
|
||
icon_buttons_frame,
|
||
text="选择预设图标",
|
||
command=select_preset_icon
|
||
).pack(side="left", padx=5, pady=5)
|
||
|
||
ctk.CTkButton(
|
||
icon_buttons_frame,
|
||
text="浏览本地图标",
|
||
command=browse_icon
|
||
).pack(side="left", padx=5, pady=5)
|
||
|
||
ctk.CTkButton(
|
||
icon_buttons_frame,
|
||
text="清除图标",
|
||
command=lambda: [selected_icon.set(""), update_icon_preview()]
|
||
).pack(side="left", padx=5, pady=5)
|
||
|
||
# 按钮颜色选择
|
||
ctk.CTkLabel(form_frame, text="按钮颜色:", font=ctk.CTkFont(size=12)).pack(anchor="w", pady=(10, 5))
|
||
|
||
color_frame = ctk.CTkFrame(form_frame)
|
||
color_frame.pack(fill="x", pady=(0, 10))
|
||
|
||
# 默认颜色(使用预设颜色列表中的第一个)
|
||
default_color = PRESET_COLORS[0] # 蓝灰色
|
||
selected_color = ctk.StringVar(value=default_color)
|
||
|
||
# 如果已有颜色设置,则加载
|
||
if app and app['path']:
|
||
custom_color = self.config_manager.get_app_color(app['path'])
|
||
if custom_color:
|
||
selected_color.set(custom_color)
|
||
|
||
# 颜色预览框
|
||
color_preview_frame = ctk.CTkFrame(color_frame, width=60, height=60, corner_radius=10)
|
||
color_preview_frame.pack(side="left", padx=(0, 10))
|
||
color_preview_frame.pack_propagate(False)
|
||
|
||
color_preview_label = ctk.CTkLabel(color_preview_frame, text="", width=60, height=60, corner_radius=10)
|
||
color_preview_label.pack(fill="both", expand=True)
|
||
|
||
# 更新颜色预览
|
||
def update_color_preview(color=None):
|
||
if not color and selected_color.get():
|
||
color = selected_color.get()
|
||
|
||
if color:
|
||
color_preview_label.configure(fg_color=color, text="")
|
||
else:
|
||
color_preview_label.configure(fg_color="transparent", text="无颜色")
|
||
|
||
# 初始化预览
|
||
update_color_preview()
|
||
|
||
# 使用配置文件中的预设颜色列表
|
||
preset_colors = PRESET_COLORS
|
||
|
||
# 颜色选择按钮容器
|
||
color_buttons_frame = ctk.CTkFrame(color_frame, fg_color="transparent")
|
||
color_buttons_frame.pack(side="left", fill="x", expand=True)
|
||
|
||
# 创建颜色按钮网格
|
||
color_grid_frame = ctk.CTkFrame(color_buttons_frame, fg_color="transparent")
|
||
color_grid_frame.pack(fill="x", pady=5)
|
||
|
||
for idx, color in enumerate(preset_colors):
|
||
row = idx // 6
|
||
col = idx % 6
|
||
|
||
def make_color_cmd(c):
|
||
return lambda: [selected_color.set(c), update_color_preview(c)]
|
||
|
||
color_btn = ctk.CTkButton(
|
||
color_grid_frame,
|
||
text="",
|
||
width=35,
|
||
height=35,
|
||
fg_color=color,
|
||
hover_color=color,
|
||
command=make_color_cmd(color),
|
||
corner_radius=8
|
||
)
|
||
color_btn.grid(row=row, column=col, padx=3, pady=3)
|
||
|
||
# 自定义颜色和清除按钮
|
||
custom_color_frame = ctk.CTkFrame(color_buttons_frame, fg_color="transparent")
|
||
custom_color_frame.pack(fill="x", pady=5)
|
||
|
||
def choose_custom_color():
|
||
from tkinter import colorchooser
|
||
color = colorchooser.askcolor(title="选择颜色")
|
||
if color and color[1]:
|
||
selected_color.set(color[1])
|
||
update_color_preview(color[1])
|
||
|
||
ctk.CTkButton(
|
||
custom_color_frame,
|
||
text="自定义颜色",
|
||
command=choose_custom_color,
|
||
width=120
|
||
).pack(side="left", padx=5)
|
||
|
||
ctk.CTkButton(
|
||
custom_color_frame,
|
||
text="清除颜色",
|
||
command=lambda: [selected_color.set(""), update_color_preview()],
|
||
width=100
|
||
).pack(side="left", padx=5)
|
||
|
||
# 按钮
|
||
btn_frame = ctk.CTkFrame(form_frame)
|
||
btn_frame.pack(fill="x", pady=(10, 0))
|
||
|
||
def save_app():
|
||
name = name_entry.get().strip()
|
||
path = path_entry.get().strip()
|
||
version = version_entry.get().strip()
|
||
|
||
if not name or not path or not version:
|
||
custom_dialogs.show_warning(dialog, "警告", "请填写所有字段")
|
||
return
|
||
|
||
# 将路径转换为正斜杠格式(JSON 存储格式)
|
||
path = path.replace("\\", "/")
|
||
|
||
# 保存图标设置
|
||
icon_path = selected_icon.get()
|
||
# 保存颜色设置
|
||
color = selected_color.get()
|
||
|
||
if index >= 0:
|
||
# 更新
|
||
if self.config_manager.update_app(project_name, index, name, path, version):
|
||
# 设置图标
|
||
if icon_path:
|
||
self.config_manager.set_app_icon(path, icon_path)
|
||
else:
|
||
self.config_manager.remove_app_icon(path)
|
||
|
||
# 设置颜色
|
||
if color:
|
||
self.config_manager.set_app_color(path, color)
|
||
else:
|
||
self.config_manager.remove_app_color(path)
|
||
|
||
custom_dialogs.show_info(dialog, "成功", "应用已更新")
|
||
dialog.destroy()
|
||
self._load_apps()
|
||
self.on_update()
|
||
else:
|
||
custom_dialogs.show_error(dialog, "错误", "更新应用失败")
|
||
else:
|
||
# 添加
|
||
if self.config_manager.add_app(project_name, name, path, version):
|
||
# 设置图标
|
||
if icon_path:
|
||
self.config_manager.set_app_icon(path, icon_path)
|
||
|
||
# 设置颜色
|
||
if color:
|
||
self.config_manager.set_app_color(path, color)
|
||
|
||
custom_dialogs.show_info(dialog, "成功", "应用已添加")
|
||
dialog.destroy()
|
||
self._load_apps()
|
||
self.on_update()
|
||
else:
|
||
custom_dialogs.show_error(dialog, "错误", "添加应用失败")
|
||
|
||
ctk.CTkButton(
|
||
btn_frame,
|
||
text="保存",
|
||
command=save_app,
|
||
fg_color=SAVE_BUTTON_COLOR,
|
||
hover_color=SAVE_BUTTON_HOVER
|
||
).pack(side="right", padx=5)
|
||
|
||
ctk.CTkButton(
|
||
btn_frame,
|
||
text="取消",
|
||
command=dialog.destroy,
|
||
fg_color=BUTTON_GRAY,
|
||
hover_color=BUTTON_GRAY_HOVER
|
||
).pack(side="right", padx=5)
|
||
|
||
def _on_item_click_or_drag_start(self, event, index: int, frame):
|
||
"""处理卡片点击或拖动开始(智能判断)"""
|
||
# 记录初始点击位置和时间,用于判断是点击还是拖动
|
||
self.click_start_pos = (event.x_root, event.y_root)
|
||
self.click_start_time = event.time
|
||
self.potential_drag_index = index
|
||
self.potential_drag_frame = frame
|
||
self.is_dragging = False
|
||
|
||
# 如果点击的是已选中的卡片,暂不处理选择逻辑,等待判断是拖动还是点击
|
||
if index in self.selected_items:
|
||
self.pending_selection = None
|
||
else:
|
||
# 如果点击的是未选中的卡片,记录待处理的选择操作
|
||
self.pending_selection = (event, index, frame)
|
||
|
||
def _on_item_drag_motion(self, event, index: int, frame):
|
||
"""处理卡片拖动移动"""
|
||
if not hasattr(self, 'click_start_pos'):
|
||
return
|
||
|
||
# 计算移动距离
|
||
dx = abs(event.x_root - self.click_start_pos[0])
|
||
dy = abs(event.y_root - self.click_start_pos[1])
|
||
|
||
# 如果移动距离超过阈值(5像素),认为是拖动操作
|
||
if dx > 5 or dy > 5:
|
||
if not self.is_dragging:
|
||
# 开始拖动
|
||
self.is_dragging = True
|
||
|
||
# 如果拖动的是未选中的卡片,先选中它
|
||
if hasattr(self, 'pending_selection') and self.pending_selection:
|
||
evt, idx, frm = self.pending_selection
|
||
self._on_item_click(evt, idx, frm)
|
||
self.pending_selection = None
|
||
|
||
# 开始拖动选中的卡片
|
||
self._start_drag(event, self.potential_drag_index, self.potential_drag_frame)
|
||
else:
|
||
# 继续拖动
|
||
self._on_drag_motion(event)
|
||
|
||
def _on_item_drag_end(self, event):
|
||
"""处理卡片拖动结束"""
|
||
if hasattr(self, 'is_dragging') and self.is_dragging:
|
||
# 结束拖动
|
||
self._end_drag(event)
|
||
self.is_dragging = False
|
||
elif hasattr(self, 'pending_selection') and self.pending_selection:
|
||
# 没有发生拖动,执行点击选择
|
||
evt, idx, frm = self.pending_selection
|
||
self._on_item_click(evt, idx, frm)
|
||
self.pending_selection = None
|
||
|
||
# 清理临时变量
|
||
if hasattr(self, 'click_start_pos'):
|
||
delattr(self, 'click_start_pos')
|
||
if hasattr(self, 'potential_drag_index'):
|
||
delattr(self, 'potential_drag_index')
|
||
if hasattr(self, 'potential_drag_frame'):
|
||
delattr(self, 'potential_drag_frame')
|
||
|
||
def _on_item_click(self, event, index: int, frame):
|
||
"""处理应用项点击事件(用于多选)"""
|
||
# 检查是否按住Shift键
|
||
if event.state & 0x0001: # Shift键
|
||
if self.last_selected_index is not None:
|
||
# Shift多选:选择从上次选中到当前点击之间的所有项
|
||
start = min(self.last_selected_index, index)
|
||
end = max(self.last_selected_index, index)
|
||
self.selected_items = list(range(start, end + 1))
|
||
else:
|
||
# 如果没有上次选中的项,只选中当前项
|
||
self.selected_items = [index]
|
||
elif event.state & 0x0004: # Ctrl键
|
||
# Ctrl多选:切换当前项的选中状态
|
||
if index in self.selected_items:
|
||
self.selected_items.remove(index)
|
||
else:
|
||
self.selected_items.append(index)
|
||
else:
|
||
# 普通点击:只选中当前项
|
||
self.selected_items = [index]
|
||
|
||
self.last_selected_index = index
|
||
self._update_selection_display()
|
||
|
||
# 阻止事件冒泡到空白区域处理
|
||
return "break"
|
||
|
||
def _update_selection_display(self):
|
||
"""更新选中项的显示效果(优化版)"""
|
||
# 取消之前的延迟更新
|
||
if hasattr(self, '_update_timer') and self._update_timer is not None:
|
||
self.after_cancel(self._update_timer)
|
||
|
||
# 延迟更新以减少频繁刷新
|
||
self._update_timer = self.after(10, self._do_update_selection_display)
|
||
|
||
def _do_update_selection_display(self):
|
||
"""执行选择显示更新"""
|
||
selected_set = set(self.selected_items) # 使用集合提高查找效率
|
||
|
||
# 遍历所有应用项,更新选中状态
|
||
for widget in self.apps_scroll_frame.winfo_children():
|
||
if hasattr(widget, 'app_index'):
|
||
is_selected = widget.app_index in selected_set
|
||
|
||
if is_selected:
|
||
# 选中状态 - 使用边框和背景色高亮
|
||
widget.configure(border_width=2, border_color=SELECTION_BORDER, fg_color=SELECTION_BG)
|
||
# 更新拖动手柄颜色为选中状态
|
||
if hasattr(widget, 'drag_handle') and hasattr(widget, 'drag_handle_line_ids'):
|
||
for line_id in widget.drag_handle_line_ids:
|
||
widget.drag_handle.itemconfig(line_id, fill=SELECTION_BORDER)
|
||
# 手柄背景与选中背景一致
|
||
widget.drag_handle.configure(bg=SELECTION_BG)
|
||
else:
|
||
# 未选中状态 - 使用与滑块一致的背景色
|
||
widget.configure(border_width=1, border_color=BORDER_COLOR, fg_color=SCROLLBAR_COLOR)
|
||
# 恢复拖动手柄颜色为默认状态
|
||
if hasattr(widget, 'drag_handle') and hasattr(widget, 'drag_handle_line_ids'):
|
||
for line_id in widget.drag_handle_line_ids:
|
||
widget.drag_handle.itemconfig(line_id, fill=LINE_COLOR_GRAY)
|
||
widget.drag_handle.configure(bg=SCROLLBAR_COLOR) # 与滚动条统一
|
||
|
||
def _handle_empty_area_click(self, event):
|
||
"""处理滚动框架内空白区域左键点击,取消选择"""
|
||
# 这个方法现在由全局点击处理方法统一处理
|
||
self._handle_global_click(event)
|
||
|
||
def _handle_global_click(self, event):
|
||
"""处理全局左键点击事件,在空白区域取消选择"""
|
||
# 检查点击的控件是否是应用卡片或其子控件
|
||
clicked_widget = event.widget
|
||
|
||
# 向上遍历控件树,检查是否点击在应用卡片上
|
||
current_widget = clicked_widget
|
||
is_app_item = False
|
||
|
||
for _ in range(10): # 增加检查层数,确保能找到应用卡片
|
||
if hasattr(current_widget, 'app_index'):
|
||
is_app_item = True
|
||
break
|
||
try:
|
||
current_widget = current_widget.master
|
||
if not current_widget:
|
||
break
|
||
except:
|
||
break
|
||
|
||
# 检查是否点击在交互控件上(按钮、输入框等)
|
||
widget_class = clicked_widget.__class__.__name__
|
||
is_interactive = any(control_type in widget_class for control_type in
|
||
['Button', 'Entry', 'Textbox', 'Combobox', 'Checkbutton',
|
||
'Radiobutton', 'Scale', 'Scrollbar'])
|
||
|
||
# 如果不是点击在应用卡片上,也不是交互控件,且有选中项,则取消选择
|
||
if not is_app_item and not is_interactive and self.selected_items:
|
||
self._log("Global click - clearing selection of {len(self.selected_items)} apps", "DEBUG")
|
||
self.selected_items.clear()
|
||
self.last_selected_index = None
|
||
self._update_selection_display()
|
||
|
||
def _setup_global_click_binding(self):
|
||
"""设置全局点击事件绑定"""
|
||
def bind_recursive(widget):
|
||
"""递归绑定所有子控件的点击事件"""
|
||
try:
|
||
# 跳过应用卡片,它们有自己的点击处理
|
||
if hasattr(widget, 'app_index'):
|
||
return
|
||
|
||
# 跳过已经有特定点击处理的控件
|
||
widget_class = widget.__class__.__name__
|
||
if any(control_type in widget_class for control_type in
|
||
['Button', 'Entry', 'Textbox', 'Combobox']):
|
||
return
|
||
|
||
# 绑定点击事件
|
||
widget.bind("<Button-1>", self._handle_global_click, "+")
|
||
|
||
# 递归绑定子控件
|
||
for child in widget.winfo_children():
|
||
bind_recursive(child)
|
||
except Exception:
|
||
pass # 忽略绑定错误
|
||
|
||
# 从根窗口开始递归绑定
|
||
bind_recursive(self)
|
||
|
||
def _show_empty_area_menu(self, event):
|
||
"""在空白区域显示右键菜单"""
|
||
from tkinter import Menu
|
||
|
||
# 创建现代化的右键菜单
|
||
menu = Menu(self, tearoff=0,
|
||
bg="#2d2d30", fg="#ffffff",
|
||
activebackground="#0e639c", activeforeground="#ffffff",
|
||
selectcolor="#ffffff",
|
||
relief="flat", borderwidth=1, bd=1,
|
||
font=("Segoe UI", 11, "normal"))
|
||
|
||
# 全选选项
|
||
current_project = self.project_combo.get()
|
||
if current_project:
|
||
apps = self.config_manager.get_apps(current_project)
|
||
if apps:
|
||
menu.add_command(
|
||
label=f"全选 ({len(apps)} 项)",
|
||
command=self._select_all_apps_direct
|
||
)
|
||
menu.add_separator()
|
||
|
||
# 粘贴选项
|
||
if self.clipboard_apps:
|
||
menu.add_command(
|
||
label=f"粘贴 ({len(self.clipboard_apps)} 项)",
|
||
command=self._paste_apps
|
||
)
|
||
|
||
# 如果菜单为空,添加提示
|
||
if menu.index("end") is None:
|
||
menu.add_command(label="无可用操作", state="disabled")
|
||
|
||
# 显示菜单
|
||
try:
|
||
menu.tk_popup(event.x_root, event.y_root)
|
||
finally:
|
||
menu.grab_release()
|
||
|
||
def _show_context_menu(self, event, index: int):
|
||
"""显示应用项右键菜单"""
|
||
from tkinter import Menu
|
||
|
||
# 如果右键点击的项不在选中列表中,则只选中当前项
|
||
if index not in self.selected_items:
|
||
self.selected_items = [index]
|
||
self.last_selected_index = index
|
||
self._update_selection_display()
|
||
|
||
# 创建现代化的右键菜单
|
||
menu = Menu(self, tearoff=0,
|
||
bg="#2d2d30", fg="#ffffff",
|
||
activebackground="#0e639c", activeforeground="#ffffff",
|
||
selectcolor="#ffffff",
|
||
relief="flat", borderwidth=1, bd=1,
|
||
font=("Segoe UI", 11, "normal"))
|
||
|
||
# 选择相关操作
|
||
current_project = self.project_combo.get()
|
||
if current_project:
|
||
apps = self.config_manager.get_apps(current_project)
|
||
if len(apps) > len(self.selected_items):
|
||
menu.add_command(
|
||
label=f"全选 ({len(apps)} 项)",
|
||
command=self._select_all_apps_direct
|
||
)
|
||
menu.add_separator()
|
||
|
||
# 编辑操作
|
||
menu.add_command(
|
||
label=f"复制 ({len(self.selected_items)} 项)",
|
||
command=self._copy_apps
|
||
)
|
||
|
||
menu.add_command(
|
||
label=f"剪切 ({len(self.selected_items)} 项)",
|
||
command=self._cut_apps
|
||
)
|
||
|
||
# 粘贴操作
|
||
if self.clipboard_apps:
|
||
menu.add_command(
|
||
label=f"粘贴 ({len(self.clipboard_apps)} 项)",
|
||
command=self._paste_apps
|
||
)
|
||
|
||
# 分隔符
|
||
menu.add_separator()
|
||
|
||
# 删除操作
|
||
menu.add_command(
|
||
label=f"删除 ({len(self.selected_items)} 项)",
|
||
command=self._delete_selected_apps
|
||
)
|
||
|
||
# 显示菜单
|
||
try:
|
||
menu.tk_popup(event.x_root, event.y_root)
|
||
finally:
|
||
menu.grab_release()
|
||
|
||
def _copy_apps(self, menu=None):
|
||
"""复制选中的应用到剪贴板"""
|
||
if menu:
|
||
menu.destroy()
|
||
|
||
# 获取界面上当前选择的项目
|
||
current_project = self.project_combo.get()
|
||
if not current_project:
|
||
return
|
||
|
||
apps = self.config_manager.get_apps(current_project)
|
||
|
||
# 复制选中的应用数据
|
||
self.clipboard_apps = []
|
||
for idx in sorted(self.selected_items):
|
||
if idx < len(apps):
|
||
# 深拷贝应用数据
|
||
app_copy = apps[idx].copy()
|
||
self.clipboard_apps.append(app_copy)
|
||
|
||
self._log("Copied {len(self.clipboard_apps)} apps to clipboard", "DEBUG")
|
||
|
||
def _cut_apps(self, menu=None):
|
||
"""剪切选中的应用到剪贴板"""
|
||
if menu:
|
||
menu.destroy()
|
||
|
||
# 先复制
|
||
self._copy_apps()
|
||
|
||
# 然后删除选中的应用
|
||
# 获取界面上当前选择的项目
|
||
current_project = self.project_combo.get()
|
||
if not current_project:
|
||
return
|
||
|
||
apps = self.config_manager.get_apps(current_project)
|
||
|
||
# 从后往前删除,避免索引变化
|
||
for idx in sorted(self.selected_items, reverse=True):
|
||
if idx < len(apps):
|
||
self.config_manager.delete_app(current_project, idx)
|
||
|
||
# 清空选中列表
|
||
self.selected_items = []
|
||
self.last_selected_index = None
|
||
|
||
# 重新加载
|
||
self._load_apps()
|
||
self.on_update()
|
||
|
||
self._log(f"Cut {len(self.clipboard_apps)} apps", "DEBUG")
|
||
|
||
def _paste_apps(self, menu=None):
|
||
"""粘贴剪贴板中的应用到当前项目"""
|
||
if menu:
|
||
menu.destroy()
|
||
|
||
if not self.clipboard_apps:
|
||
custom_dialogs.show_warning(self, "警告", "剪贴板为空")
|
||
return
|
||
|
||
# 获取界面上当前选择的项目,而不是配置文件中的当前项目
|
||
current_project = self.project_combo.get()
|
||
if not current_project:
|
||
custom_dialogs.show_warning(self, "警告", "请先选择一个项目")
|
||
return
|
||
|
||
|
||
# 获取目标项目的现有应用
|
||
existing_apps = self.config_manager.get_apps(current_project)
|
||
|
||
# 粘贴所有应用(智能去重)
|
||
success_count = 0
|
||
skipped_count = 0
|
||
|
||
for app in self.clipboard_apps:
|
||
# 检查是否已存在完全相同的应用(路径、版本都相同)
|
||
is_duplicate = False
|
||
for existing_app in existing_apps:
|
||
if (existing_app['path'] == app['path'] and
|
||
existing_app['version'] == app['version']):
|
||
# 完全相同的应用,跳过
|
||
is_duplicate = True
|
||
skipped_count += 1
|
||
self._log(f"Skipping duplicate app: {app['name']} (same path and version)", "DEBUG")
|
||
break
|
||
|
||
if is_duplicate:
|
||
continue
|
||
|
||
# 检查是否存在相同路径但版本不同的应用
|
||
has_same_path = any(existing_app['path'] == app['path'] for existing_app in existing_apps)
|
||
|
||
if has_same_path:
|
||
# 路径相同但版本不同,添加版本后缀
|
||
app['name'] = f"{app['name']} (v{app['version']})"
|
||
self._log(f"Same path but different version, renamed to: {app['name']}", "DEBUG")
|
||
|
||
# 检查名称是否重复,如果重复则添加数字后缀
|
||
base_name = app['name']
|
||
counter = 1
|
||
while any(existing_app['name'] == app['name'] for existing_app in existing_apps):
|
||
app['name'] = f"{base_name} ({counter})"
|
||
counter += 1
|
||
|
||
# 添加应用
|
||
if self.config_manager.add_app(current_project, app['name'], app['path'], app['version']):
|
||
# 复制图标设置
|
||
icon = self.config_manager.get_app_icon(app['path'])
|
||
if icon:
|
||
self.config_manager.set_app_icon(app['path'], icon)
|
||
|
||
# 复制颜色设置
|
||
color = self.config_manager.get_app_color(app['path'])
|
||
if color:
|
||
self.config_manager.set_app_color(app['path'], color)
|
||
|
||
success_count += 1
|
||
# 更新现有应用列表,以便后续检查
|
||
existing_apps = self.config_manager.get_apps(current_project)
|
||
|
||
# 重新加载
|
||
self._load_apps()
|
||
self.on_update()
|
||
|
||
# 显示结果消息
|
||
if skipped_count > 0:
|
||
message = f"已粘贴 {success_count} 个应用到 {current_project}\n跳过 {skipped_count} 个重复应用"
|
||
else:
|
||
message = f"已粘贴 {success_count} 个应用到 {current_project}"
|
||
|
||
custom_dialogs.show_info(self, "成功", message)
|
||
self._log(f"Pasted {success_count} apps, skipped {skipped_count} duplicates", "DEBUG")
|
||
|
||
def _delete_selected_apps(self):
|
||
"""删除选中的应用"""
|
||
if not self.selected_items:
|
||
return
|
||
|
||
# 确认删除
|
||
result = custom_dialogs.ask_yes_no(
|
||
self,
|
||
"Confirm Delete",
|
||
f"Are you sure you want to delete the selected {len(self.selected_items)} applications?"
|
||
)
|
||
|
||
if not result:
|
||
return
|
||
|
||
# 获取界面上当前选择的项目
|
||
current_project = self.project_combo.get()
|
||
if not current_project:
|
||
return
|
||
|
||
apps = self.config_manager.get_apps(current_project)
|
||
|
||
# 从后往前删除,避免索引变化
|
||
for idx in sorted(self.selected_items, reverse=True):
|
||
if idx < len(apps):
|
||
self.config_manager.delete_app(current_project, idx)
|
||
|
||
# 清空选中列表
|
||
self.selected_items = []
|
||
self.last_selected_index = None
|
||
|
||
# 重新加载
|
||
self._load_apps()
|
||
self.on_update()
|
||
|
||
self._log("Deleted selected apps", "DEBUG")
|
||
|
||
def _clear_selection(self):
|
||
"""清除所有选择"""
|
||
self.selected_items = []
|
||
self.last_selected_index = None
|
||
self._update_selection_display()
|
||
|
||
def _select_all_apps(self):
|
||
"""全选当前项目的所有应用"""
|
||
current_project = self.project_combo.get()
|
||
if not current_project:
|
||
return
|
||
|
||
# 获取当前项目的所有应用
|
||
apps = self.config_manager.get_apps(current_project)
|
||
|
||
if not apps:
|
||
# 清空选择状态,确保UI一致性
|
||
self.selected_items = []
|
||
self.last_selected_index = None
|
||
self._update_selection_display()
|
||
self._log("No apps to select in empty project", "DEBUG")
|
||
return
|
||
|
||
# 选择所有应用
|
||
self.selected_items = list(range(len(apps)))
|
||
self.last_selected_index = len(apps) - 1 if apps else None
|
||
self._update_selection_display()
|
||
|
||
self._log(f"Selected all {len(apps)} apps", "DEBUG")
|
||
|
||
# 为了兼容性,保留别名
|
||
def _select_all_apps_direct(self):
|
||
"""直接全选所有应用(别名方法)"""
|
||
return self._select_all_apps()
|
||
|
||
# 基本快捷键处理函数
|
||
|
||
def _handle_delete(self, event):
|
||
"""处理 Delete"""
|
||
if self.winfo_exists() and self.state() == 'normal':
|
||
focused = self.focus_get()
|
||
if focused and isinstance(focused, (ctk.CTkEntry, ctk.CTkTextbox)):
|
||
return # 让文本框处理自己的 Delete
|
||
|
||
self._delete_selected_apps()
|
||
return "break"
|
||
|
||
def _handle_escape(self, event):
|
||
"""处理 Escape"""
|
||
if self.winfo_exists() and self.state() == 'normal':
|
||
self._clear_selection()
|
||
return "break"
|
||
|
||
def _set_project_icon(self):
|
||
"""设置项目图标"""
|
||
current_project = self._ensure_project_selected()
|
||
if not current_project:
|
||
return
|
||
|
||
# 打开文件选择对话框
|
||
icon_path = filedialog.askopenfilename(
|
||
title="选择项目图标",
|
||
filetypes=[
|
||
("图片文件", "*.png *.jpg *.jpeg *.ico *.bmp *.gif"),
|
||
("所有文件", "*.*")
|
||
]
|
||
)
|
||
|
||
if icon_path:
|
||
# 保存图标路径
|
||
if self.config_manager.set_project_icon(current_project, icon_path):
|
||
custom_dialogs.show_info(self, "成功", f"项目 '{current_project}' 的图标已设置")
|
||
# 通知主窗口更新
|
||
self.on_update()
|
||
else:
|
||
custom_dialogs.show_error(self, "错误", "设置图标失败")
|
||
|
||
def _set_project_color(self):
|
||
"""设置项目背景颜色"""
|
||
current_project = self._ensure_project_selected()
|
||
if not current_project:
|
||
return
|
||
|
||
# 打开颜色选择对话框
|
||
from tkinter import colorchooser
|
||
color = colorchooser.askcolor(
|
||
title="选择项目背景颜色",
|
||
initialcolor=self.config_manager.get_project_color(current_project)
|
||
)
|
||
|
||
if color and color[1]: # color[1] 是十六进制颜色值
|
||
# 保存颜色
|
||
if self.config_manager.set_project_color(current_project, color[1]):
|
||
custom_dialogs.show_info(self, "成功", f"项目 '{current_project}' 的背景颜色已设置")
|
||
# 通知主窗口更新
|
||
self.on_update()
|
||
else:
|
||
custom_dialogs.show_error(self, "错误", "设置颜色失败")
|