975 lines
40 KiB
Python
975 lines
40 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
项目卡片组件
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import subprocess
|
||
import platform
|
||
from pathlib import Path
|
||
from PyQt5.QtWidgets import *
|
||
from PyQt5.QtCore import *
|
||
from PyQt5.QtGui import *
|
||
|
||
from MetaCore.data.project_manager import ProjectManager, Project
|
||
from MetaCore.ui.icon_manager import IconManager
|
||
from MetaCore.ui.project_settings_page import ProjectSettingsPage
|
||
class ImageDisplayWidget(QWidget):
|
||
"""
|
||
一个专门用于显示带圆角图片的控件。
|
||
它会自动将图片按比例缩放以覆盖整个区域,并进行圆角裁剪。
|
||
"""
|
||
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self.pixmap = QPixmap()
|
||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||
|
||
def setPixmap(self, pixmap):
|
||
self.pixmap = pixmap
|
||
self.update() # 触发重绘
|
||
|
||
# 这是在 ImageDisplayWidget 类内部的方法
|
||
def paintEvent(self, event):
|
||
if self.pixmap.isNull():
|
||
return
|
||
|
||
# 导入 QSize 以便创建新的尺寸对象
|
||
from PyQt5.QtCore import QSize
|
||
import math
|
||
|
||
target_rect = self.rect()
|
||
radius = 12.0
|
||
|
||
# “过缩放”逻辑保持不变,我们仍然需要一张比控件大的图片
|
||
overscale_factor = 1.05
|
||
original_size = target_rect.size()
|
||
larger_width = math.ceil(original_size.width() * overscale_factor)
|
||
larger_height = math.ceil(original_size.height() * overscale_factor)
|
||
larger_target_size = QSize(larger_width, larger_height)
|
||
|
||
scaled_pixmap = self.pixmap.scaled(larger_target_size, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation)
|
||
|
||
# --- 核心修正点在这里 ---
|
||
|
||
# 1. 计算绘制的起始坐标(x, y),以使 scaled_pixmap 的中心与控件中心对齐
|
||
# 由于 scaled_pixmap 比控件大,所以 x 和 y 通常是负数,这是正确的
|
||
draw_x = (target_rect.width() - scaled_pixmap.width()) / 2
|
||
draw_y = (target_rect.height() - scaled_pixmap.height()) / 2
|
||
|
||
# 2. 使用QPainter进行绘制
|
||
painter = QPainter(self)
|
||
painter.setRenderHint(QPainter.Antialiasing)
|
||
|
||
# 3. 创建并设置圆角裁剪路径 (逻辑不变)
|
||
path = QPainterPath()
|
||
path.addRoundedRect(QRectF(target_rect), radius, radius)
|
||
painter.setClipPath(path)
|
||
|
||
# 4. 使用最简单的 drawPixmap 版本,直接在计算好的坐标点绘制
|
||
# 这个版本不会进行任何额外的缩放,完美解决了偏移问题
|
||
painter.drawPixmap(int(draw_x), int(draw_y), scaled_pixmap)
|
||
|
||
# -----------------------
|
||
class ProjectCard(QWidget):
|
||
"""项目卡片组件"""
|
||
|
||
def __init__(self, project: Project, project_manager: ProjectManager, view_mode: str = "grid"):
|
||
super().__init__()
|
||
self.project = project
|
||
self.project_manager = project_manager
|
||
self.view_mode = view_mode
|
||
self.project_settings_page = ProjectSettingsPage()
|
||
|
||
# 设置卡片对象名称和属性
|
||
self.setObjectName("projectCard")
|
||
self.setProperty("status", project.status)
|
||
self.setAttribute(Qt.WA_StyledBackground) # 关键:启用样式背景继承
|
||
|
||
# 设置尺寸和策略
|
||
if view_mode == "grid":
|
||
self.setFixedSize(280, 240)
|
||
else:
|
||
self.setFixedHeight(80)
|
||
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
|
||
|
||
self.init_ui()
|
||
self.connect_signals()
|
||
|
||
# 确保样式正确应用
|
||
self.update_style()
|
||
|
||
def update_style(self):
|
||
"""强制刷新样式"""
|
||
self.style().unpolish(self)
|
||
self.style().polish(self)
|
||
self.update()
|
||
|
||
def init_ui(self):
|
||
"""初始化UI"""
|
||
if self.view_mode == "grid":
|
||
self.create_grid_layout()
|
||
else:
|
||
self.create_list_layout()
|
||
|
||
def create_grid_layout(self):
|
||
"""创建网格布局"""
|
||
layout = QVBoxLayout(self)
|
||
layout.setContentsMargins(0, 0, 0, 0)
|
||
layout.setSpacing(0)
|
||
|
||
# 项目头部
|
||
self.create_project_header(layout)
|
||
|
||
# 项目图片
|
||
self.create_project_image(layout)
|
||
|
||
# 项目底部
|
||
self.create_project_footer(layout)
|
||
|
||
def create_list_layout(self):
|
||
"""创建列表布局"""
|
||
layout = QHBoxLayout(self)
|
||
layout.setContentsMargins(16, 12, 16, 12)
|
||
layout.setSpacing(16)
|
||
|
||
# 项目图标 - 使用优化的图标显示
|
||
self.create_list_project_icon(layout)
|
||
|
||
# 项目信息
|
||
info_layout = QVBoxLayout()
|
||
info_layout.setSpacing(4)
|
||
|
||
# 项目名称
|
||
title_layout = QHBoxLayout()
|
||
title_label = QLabel(self.project.title)
|
||
title_label.setObjectName("projectTitle")
|
||
title_layout.addWidget(title_label)
|
||
|
||
title_layout.addStretch()
|
||
info_layout.addLayout(title_layout)
|
||
|
||
# 项目日期
|
||
date_label = QLabel(self.project.date)
|
||
date_label.setObjectName("projectDate")
|
||
info_layout.addWidget(date_label)
|
||
|
||
layout.addLayout(info_layout)
|
||
layout.addStretch()
|
||
|
||
# 菜单按钮
|
||
self.create_menu_button(layout)
|
||
|
||
def create_project_header(self, layout):
|
||
"""创建项目头部"""
|
||
header_widget = QWidget()
|
||
header_widget.setObjectName("projectHeader")
|
||
header_layout = QHBoxLayout(header_widget)
|
||
header_layout.setContentsMargins(16, 16, 16, 8)
|
||
header_layout.setSpacing(8)
|
||
|
||
# 项目标题
|
||
title_label = QLabel(self.project.title)
|
||
title_label.setObjectName("projectTitle")
|
||
title_label.setWordWrap(True)
|
||
header_layout.addWidget(title_label, 1) # 添加拉伸因子
|
||
|
||
# 右侧操作区
|
||
actions_layout = QHBoxLayout()
|
||
actions_layout.setSpacing(4)
|
||
|
||
# 根据项目状态显示不同的按钮
|
||
if self.project.status == 'pending_delete':
|
||
self.menu_btn = QPushButton("✕")
|
||
self.menu_btn.setObjectName("deleteBtn")
|
||
self.menu_btn.setFixedSize(24, 24)
|
||
self.menu_btn.clicked.connect(self.confirm_delete_project)
|
||
self.menu_btn.setToolTip("确认删除项目")
|
||
else:
|
||
self.menu_btn = QPushButton("⋯")
|
||
self.menu_btn.setObjectName("menuBtn")
|
||
self.menu_btn.setFixedSize(24, 24)
|
||
self.menu_btn.clicked.connect(self.show_context_menu)
|
||
self.menu_btn.setToolTip("项目操作菜单")
|
||
|
||
actions_layout.addWidget(self.menu_btn)
|
||
header_layout.addLayout(actions_layout)
|
||
layout.addWidget(header_widget)
|
||
|
||
def create_project_image(self, layout):
|
||
"""创建项目图片区域 - 最终版"""
|
||
import os
|
||
from PyQt5.QtWidgets import QStackedLayout # 导入 QStackedLayout
|
||
|
||
# image_container 是最外层的容器
|
||
image_container = QWidget()
|
||
image_container.setObjectName("projectImageContainer")
|
||
image_container.setContentsMargins(16, 0, 16, 0)
|
||
|
||
# image_widget 现在是堆叠布局的容器,它拥有灰色背景和圆角
|
||
image_widget = QWidget()
|
||
image_widget.setObjectName("projectImage")
|
||
|
||
# --- 使用 QStackedLayout 来切换两种状态 ---
|
||
stacked_layout = QStackedLayout(image_widget)
|
||
stacked_layout.setContentsMargins(0, 0, 0, 0)
|
||
|
||
# 状态一:显示图片 (使用我们自定义的控件)
|
||
image_display = ImageDisplayWidget()
|
||
# 注意:这里不需要给 image_display 设置 objectName 或样式,因为它只是一个“画布”
|
||
|
||
# 状态二:显示默认图标 (使用一个标准的 QLabel)
|
||
default_icon_label = QLabel("📁")
|
||
default_icon_label.setObjectName("projectIcon") # QSS 会应用到这里
|
||
default_icon_label.setAlignment(Qt.AlignCenter)
|
||
|
||
# 将两个状态控件添加到堆叠布局中
|
||
stacked_layout.addWidget(image_display) # 索引 0
|
||
stacked_layout.addWidget(default_icon_label) # 索引 1
|
||
|
||
# --- 根据条件切换显示 ---
|
||
if hasattr(self.project, 'image') and os.path.exists(self.project.image) and os.path.isfile(self.project.image):
|
||
# 如果图片存在...
|
||
pixmap = QPixmap(self.project.image)
|
||
image_display.setPixmap(pixmap)
|
||
stacked_layout.setCurrentIndex(0) # 显示图片控件
|
||
else:
|
||
# 如果图片不存在...
|
||
stacked_layout.setCurrentIndex(1) # 显示默认图标控件
|
||
|
||
# --- 最终布局 ---
|
||
# 我们只需要一个简单的 QVBoxLayout 来容纳 image_widget
|
||
container_layout = QVBoxLayout(image_container)
|
||
container_layout.setContentsMargins(0, 0, 0, 0)
|
||
container_layout.addWidget(image_widget)
|
||
|
||
layout.addWidget(image_container, 1)
|
||
|
||
def create_project_footer(self, layout):
|
||
"""创建项目底部"""
|
||
footer_widget = QWidget()
|
||
footer_widget.setObjectName("projectFooter")
|
||
footer_layout = QVBoxLayout(footer_widget)
|
||
footer_layout.setContentsMargins(16, 0, 16, 16) # 减少顶部边距
|
||
|
||
# 项目日期
|
||
date_label = QLabel(self.project.date)
|
||
date_label.setObjectName("projectDate")
|
||
date_label.setAlignment(Qt.AlignCenter)
|
||
footer_layout.addWidget(date_label)
|
||
|
||
layout.addWidget(footer_widget)
|
||
|
||
def create_menu_button(self, layout):
|
||
"""创建菜单按钮(用于列表视图)"""
|
||
self.menu_btn = QPushButton("⋮")
|
||
self.menu_btn.setObjectName("menuBtn")
|
||
self.menu_btn.setFixedSize(32, 32)
|
||
self.menu_btn.clicked.connect(self.show_context_menu)
|
||
layout.addWidget(self.menu_btn)
|
||
|
||
def create_list_project_icon(self, layout):
|
||
"""创建列表视图中的项目图标"""
|
||
import os
|
||
from PyQt5.QtWidgets import QStackedLayout
|
||
|
||
# 图标容器
|
||
icon_container = QWidget()
|
||
icon_container.setObjectName("listProjectIconContainer")
|
||
icon_container.setFixedSize(48, 48) # 列表视图中使用较小的图标
|
||
|
||
# 图标控件 - 带圆角背景
|
||
icon_widget = QWidget()
|
||
icon_widget.setObjectName("listProjectIcon")
|
||
icon_widget.setProperty("status", self.project.status) # 设置状态属性
|
||
|
||
# 使用堆叠布局来切换图片和默认图标
|
||
stacked_layout = QStackedLayout(icon_widget)
|
||
stacked_layout.setContentsMargins(0, 0, 0, 0)
|
||
|
||
# 状态一:显示图片 (使用自定义的圆角图片控件)
|
||
image_display = ListImageDisplayWidget()
|
||
|
||
# 状态二:显示默认图标
|
||
default_icon_label = QLabel("📁")
|
||
default_icon_label.setObjectName("listProjectDefaultIcon")
|
||
default_icon_label.setAlignment(Qt.AlignCenter)
|
||
|
||
# 添加到堆叠布局
|
||
stacked_layout.addWidget(image_display) # 索引 0
|
||
stacked_layout.addWidget(default_icon_label) # 索引 1
|
||
|
||
# 根据条件切换显示
|
||
if hasattr(self.project, 'image') and self.project.image and os.path.exists(self.project.image) and os.path.isfile(self.project.image):
|
||
# 如果图片存在,显示图片
|
||
pixmap = QPixmap(self.project.image)
|
||
if not pixmap.isNull():
|
||
image_display.setPixmap(pixmap)
|
||
stacked_layout.setCurrentIndex(0)
|
||
else:
|
||
stacked_layout.setCurrentIndex(1)
|
||
else:
|
||
# 如果图片不存在,显示默认图标
|
||
stacked_layout.setCurrentIndex(1)
|
||
|
||
# 容器布局
|
||
container_layout = QVBoxLayout(icon_container)
|
||
container_layout.setContentsMargins(0, 0, 0, 0)
|
||
container_layout.addWidget(icon_widget)
|
||
|
||
layout.addWidget(icon_container)
|
||
|
||
def get_type_label(self):
|
||
"""获取项目类型标签"""
|
||
type_labels = {
|
||
'industrial': '工业',
|
||
'smart': '智能',
|
||
'vr': 'VR',
|
||
'game': '游戏',
|
||
'design': '设计'
|
||
}
|
||
return type_labels.get(self.project.type, '其他')
|
||
|
||
def show_context_menu(self):
|
||
"""显示右键菜单"""
|
||
menu = QMenu(self)
|
||
|
||
# 刷新预览图
|
||
if IconManager.icon_exists("Refresh"):
|
||
refresh_action = menu.addAction(IconManager.get_icon('Refresh'), "刷新预览图")
|
||
else:
|
||
refresh_action = menu.addAction("🔄 刷新预览图")
|
||
refresh_action.triggered.connect(self.refresh_preview_image)
|
||
|
||
menu.addSeparator()
|
||
|
||
# 在资源管理器显示
|
||
if IconManager.icon_exists('folder'):
|
||
show_in_explorer_action = menu.addAction(IconManager.get_icon('folder'), "在资源管理器显示")
|
||
else:
|
||
show_in_explorer_action = menu.addAction("📁 在资源管理器显示")
|
||
show_in_explorer_action.triggered.connect(self.show_in_explorer)
|
||
|
||
menu.addSeparator()
|
||
|
||
# 删除项目
|
||
if IconManager.icon_exists('delete'):
|
||
delete_action = menu.addAction(IconManager.get_icon('delete'), "移除项目")
|
||
else:
|
||
delete_action = menu.addAction("🗑️ 移除项目")
|
||
delete_action.triggered.connect(self.delete_project)
|
||
|
||
# 显示菜单
|
||
menu.exec_(self.menu_btn.mapToGlobal(self.menu_btn.rect().bottomLeft()))
|
||
|
||
def open_project(self):
|
||
"""打开项目"""
|
||
try:
|
||
# 优先使用项目目录路径,如果不存在则使用基础路径
|
||
project_path = self.project.project_dir if self.project.project_dir else self.project.path
|
||
|
||
# 使用项目管理器的验证方法进行全面检查
|
||
is_valid, error_message = self.project_manager.validate_project_open(project_path)
|
||
if not is_valid:
|
||
QMessageBox.warning(self, "无法打开项目", f"项目无法打开: {error_message}")
|
||
return
|
||
|
||
# 验证通过,显示成功信息
|
||
# QMessageBox.information(self, "打开项目", f"正在打开项目: {self.project.title}")
|
||
# 使用question对话框提供明确的确认选项
|
||
reply = QMessageBox.question(self, "打开项目",
|
||
f"确定要打开项目: {self.project.title}?",
|
||
QMessageBox.Yes | QMessageBox.No,
|
||
QMessageBox.Yes)
|
||
|
||
if reply == QMessageBox.Yes:
|
||
# 用户确认打开项目,继续执行打开逻辑
|
||
# TODO: 在这里添加实际的项目打开逻辑
|
||
print(f"正在打开项目路径: {project_path},正在启动应用程序: {self.project.title}")
|
||
|
||
# 连接信号
|
||
self.project_manager.pycharm_started.connect(self.on_pycharm_started)
|
||
self.project_manager.project_method_called.connect(self.on_project_method_called)
|
||
|
||
# # 启动PyCharm并调用项目方法
|
||
# # 修正参数顺序:
|
||
# # 第一个参数:要在PyCharm中打开的项目路径(EG项目)
|
||
# # 第二个参数:要传递给项目方法的目标项目路径(当前项目)
|
||
success = self.project_manager.run_project_command(
|
||
self.project_settings_page.get_default_open_location(),
|
||
# "/home/tiger/文档/EG", # project_path - 要在PyCharm中打开的项目
|
||
project_path # target_project_path - 要传递给项目方法的目标项目路径
|
||
)
|
||
|
||
if not success:
|
||
QMessageBox.warning(
|
||
self,
|
||
"启动失败",
|
||
"无法启动,请检查打开项目路径。"
|
||
)
|
||
# if success:
|
||
# QMessageBox.information(
|
||
# self,
|
||
# "启动PyCharm",
|
||
# f"正在启动PyCharm并准备调用项目方法...\n目标项目: {project_path}"
|
||
# )
|
||
# else:
|
||
# QMessageBox.warning(
|
||
# self,
|
||
# "启动失败",
|
||
# "无法启动PyCharm,请检查PyCharm安装。"
|
||
# )
|
||
else:
|
||
# 用户取消操作,直接返回
|
||
return
|
||
|
||
|
||
# success = self.project_manager.launch_pycharm_and_open_project(
|
||
# "/home/tiger/文档/EG", # project_path - 要在PyCharm中打开的项目
|
||
# project_path # target_project_path - 要传递给项目方法的目标项目路径
|
||
# )
|
||
# if success:
|
||
# QMessageBox.information(
|
||
# self,
|
||
# "启动PyCharm",
|
||
# f"正在启动PyCharm并准备调用项目方法...\n目标项目: {project_path}"
|
||
# )
|
||
# else:
|
||
# QMessageBox.warning(
|
||
# self,
|
||
# "启动失败",
|
||
# "无法启动PyCharm,请检查PyCharm安装。"
|
||
# )
|
||
|
||
# self.project_manager.open_app_if_not_running(f'{project_path}/project.json', "project.json")
|
||
# self.project_manager.open_project_in_pycharm(project_path)
|
||
|
||
# except Exception as e:
|
||
# QMessageBox.critical(self, "错误", f"打开项目时发生错误:\n{str(e)}")
|
||
except Exception as e:
|
||
QMessageBox.critical(self, "错误", f"启动PyCharm时发生错误:\n{str(e)}")
|
||
|
||
def on_pycharm_started(self):
|
||
"""PyCharm启动完成回调"""
|
||
print("PyCharm has started and is ready")
|
||
|
||
def on_project_method_called(self, target_project_path):
|
||
"""项目方法调用完成回调"""
|
||
QMessageBox.information(
|
||
self,
|
||
"操作完成",
|
||
f"已成功在PyCharm中打开项目: {target_project_path}"
|
||
)
|
||
|
||
def show_in_explorer(self):
|
||
"""在资源管理器中显示项目目录"""
|
||
try:
|
||
# 获取项目路径并规范化
|
||
project_path_str = self.project.project_dir if self.project.project_dir else self.project.path
|
||
|
||
if not project_path_str:
|
||
QMessageBox.warning(self, "路径不存在", "项目路径为空,请检查项目配置。")
|
||
return
|
||
|
||
# 使用pathlib处理路径
|
||
project_path = Path(project_path_str).resolve()
|
||
|
||
print(f"项目ID: {self.project.id}")
|
||
print(f"项目标题: {self.project.title}")
|
||
print(f"最终使用的路径: '{project_path}'")
|
||
print(f"路径是否存在: {project_path.exists()}")
|
||
print(f"是否为目录: {project_path.is_dir()}")
|
||
|
||
# 检查项目路径是否存在
|
||
if not project_path.exists():
|
||
QMessageBox.warning(self, "路径不存在",
|
||
f"项目路径不存在或无效:\n{project_path}\n\n请检查项目是否已被移动或删除。")
|
||
return
|
||
|
||
# 获取操作系统类型
|
||
system = platform.system().lower()
|
||
|
||
if system == "windows":
|
||
try:
|
||
# Windows使用os.startfile,会自动使用系统默认的文件管理器
|
||
os.startfile(str(project_path))
|
||
except OSError as e:
|
||
QMessageBox.critical(self, "打开失败", f"无法打开资源管理器:\n{str(e)}")
|
||
|
||
elif system == "darwin": # macOS
|
||
if project_path.is_file():
|
||
# 如果是文件,选中该文件
|
||
subprocess.run(['open', '-R', str(project_path)], check=True)
|
||
else:
|
||
# 如果是目录,直接打开
|
||
subprocess.run(['open', str(project_path)], check=True)
|
||
|
||
else: # Linux和其他Unix系统
|
||
try:
|
||
# 首先尝试使用nautilus(GNOME文件管理器)
|
||
subprocess.run(['nautilus', str(project_path)], check=True)
|
||
except (FileNotFoundError, subprocess.CalledProcessError):
|
||
try:
|
||
# 尝试使用dolphin (KDE)
|
||
subprocess.run(['dolphin', str(project_path)], check=True)
|
||
except (FileNotFoundError, subprocess.CalledProcessError):
|
||
try:
|
||
# 尝试使用thunar (XFCE)
|
||
subprocess.run(['thunar', str(project_path)], check=True)
|
||
except (FileNotFoundError, subprocess.CalledProcessError):
|
||
try:
|
||
# 最后尝试使用xdg-open
|
||
subprocess.run(['xdg-open', str(project_path)], check=True)
|
||
except (FileNotFoundError, subprocess.CalledProcessError):
|
||
QMessageBox.warning(self, "无法打开文件管理器",
|
||
"系统中没有找到合适的文件管理器。\n"
|
||
f"请手动打开路径: {project_path}")
|
||
|
||
print(f"成功在资源管理器中打开: {project_path}")
|
||
|
||
except subprocess.CalledProcessError as e:
|
||
QMessageBox.critical(self, "打开失败",
|
||
f"无法打开资源管理器:\n{str(e)}")
|
||
except Exception as e:
|
||
QMessageBox.critical(self, "错误",
|
||
f"打开资源管理器时发生错误:\n{str(e)}")
|
||
|
||
def delete_project(self):
|
||
"""删除项目"""
|
||
reply = QMessageBox.question(self, "确认移除",
|
||
f"确定要移除项目 \"{self.project.title}\" 吗?\n此操作不可撤销。",
|
||
QMessageBox.Yes | QMessageBox.No,
|
||
QMessageBox.No)
|
||
|
||
if reply == QMessageBox.Yes:
|
||
self.project_manager.remove_project(self.project.id)
|
||
|
||
def confirm_delete_project(self):
|
||
"""确认删除待删除状态的项目"""
|
||
reply = QMessageBox.question(self, "确认删除项目",
|
||
f"确定要永久删除项目 \"{self.project.title}\" 吗?\n"
|
||
f"此操作不可撤销。\n\n"
|
||
f"提示:如果项目目录已恢复,您可以点击项目卡片来恢复项目。",
|
||
QMessageBox.Yes | QMessageBox.No,
|
||
QMessageBox.No)
|
||
|
||
if reply == QMessageBox.Yes:
|
||
self.project_manager.confirm_delete_project(self.project.id)
|
||
|
||
def update_display(self):
|
||
"""更新显示"""
|
||
# 更新项目状态属性
|
||
self.setProperty("status", self.project.status)
|
||
|
||
# 重新创建UI以反映更改
|
||
for i in reversed(range(self.layout().count())):
|
||
child = self.layout().itemAt(i).widget()
|
||
if child:
|
||
child.setParent(None)
|
||
|
||
self.init_ui()
|
||
|
||
# 强制刷新样式
|
||
self.style().unpolish(self)
|
||
self.style().polish(self)
|
||
self.update()
|
||
|
||
def apply_fallback_styles(self):
|
||
"""应用备用样式,确保卡片有正确的外观"""
|
||
# 直接设置卡片的内联样式
|
||
card_style = """
|
||
QWidget#projectCard {
|
||
background-color: #4a4a5a;
|
||
border: 1px solid #5a5a6a;
|
||
border-radius: 16px;
|
||
}
|
||
QWidget#projectCard:hover {
|
||
background-color: #5a5a6a;
|
||
border-color: #6a6a7a;
|
||
}
|
||
"""
|
||
self.setStyleSheet(card_style)
|
||
|
||
def connect_signals(self):
|
||
"""连接信号"""
|
||
pass
|
||
|
||
def mousePressEvent(self, event):
|
||
"""鼠标点击事件"""
|
||
if event.button() == Qt.LeftButton:
|
||
# 检查是否点击了菜单按钮
|
||
if not self.menu_btn.geometry().contains(event.pos()):
|
||
if self.project.status == 'pending_delete':
|
||
# 待删除状态的项目,尝试恢复
|
||
self.try_restore_project()
|
||
else:
|
||
# 正常状态的项目,打开项目
|
||
self.open_project()
|
||
super().mousePressEvent(event)
|
||
|
||
def try_restore_project(self):
|
||
"""尝试恢复待删除状态的项目"""
|
||
if self.project_manager.restore_project(self.project.id):
|
||
QMessageBox.information(self, "项目已恢复",
|
||
f"项目 \"{self.project.title}\" 已成功恢复!")
|
||
else:
|
||
QMessageBox.information(self, "项目目录不存在",
|
||
f"项目 \"{self.project.title}\" 的目录仍然不存在:\n{self.project.project_dir}\n\n"
|
||
f"提示:当您恢复项目目录后,系统会自动检测并恢复项目状态,无需手动操作。")
|
||
|
||
def enterEvent(self, event):
|
||
"""鼠标进入事件"""
|
||
self.setProperty("hover", True)
|
||
self.style().unpolish(self)
|
||
self.style().polish(self)
|
||
# 强制重绘以确保悬停效果立即生效
|
||
self.update()
|
||
super().enterEvent(event)
|
||
|
||
def leaveEvent(self, event):
|
||
"""鼠标离开事件"""
|
||
self.setProperty("hover", False)
|
||
self.style().unpolish(self)
|
||
self.style().polish(self)
|
||
# 强制重绘以确保悬停效果立即消失
|
||
self.update()
|
||
super().leaveEvent(event)
|
||
|
||
def show_success_tooltip(self, message):
|
||
"""显示成功提示工具提示"""
|
||
# 临时改变菜单按钮的工具提示来显示成功状态
|
||
original_tooltip = self.menu_btn.toolTip()
|
||
|
||
self.menu_btn.setToolTip(f"✅ {message}")
|
||
|
||
# 使用定时器恢复原状态
|
||
QTimer.singleShot(3000, lambda: self.restore_button_state(original_tooltip))
|
||
|
||
def restore_button_state(self, original_tooltip):
|
||
"""恢复按钮原始状态"""
|
||
self.menu_btn.setToolTip(original_tooltip)
|
||
|
||
def refresh_preview_image(self):
|
||
"""刷新预览图"""
|
||
try:
|
||
# 获取项目路径
|
||
project_path = self.project.project_dir if self.project.project_dir else self.project.path
|
||
if not project_path or not Path(project_path).exists():
|
||
QMessageBox.warning(self, "路径不存在", "项目路径不存在,无法生成预览图。")
|
||
return
|
||
|
||
# 显示进度提示
|
||
original_tooltip = self.menu_btn.toolTip()
|
||
self.menu_btn.setEnabled(False)
|
||
self.menu_btn.setToolTip("⏳ 正在生成预览图...")
|
||
QApplication.processEvents()
|
||
|
||
# 生成新的预览图
|
||
preview_path = self.generate_project_preview(project_path)
|
||
|
||
if preview_path and Path(preview_path).exists():
|
||
# 清理旧的预览图
|
||
self.project_manager.cleanup_old_preview_images(self.project.id)
|
||
|
||
# 更新项目的图片路径
|
||
old_image = self.project.image
|
||
self.project.image = preview_path
|
||
self.project_manager.update_project(self.project)
|
||
|
||
# 刷新显示
|
||
self.refresh_image_display()
|
||
|
||
# 显示成功提示(不使用阻塞对话框)
|
||
self.show_success_tooltip("预览图已刷新!")
|
||
|
||
# 如果有旧图片且不同于新图片,尝试删除
|
||
if old_image and old_image != preview_path and Path(old_image).exists():
|
||
try:
|
||
# 检查是否是我们生成的预览图
|
||
if "ProjectPreviews" in old_image:
|
||
Path(old_image).unlink()
|
||
except Exception:
|
||
pass # 忽略删除失败
|
||
else:
|
||
QMessageBox.warning(self, "生成失败",
|
||
"无法生成预览图。\n\n可能原因:\n"
|
||
"• 项目目录中没有图片文件\n"
|
||
"• 图片文件格式不支持\n"
|
||
"• 权限不足")
|
||
|
||
except Exception as e:
|
||
QMessageBox.critical(self, "错误", f"刷新预览图时发生错误:\n{str(e)}")
|
||
finally:
|
||
# 恢复按钮状态
|
||
self.menu_btn.setEnabled(True)
|
||
self.menu_btn.setToolTip(original_tooltip)
|
||
|
||
def generate_project_preview(self, project_path):
|
||
"""生成项目预览图"""
|
||
try:
|
||
project_path = Path(project_path)
|
||
|
||
# 查找项目中的图片文件
|
||
image_extensions = ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.webp']
|
||
found_images = []
|
||
|
||
# 搜索常见的图片目录
|
||
search_dirs = [
|
||
project_path,
|
||
project_path / 'images',
|
||
project_path / 'img',
|
||
project_path / 'assets',
|
||
project_path / 'static',
|
||
project_path / 'resources',
|
||
project_path / 'media',
|
||
project_path / 'screenshots',
|
||
project_path / 'preview'
|
||
]
|
||
|
||
for search_dir in search_dirs:
|
||
if search_dir.exists() and search_dir.is_dir():
|
||
for ext in image_extensions:
|
||
found_images.extend(search_dir.glob(f'*{ext}'))
|
||
found_images.extend(search_dir.glob(f'*{ext.upper()}'))
|
||
# 递归搜索子目录(限制深度)
|
||
found_images.extend(search_dir.glob(f'*/*{ext}'))
|
||
found_images.extend(search_dir.glob(f'*/*{ext.upper()}'))
|
||
|
||
if not found_images:
|
||
return self.create_default_preview(project_path)
|
||
|
||
# 优先选择特定名称的图片
|
||
priority_names = ['preview', 'screenshot', 'main', 'cover', 'thumbnail', 'icon']
|
||
selected_image = None
|
||
|
||
for priority_name in priority_names:
|
||
for img_path in found_images:
|
||
if priority_name in img_path.stem.lower():
|
||
selected_image = img_path
|
||
break
|
||
if selected_image:
|
||
break
|
||
|
||
# 如果没有找到优先图片,选择第一个
|
||
if not selected_image:
|
||
selected_image = found_images[0]
|
||
|
||
# 创建预览图存储目录
|
||
preview_dir = Path.cwd() / 'MetaCore' / 'Resources' / 'ProjectPreviews'
|
||
preview_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
# 生成预览图文件名
|
||
preview_filename = f"preview_{self.project.id}_{int(QDateTime.currentMSecsSinceEpoch())}.png"
|
||
preview_path = preview_dir / preview_filename
|
||
|
||
# 处理图片并保存预览图
|
||
original_pixmap = QPixmap(str(selected_image))
|
||
if not original_pixmap.isNull():
|
||
# 缩放到合适的预览尺寸
|
||
preview_size = QSize(400, 300)
|
||
scaled_pixmap = original_pixmap.scaled(
|
||
preview_size,
|
||
Qt.KeepAspectRatioByExpanding,
|
||
Qt.SmoothTransformation
|
||
)
|
||
|
||
# 裁剪到目标尺寸
|
||
if scaled_pixmap.size() != preview_size:
|
||
x = (scaled_pixmap.width() - preview_size.width()) // 2
|
||
y = (scaled_pixmap.height() - preview_size.height()) // 2
|
||
scaled_pixmap = scaled_pixmap.copy(x, y, preview_size.width(), preview_size.height())
|
||
|
||
# 保存预览图
|
||
if scaled_pixmap.save(str(preview_path), 'PNG'):
|
||
return str(preview_path)
|
||
|
||
return self.create_default_preview(project_path)
|
||
|
||
except Exception as e:
|
||
print(f"生成预览图时发生错误: {e}")
|
||
return self.create_default_preview(project_path)
|
||
|
||
def create_default_preview(self, project_path):
|
||
"""创建默认预览图"""
|
||
try:
|
||
# 创建预览图存储目录
|
||
preview_dir = Path.cwd() / 'MetaCore' / 'Resources' / 'ProjectPreviews'
|
||
preview_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
# 生成默认预览图文件名
|
||
preview_filename = f"default_preview_{self.project.id}.png"
|
||
preview_path = preview_dir / preview_filename
|
||
|
||
# 创建一个带有项目信息的默认预览图
|
||
pixmap = QPixmap(400, 300)
|
||
pixmap.fill(QColor(70, 70, 80)) # 深灰色背景
|
||
|
||
painter = QPainter(pixmap)
|
||
painter.setRenderHint(QPainter.Antialiasing)
|
||
|
||
# 设置字体
|
||
font = QFont("微软雅黑", 16, QFont.Bold)
|
||
painter.setFont(font)
|
||
painter.setPen(QColor(255, 255, 255))
|
||
|
||
# 绘制项目名称
|
||
title_rect = QRect(20, 100, 360, 40)
|
||
painter.drawText(title_rect, Qt.AlignCenter | Qt.TextWordWrap, self.project.title)
|
||
|
||
# 绘制项目类型
|
||
type_font = QFont("微软雅黑", 12)
|
||
painter.setFont(type_font)
|
||
painter.setPen(QColor(200, 200, 200))
|
||
type_rect = QRect(20, 150, 360, 30)
|
||
type_text = self.get_type_label()
|
||
painter.drawText(type_rect, Qt.AlignCenter, f"项目类型: {type_text}")
|
||
|
||
# 绘制创建日期
|
||
date_rect = QRect(20, 180, 360, 30)
|
||
painter.drawText(date_rect, Qt.AlignCenter, f"创建时间: {self.project.date}")
|
||
|
||
# 绘制文件夹图标
|
||
icon_font = QFont("Segoe UI Emoji", 48)
|
||
painter.setFont(icon_font)
|
||
painter.setPen(QColor(150, 150, 150))
|
||
icon_rect = QRect(20, 30, 360, 60)
|
||
painter.drawText(icon_rect, Qt.AlignCenter, "📁")
|
||
|
||
painter.end()
|
||
|
||
# 保存预览图
|
||
if pixmap.save(str(preview_path), 'PNG'):
|
||
return str(preview_path)
|
||
|
||
except Exception as e:
|
||
print(f"创建默认预览图时发生错误: {e}")
|
||
|
||
return None
|
||
|
||
def refresh_image_display(self):
|
||
"""刷新图片显示"""
|
||
try:
|
||
# 更新网格视图中的图片显示
|
||
if self.view_mode == "grid":
|
||
self.update_grid_image_display()
|
||
else:
|
||
self.update_list_image_display()
|
||
except Exception as e:
|
||
print(f"刷新图片显示时发生错误: {e}")
|
||
|
||
def update_grid_image_display(self):
|
||
"""更新网格视图中的图片显示"""
|
||
# 找到图片容器
|
||
for i in range(self.layout().count()):
|
||
widget = self.layout().itemAt(i).widget()
|
||
if widget and widget.objectName() == "projectImageContainer":
|
||
# 找到内部的堆叠布局
|
||
for j in range(widget.layout().count()):
|
||
image_widget = widget.layout().itemAt(j).widget()
|
||
if image_widget and image_widget.objectName() == "projectImage":
|
||
stacked_layout = image_widget.layout()
|
||
if isinstance(stacked_layout, QStackedLayout):
|
||
# 获取图片显示控件
|
||
image_display = stacked_layout.widget(0)
|
||
if isinstance(image_display, ImageDisplayWidget):
|
||
# 检查是否有新的图片文件
|
||
if (hasattr(self.project, 'image') and
|
||
self.project.image and
|
||
os.path.exists(self.project.image) and
|
||
os.path.isfile(self.project.image)):
|
||
# 重新加载图片
|
||
pixmap = QPixmap(self.project.image)
|
||
if not pixmap.isNull():
|
||
image_display.setPixmap(pixmap)
|
||
stacked_layout.setCurrentIndex(0)
|
||
else:
|
||
stacked_layout.setCurrentIndex(1)
|
||
else:
|
||
stacked_layout.setCurrentIndex(1)
|
||
break
|
||
|
||
def update_list_image_display(self):
|
||
"""更新列表视图中的图片显示"""
|
||
# 在列表视图中查找并更新图片显示
|
||
for i in range(self.layout().count()):
|
||
widget = self.layout().itemAt(i).widget()
|
||
if widget and widget.objectName() == "listProjectIconContainer":
|
||
# 找到内部的堆叠布局
|
||
for j in range(widget.layout().count()):
|
||
icon_widget = widget.layout().itemAt(j).widget()
|
||
if icon_widget and icon_widget.objectName() == "listProjectIcon":
|
||
stacked_layout = icon_widget.layout()
|
||
if isinstance(stacked_layout, QStackedLayout):
|
||
# 获取图片显示控件
|
||
image_display = stacked_layout.widget(0)
|
||
if isinstance(image_display, ListImageDisplayWidget):
|
||
# 检查是否有新的图片文件
|
||
if (hasattr(self.project, 'image') and
|
||
self.project.image and
|
||
os.path.exists(self.project.image) and
|
||
os.path.isfile(self.project.image)):
|
||
# 重新加载图片
|
||
pixmap = QPixmap(self.project.image)
|
||
if not pixmap.isNull():
|
||
image_display.setPixmap(pixmap)
|
||
stacked_layout.setCurrentIndex(0)
|
||
else:
|
||
stacked_layout.setCurrentIndex(1)
|
||
else:
|
||
stacked_layout.setCurrentIndex(1)
|
||
break
|
||
|
||
|
||
class ListImageDisplayWidget(QWidget):
|
||
"""
|
||
专门用于列表视图的圆角图片显示控件
|
||
尺寸较小,适合列表视图
|
||
"""
|
||
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self.pixmap = QPixmap()
|
||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||
|
||
def setPixmap(self, pixmap):
|
||
self.pixmap = pixmap
|
||
self.update()
|
||
|
||
def paintEvent(self, event):
|
||
if self.pixmap.isNull():
|
||
return
|
||
|
||
from PyQt5.QtCore import QSize
|
||
import math
|
||
|
||
target_rect = self.rect()
|
||
radius = 8.0 # 列表视图使用较小的圆角
|
||
|
||
# 过缩放因子
|
||
overscale_factor = 1.05
|
||
original_size = target_rect.size()
|
||
larger_width = math.ceil(original_size.width() * overscale_factor)
|
||
larger_height = math.ceil(original_size.height() * overscale_factor)
|
||
larger_target_size = QSize(larger_width, larger_height)
|
||
|
||
scaled_pixmap = self.pixmap.scaled(larger_target_size, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation)
|
||
|
||
# 计算绘制的起始坐标,使图片居中
|
||
draw_x = (target_rect.width() - scaled_pixmap.width()) / 2
|
||
draw_y = (target_rect.height() - scaled_pixmap.height()) / 2
|
||
|
||
# 绘制
|
||
painter = QPainter(self)
|
||
painter.setRenderHint(QPainter.Antialiasing)
|
||
|
||
# # 创建圆角裁剪路径
|
||
# path = QPainterPath()
|
||
# path.addRoundedRect(QRectF(target_rect), radius, radius)
|
||
# painter.setClipPath(path)
|
||
|
||
# 绘制图片
|
||
painter.drawPixmap(int(draw_x), int(draw_y), scaled_pixmap)
|
||
|