MetaCore-startup/MetaCore/ui/project_card.py
2025-10-17 16:56:28 +08:00

1361 lines
58 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 data.project_manager import ProjectManager, Project
from ui.icon_manager import IconManager
from ui.styles import StyleSheet
from ui.project_settings_page import ProjectSettingsPage
from ui.widget import UniversalMessageDialog
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 = 5.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):
"""项目卡片组件"""
CORNER_RADIUS = 5
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.project_size_text = self._compute_project_size_text()
# 设置卡片对象名称和属性
self.setObjectName("projectCard")
self.setProperty("status", project.status)
self.setAttribute(Qt.WA_StyledBackground) # 关键:启用样式背景继承
# 设置尺寸和策略 - 匹配 Figma 设计规范
if view_mode == "grid":
self.setFixedSize(276, 191)
else:
self.setFixedHeight(88) # 调整列表视图高度
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
self.init_ui()
self.connect_signals()
# 确保样式正确应用
self.update_style()
self.apply_corner_mask()
def update_style(self):
"""强制刷新样式"""
self.style().unpolish(self)
self.style().polish(self)
self.update()
def apply_corner_mask(self):
"""为整个卡片应用统一的圆角裁剪"""
rect = self.rect()
if rect.isNull():
return
path = QPainterPath()
path.addRoundedRect(QRectF(rect), self.CORNER_RADIUS, self.CORNER_RADIUS)
fill_polygon = path.toFillPolygon()
if fill_polygon.isEmpty():
return
self.setMask(QRegion(fill_polygon.toPolygon()))
def _compute_project_size_text(self):
"""根据项目路径计算大小文本"""
path = self._resolve_project_path()
if not path:
return "N/A"
try:
if not path.exists():
return "N/A"
except OSError:
return "N/A"
size_bytes = self._calculate_path_size(path)
return self._format_size(size_bytes)
def _resolve_project_path(self):
project_path = self.project.project_dir if self.project.project_dir else self.project.path
if not project_path:
return None
if isinstance(project_path, Path):
candidate = project_path
else:
candidate = Path(str(project_path))
if candidate == Path():
return None
try:
return candidate if candidate.is_absolute() else candidate.resolve()
except OSError:
return candidate
@staticmethod
def _calculate_path_size(path: Path) -> int:
try:
if path.is_file():
return path.stat().st_size
except OSError:
return 0
total = 0
stack = [path]
while stack:
current = stack.pop()
try:
with os.scandir(current) as entries:
for entry in entries:
try:
if entry.is_symlink():
continue
if entry.is_file(follow_symlinks=False):
total += entry.stat(follow_symlinks=False).st_size
elif entry.is_dir(follow_symlinks=False):
stack.append(Path(entry.path))
except OSError:
continue
except OSError:
continue
return total
@staticmethod
def _format_size(size_bytes: int) -> str:
if size_bytes <= 0:
return "0B"
units = ["B", "KB", "MB", "GB", "TB"]
size = float(size_bytes)
for unit in units:
if size < 1024 or unit == units[-1]:
if unit == "B":
return f"{int(size)}{unit}"
return f"{size:.2f}{unit}"
size /= 1024
return f"{int(size_bytes)}B"
def init_ui(self):
"""初始化UI"""
if self.view_mode == "grid":
self.create_grid_layout()
else:
self.create_list_layout()
def resizeEvent(self, event):
super().resizeEvent(event)
self.apply_corner_mask()
def create_grid_layout(self):
"""创建网格布局 - 根据Figma设计优化"""
# 使用绝对定位的方式来实现图片背景和覆盖层效果
self.setStyleSheet("""
QWidget#projectCard {
border-radius: 5px;
overflow: hidden;
}
""")
# 创建背景图片层
self.create_background_image()
# 创建右上角按钮
self.create_overlay_button()
# 创建底部信息覆盖层
self.create_bottom_overlay()
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_background_image(self):
"""创建背景图片层"""
# 背景图片标签,填充整个卡片
self.background_label = QLabel(self)
self.background_label.setGeometry(0, 0, 276, 191)
self.background_label.setScaledContents(True)
self.background_label.setAlignment(Qt.AlignCenter)
# 检查是否有项目图片
if hasattr(self.project, 'image') and self.project.image and os.path.exists(self.project.image):
pixmap = QPixmap(self.project.image)
if not pixmap.isNull():
# 缩放图片以填充整个区域,保持宽高比
scaled_pixmap = pixmap.scaled(276, 191, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation)
self.background_label.setPixmap(scaled_pixmap)
else:
self.set_default_background()
else:
self.set_default_background()
def set_default_background(self):
"""设置默认背景"""
# 使用project_empty_icon作为默认背景
if IconManager.icon_exists('project_empty_icon'):
empty_pixmap = IconManager.get_pixmap('project_empty_icon', QSize(276, 191))
if not empty_pixmap.isNull():
self.background_label.setPixmap(empty_pixmap)
return
# 如果图标不存在,创建一个默认的灰色背景
default_pixmap = QPixmap(276, 191)
default_pixmap.fill(QColor(66, 67, 71)) # #424347 - Figma中的颜色
# 在中心绘制文件夹图标
painter = QPainter(default_pixmap)
painter.setRenderHint(QPainter.Antialiasing)
# 绘制文件夹图标
font = QFont("Segoe UI Emoji", 48)
painter.setFont(font)
painter.setPen(QColor(212, 212, 212)) # #d4d4d4
painter.drawText(default_pixmap.rect(), Qt.AlignCenter, "📁")
painter.end()
self.background_label.setPixmap(default_pixmap)
def create_overlay_button(self):
"""创建右上角覆盖按钮"""
# 根据项目状态显示不同的按钮
if self.project.status == 'pending_delete':
self.overlay_btn = QPushButton("", self)
self.overlay_btn.setObjectName("overlayDeleteBtn")
self.overlay_btn.clicked.connect(self.confirm_delete_project)
self.overlay_btn.setToolTip("确认删除项目")
else:
self.overlay_btn = QPushButton(self)
self.overlay_btn.setObjectName("overlayInfoBtn")
self.overlay_btn.setCursor(Qt.PointingHandCursor)
self.overlay_btn.setAttribute(Qt.WA_Hover, True)
self.overlay_btn.installEventFilter(self)
self._set_overlay_icon("infomation", fallback_text="i")
self.overlay_btn.clicked.connect(self.show_context_menu)
# self.overlay_btn.setToolTip("项目信息")
# 设置按钮位置和大小
self.overlay_btn.setGeometry(248, 8, 20, 20)
def _set_overlay_icon(self, icon_name: str, fallback_text: str = ""):
"""Set overlay button icon with fallback text."""
if not hasattr(self, "overlay_btn"):
return
if IconManager.icon_exists(icon_name):
self.overlay_btn.setIcon(IconManager.get_icon(icon_name, QSize(16, 16)))
self.overlay_btn.setIconSize(QSize(16, 16))
self.overlay_btn.setText("")
else:
self.overlay_btn.setIcon(QIcon())
self.overlay_btn.setText(fallback_text)
def eventFilter(self, watched, event):
"""Handle hover/click states for the overlay button icon."""
if hasattr(self, "overlay_btn") and watched is self.overlay_btn:
if event.type() == QEvent.Enter:
self._set_overlay_icon("infomation_hover", fallback_text="i")
elif event.type() == QEvent.Leave:
self._set_overlay_icon("infomation", fallback_text="i")
elif event.type() == QEvent.MouseButtonPress and event.button() == Qt.LeftButton:
self._set_overlay_icon("infomation_check", fallback_text="v")
elif event.type() == QEvent.MouseButtonRelease and event.button() == Qt.LeftButton:
if self.overlay_btn.rect().contains(event.pos()):
self._set_overlay_icon("infomation_hover", fallback_text="i")
else:
self._set_overlay_icon("infomation", fallback_text="i")
return super().eventFilter(watched, event)
def create_bottom_overlay(self):
"""创建底部信息覆盖层"""
# 底部覆盖层容器
self.bottom_overlay = QWidget(self)
self.bottom_overlay.setObjectName("bottomOverlay")
self.bottom_overlay.setGeometry(0, 138, 276, 53)
# 底部布局
overlay_layout = QHBoxLayout(self.bottom_overlay)
overlay_layout.setContentsMargins(20, 8, 20, 8)
overlay_layout.setSpacing(8)
# 项目标题
title_label = QLabel(self.project.title)
title_label.setObjectName("overlayProjectTitle")
overlay_layout.addWidget(title_label)
overlay_layout.addStretch()
# 右侧信息区域
info_layout = QVBoxLayout()
info_layout.setSpacing(2)
info_layout.setContentsMargins(0, 0, 0, 0)
# 项目日期
date_label = QLabel(self.project.date)
date_label.setObjectName("overlayProjectDate")
date_label.setAlignment(Qt.AlignRight)
info_layout.addWidget(date_label)
# 项目大小
# size_label = QLabel("52.00KB") # 这里可以根据实际项目大小动态设置
size_label = QLabel(self.project_size_text)
size_label.setObjectName("overlayProjectSize")
size_label.setAlignment(Qt.AlignRight)
info_layout.addWidget(size_label)
overlay_layout.addLayout(info_layout)
def create_project_header(self, layout):
"""创建项目头部"""
header_widget = QWidget()
header_widget.setObjectName("projectHeader")
header_layout = QHBoxLayout(header_widget)
header_layout.setContentsMargins(8, 8, 8, 8)
header_layout.setSpacing(8)
header_layout.addStretch()
# 右侧操作区
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(20, 20)
self.menu_btn.clicked.connect(self.confirm_delete_project)
self.menu_btn.setToolTip("确认删除项目")
else:
self.menu_btn = QPushButton()
if IconManager.icon_exists('infomation'):
self.menu_btn.setIcon(IconManager.get_icon('infomation', QSize(20, 20)))
self.menu_btn.setIconSize(QSize(20, 20))
else:
self.menu_btn.setText("")
self.menu_btn.setObjectName("menuBtn")
self.menu_btn.setFixedSize(20, 20)
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 = QHBoxLayout(footer_widget)
footer_layout.setContentsMargins(20, 0, 20, 0)
footer_layout.setSpacing(0)
# 项目标题(左侧)
title_label = QLabel(self.project.title)
title_label.setObjectName("projectTitle")
footer_layout.addWidget(title_label)
footer_layout.addStretch()
# 项目信息(右侧)
info_layout = QVBoxLayout()
info_layout.setSpacing(0)
info_layout.setContentsMargins(0, 0, 0, 0)
# 项目日期
date_label = QLabel(self.project.date)
date_label.setObjectName("projectDate")
date_label.setAlignment(Qt.AlignRight)
info_layout.addWidget(date_label)
# 项目大小
# size_label = QLabel("52.00KB") # 这里可以根据实际项目大小动态设置
size_label = QLabel(self.project_size_text)
size_label.setObjectName("projectDate")
size_label.setAlignment(Qt.AlignRight)
info_layout.addWidget(size_label)
footer_layout.addLayout(info_layout)
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)
menu.setWindowFlags(menu.windowFlags() | Qt.FramelessWindowHint | Qt.NoDropShadowWindowHint)
menu.setAttribute(Qt.WA_TranslucentBackground, True)
menu.setStyleSheet(StyleSheet.get_context_menu_style())
# 刷新预览图
if IconManager.icon_exists("refresh_projectcard"):
refresh_action = menu.addAction(IconManager.get_icon('refresh_projectcard'), "刷新预览图")
else:
refresh_action = menu.addAction("🔄 刷新预览图")
refresh_action.triggered.connect(self.refresh_preview_image)
menu.addSeparator()
# 在资源管理器显示
if IconManager.icon_exists('open_projectcard'):
show_in_explorer_action = menu.addAction(IconManager.get_icon('open_projectcard'), "在资源管理器显示")
else:
show_in_explorer_action = menu.addAction("📁 在资源管理器显示")
show_in_explorer_action.triggered.connect(self.show_in_explorer)
menu.addSeparator()
# 删除项目
if IconManager.icon_exists('remove_projectcard'):
delete_action = menu.addAction(IconManager.get_icon('remove_projectcard'), "移除项目")
else:
delete_action = menu.addAction("🗑️ 移除项目")
delete_action.triggered.connect(self.delete_project)
# 显示菜单
# 根据视图模式选择正确的按钮来定位菜单
if self.view_mode == "grid" and hasattr(self, 'overlay_btn'):
menu.exec_(self.overlay_btn.mapToGlobal(self.overlay_btn.rect().bottomLeft()))
elif hasattr(self, 'menu_btn'):
menu.exec_(self.menu_btn.mapToGlobal(self.menu_btn.rect().bottomLeft()))
else:
# 如果没有按钮,使用鼠标位置
menu.exec_(QCursor.pos())
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}")
UniversalMessageDialog.show_warning(self,
"无法打开项目", f"项目无法打开: {error_message}",
False, "确定")
return
# 验证通过,显示成功信息
# QMessageBox.information(self, "打开项目", f"正在打开项目: {self.project.title}")
# 使用question对话框提供明确的确认选项
# reply = QMessageBox.question(self, "打开项目",
# f"确定要打开项目: {self.project.title}?",
# QMessageBox.Yes | QMessageBox.No,
# QMessageBox.Yes)
reply = UniversalMessageDialog.show_info(self,
"打开项目",
f"确定要打开项目: {self.project.title}?",
True, "确定", "取消")
if reply == QDialog.Accepted:
# 用户确认打开项目,继续执行打开逻辑
# 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:
UniversalMessageDialog.show_warning(self,
"启动失败",
"无法启动,请检查打开项目路径。",
False, "确定")
# 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)}")
UniversalMessageDialog.show_error(self,
"错误",
f"启动PyCharm时发生错误:\n{str(e)}",
False, "确定")
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}"
# )
UniversalMessageDialog.show_info(self,
"操作完成",
f"已成功在PyCharm中打开项目: {target_project_path}",
False, "确定")
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, "路径不存在", "项目路径为空,请检查项目配置。")
UniversalMessageDialog.show_warning(self,
"路径不存在",
"项目路径为空,请检查项目配置。",
False, "确定")
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请检查项目是否已被移动或删除。")
UniversalMessageDialog.show_warning(self,
"路径不存在",
f"项目路径不存在或无效:\n{project_path}\n\n请检查项目是否已被移动或删除。",
False, "确定")
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)}")
UniversalMessageDialog.show_error(self,
"打开失败",
f"无法打开资源管理器:\n{str(e)}",
False, "确定")
return
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:
# 首先尝试使用nautilusGNOME文件管理器
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}")
UniversalMessageDialog.show_warning(self,
"无法打开文件管理器",
"系统中没有找到合适的文件管理器。\n"
f"请手动打开路径: {project_path}",
False, "确定")
print(f"成功在资源管理器中打开: {project_path}")
except subprocess.CalledProcessError as e:
# QMessageBox.critical(self, "打开失败",
# f"无法打开资源管理器:\n{str(e)}")
UniversalMessageDialog.show_error(self,
"打开失败",
f"无法打开资源管理器:\n{str(e)}",
False, "确定")
except Exception as e:
# QMessageBox.critical(self, "错误",
# f"打开资源管理器时发生错误:\n{str(e)}")
UniversalMessageDialog.show_error(self,
"错误",
f"打开资源管理器时发生错误:\n{str(e)}",
False, "确定")
def delete_project(self):
"""删除项目"""
# reply = QMessageBox.question(self, "确认移除",
# f"确定要移除项目 \"{self.project.title}\" 吗?\n此操作不可撤销。",
# QMessageBox.Yes | QMessageBox.No,
# QMessageBox.No)
reply = UniversalMessageDialog.show_info(self,
"确认移除",
f"确定要移除项目 \"{self.project.title}\" 吗?\n此操作不可撤销。",
True, "确定", "取消")
if reply == QDialog.Accepted:
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)
reply = UniversalMessageDialog.show_info(self,
"确认删除项目",
f"确定要永久删除项目 \"{self.project.title}\" 吗?\n"
f"此操作不可撤销。\n\n",
True, "确定", "取消")
if reply == QDialog.Accepted:
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.project_size_text = self._compute_project_size_text()
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 self.view_mode == "grid":
if hasattr(self, 'overlay_btn') and not self.overlay_btn.geometry().contains(event.pos()):
if self.project.status == 'pending_delete':
# 待删除状态的项目,尝试恢复
self.try_restore_project()
else:
# 正常状态的项目,打开项目
self.open_project()
else:
# 列表视图的原有逻辑
if hasattr(self, 'menu_btn') and 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}\" 已成功恢复!")
UniversalMessageDialog.show_info(self,
"项目已恢复",
f"项目 \"{self.project.title}\" 已成功恢复!",
False, "确定")
else:
# QMessageBox.information(self, "项目目录不存在",
# f"项目 \"{self.project.title}\" 的目录仍然不存在:\n{self.project.project_dir}\n\n"
# f"提示:当您恢复项目目录后,系统会自动检测并恢复项目状态,无需手动操作。")
UniversalMessageDialog.show_info(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):
"""显示成功提示工具提示"""
# 根据视图模式选择正确的按钮
target_btn = None
if self.view_mode == "grid" and hasattr(self, 'overlay_btn'):
target_btn = self.overlay_btn
elif hasattr(self, 'menu_btn'):
target_btn = self.menu_btn
if target_btn:
# 临时改变按钮的工具提示来显示成功状态
original_tooltip = target_btn.toolTip()
target_btn.setToolTip(f"{message}")
# 使用定时器恢复原状态
QTimer.singleShot(3000, lambda: self.restore_button_state(target_btn, original_tooltip))
def restore_button_state(self, button, original_tooltip):
"""恢复按钮原始状态"""
if button:
button.setToolTip(original_tooltip)
def refresh_preview_image(self):
"""刷新预览图"""
# 确定要操作的按钮
target_btn = None
if self.view_mode == "grid" and hasattr(self, 'overlay_btn'):
target_btn = self.overlay_btn
elif hasattr(self, 'menu_btn'):
target_btn = self.menu_btn
original_tooltip = ""
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, "路径不存在", "项目路径不存在,无法生成预览图。")
UniversalMessageDialog.show_warning(self,
"路径不存在",
"项目路径不存在,无法生成预览图。",
False, "确定")
return
# 显示进度提示
if target_btn:
original_tooltip = target_btn.toolTip()
target_btn.setEnabled(False)
target_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:
# 生成预览图失败使用project_empty_icon作为默认图片
self.project.image = None
self.project_manager.update_project(self.project)
# 刷新显示以显示默认图标
self.refresh_image_display()
# QMessageBox.warning(self, "生成失败",
# "无法生成预览图,已使用默认图标。\n\n可能原因\n"
# "• 项目目录中没有图片文件\n"
# "• 图片文件格式不支持\n"
# "• 权限不足")
UniversalMessageDialog.show_warning(self,
"生成失败",
"无法生成预览图,已使用默认图标。\n\n可能原因: \n"
"• 项目目录中没有图片文件\n"
"• 图片文件格式不支持\n"
"• 权限不足",
False, "确定")
except Exception as e:
# 发生异常时也使用project_empty_icon作为默认图片
self.project.image = None
self.project_manager.update_project(self.project)
# 刷新显示以显示默认图标
self.refresh_image_display()
# QMessageBox.critical(self, "错误", f"刷新预览图时发生错误,已使用默认图标:\n{str(e)}")
UniversalMessageDialog.show_error(self,
"错误",
f"刷新预览图时发生错误,已使用默认图标:\n{str(e)}",
False, "确定")
finally:
# 恢复按钮状态
if target_btn:
target_btn.setEnabled(True)
target_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):
"""创建默认预览图 - 使用project_empty_icon"""
try:
# 首先尝试使用project_empty_icon
if IconManager.icon_exists('project_empty_icon'):
# 创建预览图存储目录
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
# 获取project_empty_icon并缩放到预览尺寸
empty_icon = IconManager.get_pixmap('project_empty_icon', QSize(400, 300))
if not empty_icon.isNull():
# 保存预览图
if empty_icon.save(str(preview_path), 'PNG'):
return str(preview_path)
# 如果project_empty_icon不存在创建带项目信息的默认预览图
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):
"""更新网格视图中的图片显示"""
if self.view_mode == "grid" and hasattr(self, 'background_label'):
# 检查是否有新的图片文件
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():
scaled_pixmap = pixmap.scaled(276, 191, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation)
self.background_label.setPixmap(scaled_pixmap)
else:
self.set_default_background()
else:
self.set_default_background()
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 = 5.0 # 统一使用5px圆角
# 过缩放因子
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)