diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a4e2c2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Python 编译文件 +__pycache__/ +*.py[cod] +*.so +ui/__pycache__/ + +# 打包exe相关 +dist/ +build/ +*.spec + +# 编辑器 +.idea/ +.vscode/ +*.swp \ No newline at end of file diff --git a/logo.ico b/logo.ico new file mode 100644 index 0000000..aa94ed2 Binary files /dev/null and b/logo.ico differ diff --git a/ui/__pycache__/dialog_style.cpython-314.pyc b/ui/__pycache__/dialog_style.cpython-314.pyc index e953fae..030c621 100644 Binary files a/ui/__pycache__/dialog_style.cpython-314.pyc and b/ui/__pycache__/dialog_style.cpython-314.pyc differ diff --git a/ui/__pycache__/dock.cpython-314.pyc b/ui/__pycache__/dock.cpython-314.pyc index df6709e..6e88cb9 100644 Binary files a/ui/__pycache__/dock.cpython-314.pyc and b/ui/__pycache__/dock.cpython-314.pyc differ diff --git a/ui/dialog_style.py b/ui/dialog_style.py index 2caf3a6..114f0c7 100644 --- a/ui/dialog_style.py +++ b/ui/dialog_style.py @@ -1,12 +1,16 @@ """标准对话框(QMessageBox / QInputDialog / QFileDialog)与当前主题一致。""" from __future__ import annotations +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QTextOption from PyQt6.QtWidgets import ( QFileDialog, QInputDialog, QMessageBox, QWidget, QDialog, + QLabel, + QSizePolicy, ) import ui.theme as theme @@ -29,7 +33,15 @@ def stylesheet() -> str: QMessageBox QLabel {{ color: {fg}; background: transparent; - min-width: 160px; + min-width: 0px; + }} + QMessageBox QLabel#qt_msgbox_label {{ + min-width: 200px; + max-width: 560px; + }} + QMessageBox QLabel#qt_msgboxex_icon_label {{ + min-width: 28px; + max-width: 28px; }} QMessageBox QPushButton {{ min-width: 64px; @@ -127,6 +139,34 @@ def _apply(w: QWidget | None) -> None: w.setStyleSheet(stylesheet()) +def _prepare_qmessagebox(msg: QMessageBox) -> None: + """让提示框按内容换行并自适应宽高(避免固定宽度截断文字)。""" + lbl = msg.findChild(QLabel, "qt_msgbox_label") + if lbl is not None: + lbl.setWordWrap(True) + lbl.setTextFormat(Qt.TextFormat.PlainText) + # 无空格长中文也能在边界处断行,而不是整段挤在一行被裁切 + try: + lbl.setWordWrapMode(QTextOption.WrapMode.WrapAnywhere) + except AttributeError: + pass + lbl.setMinimumWidth(200) + lbl.setMaximumWidth(560) + lbl.setSizePolicy( + QSizePolicy.Policy.Preferred, + QSizePolicy.Policy.MinimumExpanding, + ) + lay = msg.layout() + if lay is not None: + lay.activate() + msg.setSizePolicy( + QSizePolicy.Policy.Preferred, + QSizePolicy.Policy.Preferred, + ) + msg.setMinimumSize(0, 0) + msg.adjustSize() + + def question( parent: QWidget | None, title: str, @@ -146,8 +186,7 @@ def question( if default_button is not None: msg.setDefaultButton(default_button) _apply(msg) - # 防止对话框因为文本长度/默认最小宽度变得过宽 - msg.setFixedWidth(320) + _prepare_qmessagebox(msg) return QMessageBox.StandardButton(msg.exec()) @@ -158,7 +197,7 @@ def warning(parent: QWidget | None, title: str, text: str) -> None: msg.setIcon(QMessageBox.Icon.Warning) msg.setStandardButtons(QMessageBox.StandardButton.Ok) _apply(msg) - msg.setFixedWidth(320) + _prepare_qmessagebox(msg) msg.exec() @@ -169,7 +208,7 @@ def information(parent: QWidget | None, title: str, text: str) -> None: msg.setIcon(QMessageBox.Icon.Information) msg.setStandardButtons(QMessageBox.StandardButton.Ok) _apply(msg) - msg.setFixedWidth(320) + _prepare_qmessagebox(msg) msg.exec() diff --git a/ui/dock.py b/ui/dock.py index 2d740f3..3a147a7 100644 --- a/ui/dock.py +++ b/ui/dock.py @@ -7,6 +7,7 @@ import struct import time as _time import datetime import urllib.request +import webbrowser import qtawesome as qta from PyQt6.QtWidgets import ( QWidget, @@ -647,6 +648,14 @@ class PanelWindow(QWidget): bottom_layout.setContentsMargins(6, 0, 6, 0) bottom_layout.setSpacing(4) + self.menu_btn = QPushButton() + self.menu_btn.setFixedSize(28, 28) + self.menu_btn.setToolTip("菜单") + self.menu_btn.setStyleSheet( + "border:none; background:transparent; border-radius:4px;" + ) + self.menu_btn.clicked.connect(self._show_bottom_menu) + self.theme_btn = QPushButton() self.theme_btn.setFixedSize(28, 28) self.theme_btn.setToolTip("切换亮/暗主题") @@ -671,6 +680,7 @@ class PanelWindow(QWidget): ) self.quit_btn.clicked.connect(self._quit_application) + bottom_layout.addWidget(self.menu_btn) bottom_layout.addStretch() bottom_layout.addWidget(self.theme_btn) bottom_layout.addWidget(self.settings_btn) @@ -787,6 +797,8 @@ class PanelWindow(QWidget): qta.icon("fa5s.sun" if is_dark else "fa5s.moon", color=ic) ) self.theme_btn.setIconSize(QSize(14, 14)) + self.menu_btn.setIcon(qta.icon("fa5s.bars", color=ic)) + self.menu_btn.setIconSize(QSize(14, 14)) self.settings_btn.setIcon(qta.icon("fa5s.cog", color=ic)) self.settings_btn.setIconSize(QSize(14, 14)) self.quit_btn.setIcon(qta.icon("fa5s.sign-out-alt", color=ic)) @@ -827,6 +839,96 @@ class PanelWindow(QWidget): self._settings_win = SettingsWindow(self) self._settings_win.show() + def _show_bottom_menu(self): + menu = QMenu(self) + menu.setStyleSheet(self._bottom_menu_stylesheet()) + menu.addAction(self._make_doubao_icon(), "豆包会话").triggered.connect( + self._open_doubao_session + ) + menu.addAction( + self._make_round_menu_icon("D", "#4d6bfe"), "DeepSeek" + ).triggered.connect( + lambda: self._open_url_in_browser( + "https://chat.deepseek.com/", "DeepSeek" + ) + ) + menu.addAction( + self._make_round_menu_icon("G", "#4285f4"), "Gemini" + ).triggered.connect( + lambda: self._open_url_in_browser( + "https://gemini.google.com/", "Gemini" + ) + ) + # 菜单贴近按钮下方显示,避免漂移到不自然位置 + pos = self.menu_btn.mapToGlobal(self.menu_btn.rect().bottomLeft()) + menu.exec(pos) + + def _bottom_menu_stylesheet(self) -> str: + t = theme.current() + return f""" + QMenu {{ + background: {t['menu_bg']}; + color: {t['menu_color']}; + border: 1px solid {t['menu_border']}; + border-radius: 8px; + padding: 4px; + }} + QMenu::item {{ + padding: 6px 14px; + border-radius: 5px; + background: transparent; + }} + QMenu::item:selected {{ + background: {t['menu_selected']}; + }} + QMenu::separator {{ + height: 1px; + background: {t['menu_border']}; + margin: 4px 8px; + }} + """ + + def _make_doubao_icon(self) -> QIcon: + return self._make_round_menu_icon("豆", "#3b82f6", font_pt=9) + + def _make_round_menu_icon( + self, letter: str, bg_hex: str, *, font_pt: int = 9 + ) -> QIcon: + pm = QPixmap(18, 18) + pm.fill(Qt.GlobalColor.transparent) + p = QPainter(pm) + p.setRenderHint(QPainter.RenderHint.Antialiasing) + p.setBrush(QBrush(QColor(bg_hex))) + p.setPen(Qt.PenStyle.NoPen) + p.drawEllipse(1, 1, 16, 16) + p.setPen(QPen(QColor("white"))) + f = p.font() + f.setPointSize(font_pt) + f.setBold(True) + p.setFont(f) + p.drawText(pm.rect(), int(Qt.AlignmentFlag.AlignCenter), letter) + p.end() + return QIcon(pm) + + def _open_url_in_browser(self, url: str, title: str) -> None: + try: + ok = webbrowser.open(url) + except Exception as e: + dialog_style.warning( + self, "打开失败", f"无法在默认浏览器中打开{title}:{e}" + ) + return + if not ok: + dialog_style.warning( + self, + "打开失败", + f"系统未注册可用的默认浏览器,请手动在浏览器中打开{title}:\n{url}", + ) + + def _open_doubao_session(self): + """用系统默认浏览器打开豆包会话页。""" + self._open_url_in_browser("https://www.doubao.com/chat/", "豆包") + def refresh_groups(self): # 记录当前折叠状态 collapsed_ids = set()