#!/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: # 首先尝试使用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}") 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)