diff --git a/.gitignore b/.gitignore index 9740f15468470f6c303379e4b36266fb3090d627..7b76246d9d1d51f74c6f57b7480f5d77a165d0e9 100644 --- a/.gitignore +++ b/.gitignore @@ -138,3 +138,6 @@ dmypy.json cython_debug/ *.deb + +# References +curly_ref/* diff --git a/Makefile b/Makefile index b90dfee98c56896f4c3666b74d33162eb6357bd6..f52bc1f4500702964ff7cd376bacbe6777d1622e 100644 --- a/Makefile +++ b/Makefile @@ -8,9 +8,11 @@ install: mkdir -pv $(DESTDIR)/usr/share/gxde-hardware-viewer/translations mkdir -pv $(DESTDIR)/usr/bin mkdir -pv $(DESTDIR)/usr/share/polkit-1/actions/ + mkdir -pv $(DESTDIR)/usr/share/gxde-hardware-viewer/icons cp -rv polkit/* $(DESTDIR)/usr/share/polkit-1/actions/ cp -rv gxde-hardware-viewer.desktop $(DESTDIR)/usr/share/applications cp -rv gxde-hardware-viewer.py $(DESTDIR)/usr/bin/gxde-hardware-viewer cp -rv gxde-hardware-viewer-helper.sh $(DESTDIR)/usr/bin/gxde-hardware-viewer-helper cp -rv translations/*.qm $(DESTDIR)/usr/share/gxde-hardware-viewer/translations + cp -rv icons/*.svg $(DESTDIR)/usr/share/gxde-hardware-viewer/icons cp -rv gxde-logo_new.png $(DESTDIR)/usr/share/gxde-hardware-viewer \ No newline at end of file diff --git a/debian/changelog b/debian/changelog index 499977ad48ef369cef33a42f70b4017385a6d505..30d86ee7f643c23a63258ea70e725ca70bd6f7bf 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,12 @@ +gxde-hardware-viewer (2.6.2-1) UNRELEASED; urgency=medium + + [ CharOfString ] + * 新增仿DTK2风格的标题栏与侧边栏 + * 使UI圆角跟随GXDE系统设置(非GXDE/检测失败时使用8px) + * 新增了一些图标 + + -- CharOfString Wed, 29 Apr 2026 19:47:21 -0500 + gxde-hardware-viewer (2.6.1-2) UNRELEASED; urgency=medium [ zeqi ] diff --git a/debug_copy_icon_resources.sh b/debug_copy_icon_resources.sh new file mode 100755 index 0000000000000000000000000000000000000000..e8c75d9965d7bfbf3918c0191f006a0fcf8c2426 --- /dev/null +++ b/debug_copy_icon_resources.sh @@ -0,0 +1,3 @@ +# Run using sudo +mkdir -p /usr/share/gxde-hardware-viewer/icons +cp -rv icons/*.svg /usr/share/gxde-hardware-viewer/icons diff --git a/gxde-hardware-viewer.py b/gxde-hardware-viewer.py index 78d1267fe3dd1dd220c75af7fc360671f1a9cd34..19930caf28d18d009c2a1df354e6bf744739c67d 100644 --- a/gxde-hardware-viewer.py +++ b/gxde-hardware-viewer.py @@ -15,14 +15,202 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QListWidget, QListWidgetItem, QStackedWidget, QLabel, QGroupBox, QFormLayout, QTextEdit, QFileDialog, QTableWidget, QTableWidgetItem, QProgressBar, QFrame, - QPushButton, QMenu, QMessageBox, QAbstractItemView, QDialog, QDialogButtonBox) -from PyQt6.QtCore import Qt, QTimer, QTranslator, QCoreApplication, QLocale, QThread, pyqtSignal, QProcess, QSettings -from PyQt6.QtGui import QColor, QIcon, QFont, QPainter, QPalette, QPixmap + QPushButton, QMenu, QMessageBox, QAbstractItemView, QDialog, QDialogButtonBox, QScrollArea, QToolButton) +from PyQt6.QtCore import Qt, QTimer, QTranslator, QCoreApplication, QLocale, QThread, pyqtSignal, QProcess, QSettings, QRect, QRectF, QPoint, QEvent, QSize +from PyQt6.QtGui import QColor, QIcon, QFont, QPainter, QPalette, QPixmap, QImage, QFontMetrics, QPainterPath, QRegion, QPen +from enum import Enum +import dbus version = "2.6.1-2" uname = platform.uname() +class SettingsUtils(): + @staticmethod + # 从GXDE设置获取窗口圆角设置;若获取失败或非GXDE则默认返回8 + def get_window_radius() -> int: + try: + bus = dbus.SessionBus() + obj = bus.get_object( + "com.gxde.daemon.personalization", + "/com/gxde/daemon/personalization" + ) + interface = dbus.Interface(obj, dbus_interface="com.gxde.daemon.personalization") + resultGen = int(interface.Radius()) + print(f"GetWindowRadius: Captured window radius: {resultGen}.") + return resultGen + + except Exception as e: + print(f"D-Bus service: Failed to capture window radius: {e}") + print(f"GetWindowRadius: As a result, 8 is returned as radius.") + return 8 + +# 标题栏按钮类型 +class TitleBarBtnType(Enum): + MINIMIZE = 0 + MAXIMIZE = 1 + CLOSE = 2 + MENU = 3 + +# 标题栏按钮(最大化、最小化、关闭、菜单) +class TitleBarBtns(QWidget): + _ICON_PREFIX = { + TitleBarBtnType.MINIMIZE: "minimize", + TitleBarBtnType.MAXIMIZE: "maximize", + TitleBarBtnType.CLOSE: "close", + TitleBarBtnType.MENU: "menu", + } + + clicked = pyqtSignal() + + def __init__(self, parent=None, btnType=TitleBarBtnType.CLOSE): + super().__init__(parent) + self.parent = parent + self.btnType = btnType + self._menu = None + + # 初始化缩放因子 + self.scaling_factor = parent.scaling_factor if hasattr(parent, 'scaling_factor') else 1.0 + + # 绘制局部 + self.setFixedSize(self.scaled(40), self.scaled(40)) + self.layoutGen = QHBoxLayout(self) + self.layoutGen.setContentsMargins(0, 0, 0, 0) + + self.btn = QToolButton(self) + self.btn.setFixedSize(self.scaled(30), self.scaled(30)) + self.btn.setAutoRaise(True) + self.btn.setCursor(Qt.CursorShape.PointingHandCursor) + self.btn.setStyleSheet("QToolButton { border: none; background: transparent; }") + self.layoutGen.addWidget(self.btn, 0, Qt.AlignmentFlag.AlignCenter) + + # 疑似对于标题栏来说最大化按钮太大了 + if self.btnType == TitleBarBtnType.MAXIMIZE: + self.btn.setIconSize(QSize(self.scaled(16), self.scaled(16))) + else: + self.btn.setIconSize(QSize(self.scaled(20), self.scaled(20))) + + # 状态跟踪,用以切换 default / hover / pressed 图标 + self._hovered = False + self._pressed = False + self.btn.installEventFilter(self) + + self._refresh_icon() + + # 系统主题切换时刷新图标 + QApplication.styleHints().colorSchemeChanged.connect(self._refresh_icon) + + # 行为绑定 + self.btn.clicked.connect(self._on_clicked) + + def scaled(self, value): + return int(value * self.scaling_factor) + + def _is_dark(self) -> bool: + return QApplication.styleHints().colorScheme() == Qt.ColorScheme.Dark + + def _icon_path(self, state: str) -> str: + prefix = self._ICON_PREFIX[self.btnType] + theme = "dark" if self._is_dark() else "light" + suffix = f"_{state}_{theme}" if state else f"_{theme}" + filename = f"{prefix}{suffix}.svg" + for base in ( + "/usr/share/gxde-hardware-viewer/icons", + os.path.join(os.path.dirname(os.path.abspath(__file__)), "icons"), + ): + path = os.path.join(base, filename) + if os.path.exists(path): + return path + return "" + + def _refresh_icon(self): + if self._pressed: + state = "pressed" + elif self._hovered: + state = "hover" + else: + state = "" + path = self._icon_path(state) + if path: + self.btn.setIcon(QIcon(path)) + self.update() + + def paintEvent(self, event): + # 深色模式下保持透明背景 + if self._is_dark(): + return + if self._pressed: + bg = QColor("#EAEAED") + elif self._hovered: + bg = QColor("#EFEFF2") + else: + return + + painter = QPainter(self) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + + # 关闭按钮位于标题栏右上角,需匹配窗口圆角 + if self.btnType == TitleBarBtnType.CLOSE: + radius = SettingsUtils().get_window_radius() + rect = QRectF(self.rect()) + path = QPainterPath() + if radius > 0: + path.moveTo(rect.left(), rect.top()) + path.lineTo(rect.right() - radius, rect.top()) + path.quadTo(rect.right(), rect.top(), rect.right(), rect.top() + radius) + path.lineTo(rect.right(), rect.bottom()) + path.lineTo(rect.left(), rect.bottom()) + path.closeSubpath() + else: + path.addRect(rect) + painter.fillPath(path, bg) + else: + painter.fillRect(self.rect(), bg) + + def eventFilter(self, obj, event): + if obj is self.btn: + t = event.type() + if t == QEvent.Type.Enter: + self._hovered = True + self._refresh_icon() + elif t == QEvent.Type.Leave: + self._hovered = False + self._pressed = False + self._refresh_icon() + elif t == QEvent.Type.MouseButtonPress: + self._pressed = True + self._refresh_icon() + elif t == QEvent.Type.MouseButtonRelease: + self._pressed = False + self._refresh_icon() + return super().eventFilter(obj, event) + + def setMenu(self, menu): + self._menu = menu + + def menu(self): + return self._menu + + def _on_clicked(self): + win = self.window() + if self.btnType == TitleBarBtnType.MINIMIZE: + win.showMinimized() + elif self.btnType == TitleBarBtnType.MAXIMIZE: + if win.isMaximized(): + win.showNormal() + else: + win.showMaximized() + self._refresh_icon() + elif self.btnType == TitleBarBtnType.CLOSE: + win.close() + elif self.btnType == TitleBarBtnType.MENU: + if self._menu is not None: + pos = self.mapToGlobal(QPoint(0, self.height())) + self._menu.exec(pos) + self.clicked.emit() + + + class GXDETitleBar(QWidget): def __init__(self, parent=None): super().__init__(parent) @@ -39,8 +227,22 @@ class GXDETitleBar(QWidget): self.layout.setSpacing(self.scaled(8)) # 1. 设置标题栏布局 - self.layout.setContentsMargins(self.scaled(12), self.scaled(8), self.scaled(12), self.scaled(8)) + self.layout.setContentsMargins(0, 0, 0, 0) self.layout.setSpacing(self.scaled(15)) + + self.titleBarInnerLayout = QHBoxLayout(self) + self.titleBarInnerLayout.setContentsMargins(self.scaled(12), self.scaled(8), self.scaled(12), self.scaled(8)) + self.layout.addLayout(self.titleBarInnerLayout) + + # 1.1 设定标题栏颜色 + self.lightBg = QColor("#FBFBFB") + self.DarkBg = QColor("#050505") + self.lightBorder = QColor(0, 0, 0, 25) + self.darkBorder = QColor(255, 255, 255, 50) + + + # 1.2 标题栏高度对齐文件管理器 + self.setFixedHeight(self.scaled(40)) # 2. 左侧:窗口标题标签 app_icon = QIcon.fromTheme("utilities-system-monitor") @@ -51,57 +253,26 @@ class GXDETitleBar(QWidget): self.title_icon_label.setPixmap(icon_pixmap) self.title_icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.title_icon_label.setFixedSize(self.scaled(24), self.scaled(24)) - self.layout.addWidget(self.title_icon_label) - self.layout.addStretch() + self.titleBarInnerLayout.addWidget(self.title_icon_label) + self.titleBarInnerLayout.addStretch() # 3. 右侧:菜单按钮 - self.menu_button = QPushButton("☰") - self.menu_button.setFixedSize(self.scaled(24), self.scaled(24)) - self.menu_button.setStyleSheet(f""" - QPushButton {{ - border: none; - border-radius: {self.scaled(4)}px; - background-color: transparent; - font-size: {self.scaled(16)}px; - }} - QPushButton::menu-indicator {{ - image: none; - width: 0px; - }} - QPushButton:hover {{ - background-color: grey; - }} - QPushButton:pressed {{ - color: white; - background-color: #F380A6 - }} - """) - self.layout.addWidget(self.menu_button) + self.titleBarBtnLayout = QHBoxLayout() + self.titleBarBtnLayout.setContentsMargins(0, 0, 0, 0) + self.titleBarBtnLayout.setSpacing(0) + + self.menu_button = TitleBarBtns(self, TitleBarBtnType.MENU) + self.titleBarBtnLayout.addWidget(self.menu_button) # 4. 右侧:窗口控制按钮 - self.min_btn = self.create_gxde_control_btn("—") - self.max_btn = self.create_gxde_control_btn("□") + self.min_btn = TitleBarBtns(self, TitleBarBtnType.MINIMIZE) + self.max_btn = TitleBarBtns(self, TitleBarBtnType.MAXIMIZE) + self.close_btn = TitleBarBtns(self, TitleBarBtnType.CLOSE) - self.close_btn = self.create_gxde_control_btn("×") - # 关闭按钮样式 - self.close_btn.setStyleSheet(f""" - QPushButton {{ - border: none; - border-radius: {self.scaled(4)}px; - background-color: transparent; - font-size: {self.scaled(14)}px; - }} - QPushButton:hover {{ - background-color: #E6004C; - color: white; - }} - QPushButton:pressed {{ - background-color: #cc0000; - }} - """) - self.layout.addWidget(self.min_btn) - self.layout.addWidget(self.max_btn) - self.layout.addWidget(self.close_btn) + self.titleBarBtnLayout.addWidget(self.min_btn) + self.titleBarBtnLayout.addWidget(self.max_btn) + self.titleBarBtnLayout.addWidget(self.close_btn) + self.layout.addLayout(self.titleBarBtnLayout) # 5. 绑定窗口控制按钮事件 self.min_btn.clicked.connect(self.parent.showMinimized) @@ -154,6 +325,45 @@ class GXDETitleBar(QWidget): return super().mousePressEvent(event) + # 6. 重载绘制函数 + # 模仿DTK2.0时代的标题栏 + def is_dark_mode(self) -> bool: + # 需要Qt 6.5+ + scheme = QApplication.styleHints().colorScheme() + return scheme == Qt.ColorScheme.Dark + + def paintEvent(self, event): + painter = QPainter(self) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + + # 处理背景色 + if self.is_dark_mode(): + bgColorGen = self.DarkBg + borderColorGen = self.darkBorder + else: + bgColorGen = self.lightBg + borderColorGen = self.lightBorder + + # 处理窗口圆角 + radius = SettingsUtils().get_window_radius() + rect = QRectF(self.rect()) + path = QPainterPath() + if radius > 0: + path.moveTo(rect.left(), rect.bottom()) + path.lineTo(rect.left(), rect.top() + radius) + path.quadTo(rect.left(), rect.top(), rect.left() + radius, rect.top()) + path.lineTo(rect.right() - radius, rect.top()) + path.quadTo(rect.right(), rect.top(), rect.right(), rect.top() + radius) + path.lineTo(rect.right(), rect.bottom()) + path.closeSubpath() + else: + path.addRect(rect) + + painter.fillPath(path, bgColorGen) + + # 处理衬线 + painter.fillRect(0, self.height() - 1, self.width(), 1, borderColorGen) + class CacheManager: """缓存管理器""" def __init__(self, default_ttl=300): @@ -232,9 +442,27 @@ class CentralWidget(QWidget): self.overlay_color = QColor(255, 255, 255, 125) def paintEvent(self, event): - super().paintEvent(event) painter = QPainter(self) - + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + + # 用窗口圆角裁剪绘制区域,实现抗锯齿圆角 + win = self.window() + radius = win.corner_radius() if hasattr(win, 'corner_radius') else 0 + rect = QRectF(self.rect()) + path = QPainterPath() + if radius > 0: + path.addRoundedRect(rect, radius, radius) + else: + path.addRect(rect) + painter.setClipPath(path) + + # 填充背景色 + if self.bg_image_path and os.path.exists(self.bg_image_path): + painter.fillPath(path, self.palette().color(QPalette.ColorRole.Window)) + else: + is_dark = self.palette().color(QPalette.ColorRole.Window).lightness() < 128 + painter.fillPath(path, QColor("#202020") if is_dark else QColor("#FFFFFF")) + if self.bg_image_path and os.path.exists(self.bg_image_path): # 获取当前控件的逻辑尺寸 target_size = self.size() @@ -254,7 +482,7 @@ class CentralWidget(QWidget): scaled.setDevicePixelRatio(dpr) self.cached_scaled_pixmap = scaled self.cached_size = target_size - + if self.cached_scaled_pixmap is not None: # 计算居中偏移量,使图片中心与控件中心对齐 pixmap_size = self.cached_scaled_pixmap.size() @@ -266,7 +494,291 @@ class CentralWidget(QWidget): # 绘制半透明遮罩层 if self.overlay_enabled: - painter.fillRect(self.rect(), self.overlay_color) + painter.fillPath(path, self.overlay_color) + + +# Mod: 顶层透明层,负责窗体衬线 +class WindowBorderOverlay(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents) + self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground) + self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + + def paintEvent(self, event): + win = self.window() + if hasattr(win, 'isMaximized') and (win.isMaximized() or win.isFullScreen()): + return + painter = QPainter(self) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + radius = win.corner_radius() if hasattr(win, 'corner_radius') else 0 + isDark = self.palette().color(QPalette.ColorRole.Window).lightness() < 128 + borderColor = QColor(255, 255, 255, 50) if isDark else QColor(0, 0, 0, 25) + borderRect = QRectF(self.rect()).adjusted(0.5, 0.5, -0.5, -0.5) + borderPath = QPainterPath() + if radius > 0: + innerRadius = max(radius - 0.5, 0.0) + borderPath.addRoundedRect(borderRect, innerRadius, innerRadius) + else: + borderPath.addRect(borderRect) + painter.setPen(QPen(borderColor, 1)) + painter.setBrush(Qt.BrushStyle.NoBrush) + painter.drawPath(borderPath) + + +# SideBarItem类,移植自MarcusPy827/Curly +class SideBarItem(QWidget): + itemClicked = pyqtSignal(int) + + def __init__(self, icon_name, text, index, width_override=-1, parent=None): + super().__init__(parent) + self._index = index + self._text = text + self._icon_name = icon_name + self._width_override = width_override if width_override > 0 else -1 + self._is_checked = False + self._is_hovered = False + + # Mod: 窗口自定义背景启用状态 + self._bg_active = False + + self.setFixedHeight(30) + self.setCursor(Qt.CursorShape.PointingHandCursor) + self.setAttribute(Qt.WidgetAttribute.WA_Hover) + + def setChecked(self, is_checked): + self._is_checked = is_checked + self.update() + + # Mod: 通过SideBar的信号驱动,告知是否启用自定义背景图 + def setBackgroundActive(self, active): + if self._bg_active == bool(active): + return + self._bg_active = bool(active) + self.update() + + def getIndex(self): + return self._index + + def setText(self, text): + self._text = text + self.update() + + def mousePressEvent(self, event): + if event.button() == Qt.MouseButton.LeftButton: + self.itemClicked.emit(self._index) + super().mousePressEvent(event) + + def enterEvent(self, event): + self._is_hovered = True + self.update() + super().enterEvent(event) + + def leaveEvent(self, event): + self._is_hovered = False + self.update() + super().leaveEvent(event) + + # Mod: 渲染内置图标,而不是走XDG Icon + def renderTintedIcon(self, size, color): + filename = f"{self._icon_name}.svg" + path = "" + for base in ( + "/usr/share/gxde-hardware-viewer/icons", + os.path.join(os.path.dirname(os.path.abspath(__file__)), "icons"), + ): + candidate = os.path.join(base, filename) + if os.path.exists(candidate): + path = candidate + break + if not path: + return None + try: + with open(path, "rb") as f: + data = f.read() + except OSError: + return None + data = data.replace(b"currentColor", color.name().encode("ascii")) + dpr = self.devicePixelRatioF() + physical = max(1, int(round(size * dpr))) + pixmap = QPixmap() + if not pixmap.loadFromData(data, "svg"): + return None + if pixmap.width() != physical or pixmap.height() != physical: + pixmap = pixmap.scaled( + physical, physical, + Qt.AspectRatioMode.KeepAspectRatio, + Qt.TransformationMode.SmoothTransformation, + ) + pixmap.setDevicePixelRatio(dpr) + return pixmap + + def paintEvent(self, event): + painter = QPainter(self) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + + item_padding = 13 + icon_size = 16 + icon_text_gap = 8 + check_mark_border = 3 + + is_dark = self.palette().color(QPalette.ColorRole.Window).lightness() < 100 + + if self._is_checked: + bg_color = QColor(230, 0, 76, 80) if is_dark else QColor(230, 0, 76, 60) + text_color = QColor(230, 0, 76) + painter.setFont(QFont("Sans", 9, QFont.Weight.Bold)) + elif self._is_hovered: + bg_color = QColor(255, 255, 255, 30) if is_dark else QColor(0, 0, 0, 20) + text_color = QColor(220, 220, 220) if is_dark else QColor(0, 0, 0, 204) + painter.setFont(QFont("Sans", 9)) + else: + # Mod: 背景图片启用时,未选中时Item底色交由SideBar负责,不再由Item自行负责 + bg_color = QColor(0, 0, 0, 0) + text_color = QColor(220, 220, 220) if is_dark else QColor(0, 0, 0, 204) + painter.setFont(QFont("Sans", 9)) + + if bg_color.alpha() > 0: + painter.fillRect(self.rect(), bg_color) + + icon_top = (self.height() - icon_size) // 2 + icon_rect = QRect(item_padding, icon_top, icon_size, icon_size) + pixmap = self.renderTintedIcon(icon_size, text_color) + if pixmap is not None: + painter.drawPixmap(icon_rect, pixmap) + + painter.setPen(text_color) + text_padding_left = item_padding + icon_size + icon_text_gap + text_rect = QRect(text_padding_left, 0, + self.width() - text_padding_left - icon_text_gap, + self.height()) + fm = QFontMetrics(painter.font()) + elided = fm.elidedText(self._text, Qt.TextElideMode.ElideRight, text_rect.width()) + painter.drawText(text_rect, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, elided) + + if self._is_checked: + x = (self._width_override if self._width_override > 0 else self.width()) - check_mark_border + painter.fillRect(x, 0, check_mark_border, self.height(), text_color) + + +# Mod: 将 CentralWidget 的窗口背景图按当前 widget 在窗口中的位置绘制到 painter +def _paint_central_bg(widget, painter): + window = widget.window() + if window is None: + return False + central = window.centralWidget() if hasattr(window, "centralWidget") else None + if not isinstance(central, CentralWidget): + return False + pixmap = getattr(central, "cached_scaled_pixmap", None) + if pixmap is None or pixmap.isNull(): + return False + target_size = central.size() + dpr = central.devicePixelRatioF() + pix_w = pixmap.width() + pix_h = pixmap.height() + cx = (target_size.width() * dpr - pix_w) / 2 + cy = (target_size.height() * dpr - pix_h) / 2 + tl = widget.mapTo(central, QPoint(0, 0)) + painter.drawPixmap(int(cx - tl.x()), int(cy - tl.y()), pixmap) + if getattr(central, "overlay_enabled", False): + painter.fillRect(widget.rect(), central.overlay_color) + return True + + +# SideBar类,移植自MarcusPy827/Curly +class SideBar(QWidget): + sideBarItemClicked = pyqtSignal(int) + # Mod: 通知Item窗体背景已被设置 + backgroundActiveChanged = pyqtSignal(bool) + + def __init__(self, item_width_override=-1, parent=None): + super().__init__(parent) + + self._width_override = item_width_override if item_width_override > 0 else -1 + self._item_list = [] + self._bg_active = False # Mod: 窗体背景启用状态 + + self._layout = QVBoxLayout(self) + self._layout.setSpacing(0) + self._layout.setContentsMargins(0, 0, 0, 0) + self._layout.setAlignment(Qt.AlignmentFlag.AlignTop) + + # Mod: SideBar加入paintEvent重载以支持在自定义背景下实现半透明侧栏 + def paintEvent(self, event): + painter = QPainter(self) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + is_dark = self.palette().color(QPalette.ColorRole.Window).lightness() < 128 + + # 处理窗口圆角 + radius = SettingsUtils().get_window_radius() + rect = QRectF(self.rect()) + path = QPainterPath() + if radius > 0: + path.moveTo(rect.left(), rect.top()) + path.lineTo(rect.right(), rect.top()) + path.lineTo(rect.right(), rect.bottom()) + path.lineTo(rect.left() + radius, rect.bottom()) + path.quadTo(rect.left(), rect.bottom(), rect.left(), rect.bottom() - radius) + path.closeSubpath() + else: + path.addRect(rect) + + if self._bg_active: + painter.save() + painter.setClipPath(path) + _paint_central_bg(self, painter) + overlay = QColor(37, 37, 37, 102) if is_dark else QColor(249, 249, 250, 102) + painter.fillPath(path, overlay) + painter.restore() + else: + color = QColor("#222222") if is_dark else QColor("#FDFDFD") + painter.fillPath(path, color) + + # 处理衬线 + borderColor = QColor(255, 255, 255, 50) if is_dark else QColor(0, 0, 0, 25) + painter.fillRect(self.width() - 1, 0, 1, self.height(), borderColor) + + super().paintEvent(event) + + # Mod: 启用/关闭窗口背景模式,刷新自身和所有子Item + def setBackgroundActive(self, active): + active = bool(active) + if self._bg_active == active: + return + self._bg_active = active + self.update() + self.backgroundActiveChanged.emit(active) + + def isBackgroundActive(self): + return self._bg_active + + def addItem(self, icon_name, text, index): + item = SideBarItem(icon_name, text, index, self._width_override, self) + self._item_list.append(item) + self._layout.addWidget(item) + item.itemClicked.connect(lambda i, it=item: self._on_item_clicked(i, it)) + # Mod:设置状态跟随 + self.backgroundActiveChanged.connect(item.setBackgroundActive) + item.setBackgroundActive(self._bg_active) + + def _on_item_clicked(self, index, item): + self.setCurrentItem(item) + self.sideBarItemClicked.emit(index) + + def setCurrentItem(self, item): + for it in self._item_list: + it.setChecked(it is item) + + def setCurrentIndex(self, index): + for it in self._item_list: + it.setChecked(it.getIndex() == index) + + def item(self, index): + for it in self._item_list: + if it.getIndex() == index: + return it + return None + class HardwareManager(QMainWindow): def __init__(self): @@ -350,9 +862,37 @@ class HardwareManager(QMainWindow): def scaled(self, value): """根据缩放因子缩放数值""" return int(value * self.scaling_factor) - + + def corner_radius(self): + """读取GXDE设置中的窗口圆角半径,最大化时则返回0""" + if self.isMaximized() or self.isFullScreen(): + return 0 + return SettingsUtils().get_window_radius() + + def changeEvent(self, event): + if event.type() == QEvent.Type.WindowStateChange: + # 处理圆角 + self.update() + for w in (getattr(self, 'gxde_title_bar', None), + getattr(self, 'sidebar', None), + getattr(self, 'border_overlay', None), + self.centralWidget()): + if w is not None: + w.update() + super().changeEvent(event) + + def resizeEvent(self, event): + super().resizeEvent(event) + # Mod: 更新顶层透明层尺寸 + overlay = getattr(self, 'border_overlay', None) + cw = self.centralWidget() + if overlay is not None and cw is not None: + overlay.setGeometry(cw.rect()) + overlay.raise_() + def initUI(self): self.setWindowFlags(Qt.WindowType.FramelessWindowHint) + self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) self.setWindowTitle(self.tr("GXDE Hardware Manager")) self.resize(self.scaled(900), self.scaled(600)) @@ -378,41 +918,20 @@ class HardwareManager(QMainWindow): content_layout.setSpacing(0) # 3. 创建侧边栏 - self.sidebar = QListWidget() - self.sidebar.setFixedWidth(self.scaled(180)) - self.sidebar.setSpacing(self.scaled(2)) - self.sidebar.setStyleSheet(f""" - QListWidget {{ - padding-top: {self.scaled(10)}px; - border-right: none; - border-top: none; - }} - QListWidgetItem {{ - height: {self.scaled(36)}px; - padding-left: {self.scaled(15)}px; - font-size: {self.scaled(14)}px; - }} - QListWidget::item:selected {{ - color: #E6004C; - selection-background-color: #F380A6; - border-left: 3px solid #E6004C; - }} - QListWidget::item:hover:!selected {{ - color: grey; - border-left: 3px solid grey; - }} - """) - + sidebar_width = self.scaled(200) + self.sidebar = SideBar(sidebar_width, self) + self.sidebar.setFixedWidth(sidebar_width) + # 4. 添加侧边栏项目 - self.add_sidebar_item(self.tr("System"), "computer") + self.add_sidebar_item(self.tr("System"), "system_information") self.add_sidebar_item(self.tr("CPU"), "cpu") - self.add_sidebar_item(self.tr("Memory"), "memory") - self.add_sidebar_item(self.tr("Storage"), "disk-quota") + self.add_sidebar_item(self.tr("Memory"), "ram") + self.add_sidebar_item(self.tr("Storage"), "rom") self.add_sidebar_item(self.tr("Network"), "network") self.add_sidebar_item(self.tr("Display"), "display") self.add_sidebar_item(self.tr("Sound"), "sound") - self.add_sidebar_item(self.tr("Input Devices"), "dialog-input-devices") - self.add_sidebar_item(self.tr("Driver Update"), "system-upgrade") + self.add_sidebar_item(self.tr("Input Devices"), "input_device") + self.add_sidebar_item(self.tr("Driver Update"), "driver_update") # 5. 创建主内容区域 self.stack = QStackedWidget() @@ -436,8 +955,9 @@ class HardwareManager(QMainWindow): main_layout.addWidget(content_widget, 1) # 9. 连接信号 - self.sidebar.currentRowChanged.connect(self.stack.setCurrentIndex) - self.sidebar.setCurrentRow(0) + self.sidebar.sideBarItemClicked.connect(self.stack.setCurrentIndex) + self.sidebar.setCurrentIndex(0) + self.stack.setCurrentIndex(0) # 10. 设置菜单 self.menu = QMenu() @@ -457,6 +977,12 @@ class HardwareManager(QMainWindow): # 12. 设置文本选择功能 self.setup_text_selection() + # Mod: 顶层透明层,负责窗体衬线,包住整个窗口 + self.border_overlay = WindowBorderOverlay(central_widget) + self.border_overlay.setGeometry(central_widget.rect()) + self.border_overlay.raise_() + self.border_overlay.show() + def setup_text_selection(self): """设置文本选中复制功能""" for widget in self.findChildren(QLabel): @@ -729,33 +1255,15 @@ class HardwareManager(QMainWindow): return self.centralWidget().set_background_image(path) - self.sidebar.setStyleSheet(f""" - QListWidget {{ - padding-top: {self.scaled(10)}px; - border-right: none; - border-top: none; - }} - QListWidgetItem {{ - height: {self.scaled(36)}px; - padding-left: {self.scaled(15)}px; - font-size: {self.scaled(14)}px; - }} - QListWidget::item:selected {{ - color: #E6004C; - selection-background-color: #F380A6; - border-left: 3px solid #E6004C; - }} - QListWidget::item:hover:!selected {{ - color: grey; - border-left: 3px solid grey; - }} - """) - - self.sidebar.viewport().setStyleSheet("background-color: transparent;") + # Mod:发送信号通知SideBar背景变更 + self.sidebar.setBackgroundActive(True) def remove_background_image(self): """移除背景图片""" self.centralWidget().set_background_image("") + + # Mod:同步通知SideBar背景变更 + self.sidebar.setBackgroundActive(False) # 清除保存的设置 settings = QSettings("GXDE", "HardwareViewer") settings.remove("background/image_path") @@ -801,11 +1309,8 @@ class HardwareManager(QMainWindow): def add_sidebar_item(self, text, icon_name): """添加侧边栏项目""" - item = QListWidgetItem(text) - # 图标大小自适应 - icon = QIcon.fromTheme(icon_name, QIcon()) - item.setIcon(icon) - self.sidebar.addItem(item) + index = len(self.sidebar._item_list) + self.sidebar.addItem(icon_name, text, index) def create_group_box(self, title, widget): """创建带标题的分组框""" diff --git a/icons/README.md b/icons/README.md new file mode 100644 index 0000000000000000000000000000000000000000..8c01eed94d6947f15d4aecf151a766c62bb84eb1 --- /dev/null +++ b/icons/README.md @@ -0,0 +1,2 @@ +# From: Tabler Icons +See https://tabler.io/icons | Icon licensed under MIT license. diff --git a/icons/close_dark.svg b/icons/close_dark.svg new file mode 100644 index 0000000000000000000000000000000000000000..c933a906d4e6f95cce214f3fae544d34bf91b854 --- /dev/null +++ b/icons/close_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/close_hover_dark.svg b/icons/close_hover_dark.svg new file mode 100644 index 0000000000000000000000000000000000000000..871c6fecd458679ff705560fae00bfa18f85d0fd --- /dev/null +++ b/icons/close_hover_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/close_hover_light.svg b/icons/close_hover_light.svg new file mode 100644 index 0000000000000000000000000000000000000000..66b50c7cf6ea6b4f9bf04819aa6ce0f1dc61d9c8 --- /dev/null +++ b/icons/close_hover_light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/close_light.svg b/icons/close_light.svg new file mode 100644 index 0000000000000000000000000000000000000000..00fc2c51b6ff17c446ef54cdc97960f3d61d4fae --- /dev/null +++ b/icons/close_light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/close_pressed_dark.svg b/icons/close_pressed_dark.svg new file mode 100644 index 0000000000000000000000000000000000000000..9281b903c42cfbad4a29e82a0c4281211b386a88 --- /dev/null +++ b/icons/close_pressed_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/close_pressed_light.svg b/icons/close_pressed_light.svg new file mode 100644 index 0000000000000000000000000000000000000000..d4dbde5689f3e418e744821f939d3296d07aa052 --- /dev/null +++ b/icons/close_pressed_light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/cpu.svg b/icons/cpu.svg new file mode 100644 index 0000000000000000000000000000000000000000..a1c4d595665324603bd95e1513ca09779aaa6d3c --- /dev/null +++ b/icons/cpu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/display.svg b/icons/display.svg new file mode 100644 index 0000000000000000000000000000000000000000..39e5375c410af8c177f81c0bdeadf4dcf8902b52 --- /dev/null +++ b/icons/display.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/driver_update.svg b/icons/driver_update.svg new file mode 100644 index 0000000000000000000000000000000000000000..b391432c1066e30495eeeeb386a0d8113d7e7bfa --- /dev/null +++ b/icons/driver_update.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/input_device.svg b/icons/input_device.svg new file mode 100644 index 0000000000000000000000000000000000000000..fd3fd6b947e2d5142f59df1640b3199a534715f6 --- /dev/null +++ b/icons/input_device.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/maximize_dark.svg b/icons/maximize_dark.svg new file mode 100644 index 0000000000000000000000000000000000000000..4f17c1ced1d5867e9999fac7fcd07bcbc2c65f8e --- /dev/null +++ b/icons/maximize_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/maximize_hover_dark.svg b/icons/maximize_hover_dark.svg new file mode 100644 index 0000000000000000000000000000000000000000..591a057f82ba57a9d0154c84c5901a18af8dee49 --- /dev/null +++ b/icons/maximize_hover_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/maximize_hover_light.svg b/icons/maximize_hover_light.svg new file mode 100644 index 0000000000000000000000000000000000000000..6ce251d7b175ba4d3b5ea6d5ea2c22f04ea61e6c --- /dev/null +++ b/icons/maximize_hover_light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/maximize_light.svg b/icons/maximize_light.svg new file mode 100644 index 0000000000000000000000000000000000000000..6ce251d7b175ba4d3b5ea6d5ea2c22f04ea61e6c --- /dev/null +++ b/icons/maximize_light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/maximize_pressed_dark.svg b/icons/maximize_pressed_dark.svg new file mode 100644 index 0000000000000000000000000000000000000000..3c0d815712cb994474cc4540571fd62d88bef8e8 --- /dev/null +++ b/icons/maximize_pressed_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/maximize_pressed_light.svg b/icons/maximize_pressed_light.svg new file mode 100644 index 0000000000000000000000000000000000000000..717f9e739132d1a4ff9673f0b9938ad1b6d4b234 --- /dev/null +++ b/icons/maximize_pressed_light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/menu_dark.svg b/icons/menu_dark.svg new file mode 100644 index 0000000000000000000000000000000000000000..f5b096df064ac6e62104a4de538d6d51fb8a7c25 --- /dev/null +++ b/icons/menu_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/menu_hover_dark.svg b/icons/menu_hover_dark.svg new file mode 100644 index 0000000000000000000000000000000000000000..1166f44c39d07b9d7ccd399926322f78500cf887 --- /dev/null +++ b/icons/menu_hover_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/menu_hover_light.svg b/icons/menu_hover_light.svg new file mode 100644 index 0000000000000000000000000000000000000000..24af7d25d4edd5c284a4114e136d6488afc37332 --- /dev/null +++ b/icons/menu_hover_light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/menu_light.svg b/icons/menu_light.svg new file mode 100644 index 0000000000000000000000000000000000000000..24af7d25d4edd5c284a4114e136d6488afc37332 --- /dev/null +++ b/icons/menu_light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/menu_pressed_dark.svg b/icons/menu_pressed_dark.svg new file mode 100644 index 0000000000000000000000000000000000000000..924a2f9cb9fa02bace7929236721013e8636c56f --- /dev/null +++ b/icons/menu_pressed_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/menu_pressed_light.svg b/icons/menu_pressed_light.svg new file mode 100644 index 0000000000000000000000000000000000000000..65cb20d03c6cd84af5d2b7cb40bfeb378642c5a7 --- /dev/null +++ b/icons/menu_pressed_light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/minimize_dark.svg b/icons/minimize_dark.svg new file mode 100644 index 0000000000000000000000000000000000000000..d2e53da82abd2050c137097b745effc7e9291f9f --- /dev/null +++ b/icons/minimize_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/minimize_hover_dark.svg b/icons/minimize_hover_dark.svg new file mode 100644 index 0000000000000000000000000000000000000000..1f27cbab1a8921c88632ac6ad30cb83c225c585a --- /dev/null +++ b/icons/minimize_hover_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/minimize_hover_light.svg b/icons/minimize_hover_light.svg new file mode 100644 index 0000000000000000000000000000000000000000..bdca6c135147d738d4a5c5ac863ad7f0dcf7bd82 --- /dev/null +++ b/icons/minimize_hover_light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/minimize_light.svg b/icons/minimize_light.svg new file mode 100644 index 0000000000000000000000000000000000000000..bdca6c135147d738d4a5c5ac863ad7f0dcf7bd82 --- /dev/null +++ b/icons/minimize_light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/minimize_pressed_dark.svg b/icons/minimize_pressed_dark.svg new file mode 100644 index 0000000000000000000000000000000000000000..e7ae2eac524a31fec8a14dd1f768806b13531ab5 --- /dev/null +++ b/icons/minimize_pressed_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/minimize_pressed_light.svg b/icons/minimize_pressed_light.svg new file mode 100644 index 0000000000000000000000000000000000000000..4413bb25ca5a4d02e6272ef549c10f02c46aff45 --- /dev/null +++ b/icons/minimize_pressed_light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/network.svg b/icons/network.svg new file mode 100644 index 0000000000000000000000000000000000000000..3178238814b70e5d294df7ac675ffc0140854bdc --- /dev/null +++ b/icons/network.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/ram.svg b/icons/ram.svg new file mode 100644 index 0000000000000000000000000000000000000000..95717a43d52b1332b2babeeaeeb6f2a2d1f0e365 --- /dev/null +++ b/icons/ram.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/rom.svg b/icons/rom.svg new file mode 100644 index 0000000000000000000000000000000000000000..c78554526f098392a3fab711e09a585bef852130 --- /dev/null +++ b/icons/rom.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/sound.svg b/icons/sound.svg new file mode 100644 index 0000000000000000000000000000000000000000..197e51915df124d5743fe5759653b9fdce958f1c --- /dev/null +++ b/icons/sound.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/system_information.svg b/icons/system_information.svg new file mode 100644 index 0000000000000000000000000000000000000000..146f4c85f18ebc31563398539ced65740d3e73ef --- /dev/null +++ b/icons/system_information.svg @@ -0,0 +1 @@ + \ No newline at end of file