209 lines
6.6 KiB
Python
209 lines
6.6 KiB
Python
from PyQt6.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, QLabel
|
||
from PyQt6.QtCore import Qt, pyqtSignal, QPoint
|
||
from PyQt6.QtGui import QColor, QTextCursor, QMouseEvent
|
||
|
||
from .config import OverlayCfg
|
||
|
||
_TITLE = "AI 答题助手"
|
||
_LOADING_TEXT = "正在分析截图,请稍候..."
|
||
|
||
|
||
class _CloseBtn(QLabel):
|
||
def __init__(self, on_click, parent=None):
|
||
super().__init__("×", parent)
|
||
self._on_click = on_click
|
||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||
|
||
def mousePressEvent(self, event) -> None:
|
||
if event.button() == Qt.MouseButton.LeftButton:
|
||
self._on_click()
|
||
|
||
|
||
class _TitleBar(QWidget):
|
||
def __init__(self, win: "OverlayWindow", cfg: OverlayCfg):
|
||
super().__init__(win)
|
||
self._win = win
|
||
self.setFixedHeight(24)
|
||
self.setStyleSheet(f"background-color: {cfg.bg_color}; border-radius: 6px 6px 0 0;")
|
||
|
||
self.setMouseTracking(True)
|
||
self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
|
||
|
||
self._drag_pos = None
|
||
|
||
layout = QHBoxLayout(self)
|
||
layout.setContentsMargins(10, 0, 6, 0)
|
||
|
||
title_lbl = QLabel(f"· {_TITLE}")
|
||
title_lbl.setStyleSheet(f"color: {cfg.accent_color}; font-size: 11px;")
|
||
# 让标签对鼠标透明,事件穿透到 _TitleBar
|
||
title_lbl.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
|
||
|
||
close_btn = _CloseBtn(win._on_hide)
|
||
close_btn.setStyleSheet(f"color: {cfg.accent_color}; padding: 0 4px; font-size: 14px;")
|
||
|
||
layout.addWidget(title_lbl)
|
||
layout.addStretch()
|
||
layout.addWidget(close_btn)
|
||
|
||
def mousePressEvent(self, event):
|
||
if event.button() == Qt.MouseButton.LeftButton:
|
||
# 👇 就是在这里用 frameGeometry()
|
||
self._drag_pos = event.globalPosition().toPoint() - self._win.frameGeometry().topLeft()
|
||
|
||
def mouseMoveEvent(self, event):
|
||
if self._drag_pos is not None and event.buttons() & Qt.MouseButton.LeftButton:
|
||
self._win.move(event.globalPosition().toPoint() - self._drag_pos)
|
||
|
||
def mouseReleaseEvent(self, event):
|
||
self._drag_pos = None
|
||
|
||
|
||
|
||
class OverlayWindow(QWidget):
|
||
_sig_append = pyqtSignal(str)
|
||
_sig_loading = pyqtSignal()
|
||
_sig_clear = pyqtSignal()
|
||
_sig_show = pyqtSignal()
|
||
_sig_hide = pyqtSignal()
|
||
_sig_toggle = pyqtSignal()
|
||
|
||
def __init__(self, cfg: OverlayCfg):
|
||
self._app = QApplication.instance() or QApplication([])
|
||
super().__init__()
|
||
self._cfg = cfg
|
||
self._drag_pos: QPoint | None = None
|
||
|
||
self._sig_append.connect(self._on_append)
|
||
self._sig_loading.connect(self._on_loading)
|
||
self._sig_clear.connect(self._on_clear)
|
||
self._sig_show.connect(self._on_show)
|
||
self._sig_hide.connect(self._on_hide)
|
||
self._sig_toggle.connect(self._on_toggle)
|
||
|
||
self._setup_window()
|
||
self._setup_ui()
|
||
self._position_bottom_right()
|
||
self.show()
|
||
|
||
def _setup_window(self) -> None:
|
||
self.setWindowFlags(
|
||
Qt.WindowType.WindowStaysOnTopHint |
|
||
Qt.WindowType.Tool
|
||
)
|
||
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||
self.setWindowOpacity(self._cfg.alpha)
|
||
self.resize(self._cfg.width, self._cfg.height)
|
||
|
||
def _setup_ui(self) -> None:
|
||
cfg = self._cfg
|
||
outer = QVBoxLayout(self)
|
||
outer.setContentsMargins(0, 0, 0, 0)
|
||
|
||
container = QWidget()
|
||
container.setObjectName("container")
|
||
container.setStyleSheet(f"""
|
||
#container {{
|
||
background-color: {cfg.bg_color};
|
||
border-radius: 6px;
|
||
}}
|
||
""")
|
||
outer.addWidget(container)
|
||
|
||
inner = QVBoxLayout(container)
|
||
inner.setContentsMargins(0, 0, 0, 0)
|
||
inner.setSpacing(0)
|
||
|
||
inner.addWidget(_TitleBar(self, cfg))
|
||
|
||
self._text = QTextEdit()
|
||
self._text.setReadOnly(True)
|
||
self._text.setStyleSheet(f"""
|
||
QTextEdit {{
|
||
background-color: {cfg.bg_color};
|
||
color: {cfg.fg_color};
|
||
border: none;
|
||
padding: 6px 8px;
|
||
font-size: 13px;
|
||
}}
|
||
QScrollBar:vertical {{
|
||
background: transparent;
|
||
width: 4px;
|
||
border: none;
|
||
}}
|
||
QScrollBar::handle:vertical {{
|
||
background: {cfg.accent_color};
|
||
border-radius: 2px;
|
||
}}
|
||
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{
|
||
height: 0;
|
||
}}
|
||
""")
|
||
self._text.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
|
||
inner.addWidget(self._text)
|
||
|
||
def _position_bottom_right(self) -> None:
|
||
cfg = self._cfg
|
||
screen = QApplication.primaryScreen().availableGeometry()
|
||
x = screen.width() - cfg.width - cfg.margin_right
|
||
y = screen.height() - cfg.height - cfg.margin_bottom
|
||
self.move(x, y)
|
||
|
||
# ------------------------------------------------------------------
|
||
# 公开 API
|
||
# ------------------------------------------------------------------
|
||
|
||
def set_loading(self) -> None:
|
||
self._sig_loading.emit()
|
||
|
||
def append_text(self, chunk: str) -> None:
|
||
self._sig_append.emit(chunk)
|
||
|
||
def clear(self) -> None:
|
||
self._sig_clear.emit()
|
||
|
||
def toggle(self) -> None:
|
||
self._sig_toggle.emit()
|
||
|
||
# ------------------------------------------------------------------
|
||
# 槽
|
||
# ------------------------------------------------------------------
|
||
|
||
def _on_loading(self) -> None:
|
||
self._text.clear()
|
||
self._text.setTextColor(QColor(self._cfg.accent_color))
|
||
self._text.insertPlainText(_LOADING_TEXT)
|
||
self._text.setTextColor(QColor(self._cfg.fg_color))
|
||
self._on_show()
|
||
|
||
def _on_append(self, chunk: str) -> None:
|
||
current = self._text.toPlainText()
|
||
if current in (_LOADING_TEXT, _LOADING_TEXT + "\n"):
|
||
self._text.clear()
|
||
cursor = self._text.textCursor()
|
||
cursor.movePosition(QTextCursor.MoveOperation.End)
|
||
self._text.setTextCursor(cursor)
|
||
self._text.insertPlainText(chunk)
|
||
self._text.ensureCursorVisible()
|
||
|
||
def _on_clear(self) -> None:
|
||
self._text.clear()
|
||
|
||
def _on_show(self) -> None:
|
||
self.show()
|
||
self.raise_()
|
||
|
||
def _on_hide(self) -> None:
|
||
self.hide()
|
||
|
||
def _on_toggle(self) -> None:
|
||
if self.isVisible():
|
||
self.hide()
|
||
else:
|
||
self.show()
|
||
self.raise_()
|
||
|
||
@property
|
||
def app(self) -> QApplication:
|
||
return self._app
|