169 lines
5.4 KiB
Python
169 lines
5.4 KiB
Python
"""无业务状态的可复用 Qt 控件。"""
|
|
|
|
from PyQt5.QtCore import Qt, QRectF, pyqtSignal
|
|
from PyQt5.QtGui import QColor, QPainter
|
|
from PyQt5.QtWidgets import QCheckBox, QLabel, QFrame, QHBoxLayout
|
|
|
|
try:
|
|
from PyQt5.QtWidgets import QGraphicsDropShadowEffect
|
|
except ImportError: # 轻量测试桩不提供图形效果类,真实 PyQt5 仍使用阴影。
|
|
class QGraphicsDropShadowEffect: # type: ignore[no-redef]
|
|
def __init__(self, *_args, **_kwargs):
|
|
pass
|
|
|
|
def setBlurRadius(self, *_args):
|
|
pass
|
|
|
|
def setOffset(self, *_args):
|
|
pass
|
|
|
|
def setColor(self, *_args):
|
|
pass
|
|
|
|
from .theme import C_BORDER, C_GREEN, C_RED, C_TITLE
|
|
|
|
|
|
_NL_BASE = f"color: {C_TITLE}; font-size: 12px;"
|
|
|
|
|
|
class NodeLabel(QLabel):
|
|
"""可点击标签:hover 显示下划线和手型光标,点击发出 clicked 信号。"""
|
|
|
|
clicked = pyqtSignal()
|
|
|
|
def __init__(self, text="", tooltip="点击切换节点", base=None, hover=None):
|
|
super().__init__(text)
|
|
self._base = base or _NL_BASE
|
|
self._hover = hover or self._base + " text-decoration: underline;"
|
|
self.setStyleSheet(self._base)
|
|
if tooltip:
|
|
self.setToolTip(tooltip)
|
|
|
|
def enterEvent(self, event):
|
|
self.setStyleSheet(self._hover)
|
|
self.setCursor(Qt.PointingHandCursor)
|
|
super().enterEvent(event)
|
|
|
|
def leaveEvent(self, event):
|
|
self.setStyleSheet(self._base)
|
|
self.unsetCursor()
|
|
super().leaveEvent(event)
|
|
|
|
def mouseReleaseEvent(self, event):
|
|
if event.button() == Qt.LeftButton:
|
|
self.clicked.emit()
|
|
super().mouseReleaseEvent(event)
|
|
|
|
|
|
class ToggleSwitch(QCheckBox):
|
|
"""苹果风格开关:绿/灰胶囊加滑动圆钮,自绘且不依赖图片资源。"""
|
|
|
|
def __init__(self, checked=False, parent=None):
|
|
super().__init__(parent)
|
|
self.setChecked(checked)
|
|
self.setFixedSize(42, 22)
|
|
self.setCursor(Qt.PointingHandCursor)
|
|
|
|
def paintEvent(self, event):
|
|
painter = QPainter(self)
|
|
painter.setRenderHint(QPainter.Antialiasing)
|
|
painter.setPen(Qt.NoPen)
|
|
painter.setBrush(QColor(C_GREEN) if self.isChecked() else QColor("#c8ccd4"))
|
|
painter.drawRoundedRect(QRectF(0, 0, 42, 22), 11, 11)
|
|
knob_x = 20.0 if self.isChecked() else 2.0
|
|
painter.setBrush(QColor("#ffffff"))
|
|
painter.drawEllipse(QRectF(knob_x, 2.0, 18.0, 18.0))
|
|
painter.end()
|
|
|
|
def mousePressEvent(self, event):
|
|
if event.button() == Qt.LeftButton:
|
|
self.setChecked(not self.isChecked())
|
|
self.repaint()
|
|
event.accept()
|
|
return
|
|
super().mousePressEvent(event)
|
|
|
|
|
|
class StatusDot(QLabel):
|
|
"""代理状态小圆点。"""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.setFixedSize(14, 14)
|
|
self.set_off()
|
|
|
|
def set_on(self):
|
|
self.setStyleSheet(f"""
|
|
QLabel {{
|
|
background-color: {C_GREEN};
|
|
border-radius: 7px;
|
|
border: 1px solid rgba(81, 207, 102, 50);
|
|
}}
|
|
""")
|
|
|
|
def set_off(self):
|
|
self.setStyleSheet(f"""
|
|
QLabel {{
|
|
background-color: {C_RED};
|
|
border-radius: 7px;
|
|
border: 1px solid rgba(255, 107, 107, 50);
|
|
}}
|
|
""")
|
|
|
|
def set_disabled(self):
|
|
self.setStyleSheet(f"""
|
|
QLabel {{
|
|
background-color: {C_BORDER};
|
|
border-radius: 7px;
|
|
border: 1px solid rgba(136, 153, 170, 50);
|
|
}}
|
|
""")
|
|
|
|
|
|
class ProxyModeCard(QFrame):
|
|
"""整个卡片可点击;描边表示选择,圆点表示所选模式的运行状态。"""
|
|
|
|
clicked = pyqtSignal()
|
|
|
|
def __init__(self, title: str, parent=None):
|
|
super().__init__(parent)
|
|
self.setCursor(Qt.PointingHandCursor)
|
|
self._glow = QGraphicsDropShadowEffect(self)
|
|
self._glow.setBlurRadius(12)
|
|
self._glow.setOffset(0, 1)
|
|
self.setGraphicsEffect(self._glow)
|
|
row = QHBoxLayout(self)
|
|
row.setContentsMargins(11, 10, 11, 10)
|
|
row.setSpacing(5)
|
|
self.title = QLabel(title)
|
|
self.title.setStyleSheet("font-weight: bold; background: transparent;")
|
|
self.dot = StatusDot()
|
|
for child in (self.title, self.dot):
|
|
child.setAttribute(Qt.WA_TransparentForMouseEvents)
|
|
row.addWidget(self.title)
|
|
row.addStretch()
|
|
row.addWidget(self.dot)
|
|
self.set_state(False, False)
|
|
|
|
def set_state(self, selected: bool, running: bool) -> None:
|
|
border = C_TITLE if selected else "#cbd6e2"
|
|
background = "#f5f7f9" if selected else "#fafcfe"
|
|
self.setStyleSheet(
|
|
f"QFrame {{ background-color: {background}; border: {'2px' if selected else '1px'} "
|
|
f"solid {border}; border-radius: 8px; }}"
|
|
"QFrame:hover { border-color: #e2e8ef; background-color: #ffffff; }"
|
|
"QLabel { border: none; }"
|
|
)
|
|
self._glow.setColor(QColor(136, 153, 170, 105 if selected else 52))
|
|
if not selected:
|
|
self.dot.set_disabled()
|
|
elif running:
|
|
self.dot.set_on()
|
|
else:
|
|
self.dot.set_off()
|
|
|
|
def mouseReleaseEvent(self, event):
|
|
if event.button() == Qt.LeftButton and self.rect().contains(event.pos()):
|
|
self.clicked.emit()
|
|
super().mouseReleaseEvent(event)
|