304 lines
9.0 KiB
Python
304 lines
9.0 KiB
Python
"""标准对话框(QMessageBox / QInputDialog / QFileDialog)与当前主题一致。"""
|
||
from __future__ import annotations
|
||
|
||
from PyQt6.QtCore import Qt, QTimer
|
||
from PyQt6.QtGui import QTextOption
|
||
from PyQt6.QtWidgets import (
|
||
QFileDialog,
|
||
QInputDialog,
|
||
QMessageBox,
|
||
QWidget,
|
||
QDialog,
|
||
QLabel,
|
||
QSizePolicy,
|
||
)
|
||
|
||
import ui.theme as theme
|
||
|
||
|
||
def stylesheet() -> str:
|
||
t = theme.current()
|
||
bg = t["menu_bg"]
|
||
fg = t["menu_color"]
|
||
border = t["menu_border"]
|
||
hover = t["menu_selected"]
|
||
inp = t["search_bg"]
|
||
inp_b = t["search_border"]
|
||
focus = t["search_focus"]
|
||
return f"""
|
||
QMessageBox {{
|
||
background-color: {bg};
|
||
color: {fg};
|
||
}}
|
||
QMessageBox QLabel {{
|
||
color: {fg};
|
||
background: transparent;
|
||
min-width: 0px;
|
||
}}
|
||
QMessageBox QLabel#qt_msgbox_label {{
|
||
min-width: 200px;
|
||
}}
|
||
QMessageBox QLabel#qt_msgboxex_icon_label {{
|
||
min-width: 28px;
|
||
max-width: 28px;
|
||
}}
|
||
QMessageBox QPushButton {{
|
||
min-width: 64px;
|
||
padding: 6px 14px;
|
||
border: 1px solid {border};
|
||
border-radius: 5px;
|
||
background: {inp};
|
||
color: {fg};
|
||
}}
|
||
QMessageBox QPushButton:hover {{
|
||
background: {hover};
|
||
}}
|
||
QMessageBox QPushButton:default {{
|
||
border-color: {focus};
|
||
}}
|
||
QInputDialog {{
|
||
background-color: {bg};
|
||
color: {fg};
|
||
}}
|
||
QInputDialog QLabel {{
|
||
color: {fg};
|
||
background: transparent;
|
||
}}
|
||
QInputDialog QLineEdit {{
|
||
background: {inp};
|
||
border: 1px solid {inp_b};
|
||
border-radius: 5px;
|
||
padding: 6px 8px;
|
||
color: {fg};
|
||
min-width: 260px;
|
||
}}
|
||
QInputDialog QLineEdit:focus {{
|
||
border-color: {focus};
|
||
}}
|
||
QInputDialog QPushButton {{
|
||
min-width: 72px;
|
||
padding: 6px 14px;
|
||
border: 1px solid {border};
|
||
border-radius: 5px;
|
||
background: {inp};
|
||
color: {fg};
|
||
}}
|
||
QInputDialog QPushButton:hover {{
|
||
background: {hover};
|
||
}}
|
||
QFileDialog {{
|
||
background-color: {bg};
|
||
color: {fg};
|
||
}}
|
||
QFileDialog QLabel {{
|
||
color: {fg};
|
||
background: transparent;
|
||
}}
|
||
QFileDialog QLineEdit, QFileDialog QComboBox {{
|
||
background: {inp};
|
||
border: 1px solid {inp_b};
|
||
border-radius: 4px;
|
||
padding: 4px 6px;
|
||
color: {fg};
|
||
}}
|
||
QFileDialog QLineEdit:focus, QFileDialog QComboBox:focus {{
|
||
border-color: {focus};
|
||
}}
|
||
QFileDialog QTreeView, QFileDialog QListView, QFileDialog QTableView {{
|
||
background: {inp};
|
||
border: 1px solid {inp_b};
|
||
border-radius: 4px;
|
||
color: {fg};
|
||
outline: none;
|
||
}}
|
||
QFileDialog QTreeView::item:selected, QFileDialog QListView::item:selected {{
|
||
background: {hover};
|
||
}}
|
||
QFileDialog QPushButton {{
|
||
min-width: 72px;
|
||
padding: 6px 12px;
|
||
border: 1px solid {border};
|
||
border-radius: 5px;
|
||
background: {inp};
|
||
color: {fg};
|
||
}}
|
||
QFileDialog QPushButton:hover {{
|
||
background: {hover};
|
||
}}
|
||
QFileDialog QComboBox QAbstractItemView {{
|
||
background: {bg};
|
||
color: {fg};
|
||
border: 1px solid {border};
|
||
}}
|
||
"""
|
||
|
||
|
||
def _apply(w: QWidget | None) -> None:
|
||
if w is not None:
|
||
w.setStyleSheet(stylesheet())
|
||
|
||
|
||
def _prepare_qmessagebox(msg: QMessageBox) -> None:
|
||
"""让提示框按内容换行并自适应宽高。"""
|
||
def _tune_labels() -> None:
|
||
# QMessageBox 里通常有两段文字:
|
||
# - qt_msgbox_label(主文本)
|
||
# - qt_msgbox_informativelabel(辅助文本/更长的提示)
|
||
# 部分平台会在显示/布局后才创建这些 label,因此这里支持延迟执行一次。
|
||
for obj_name in ("qt_msgbox_label", "qt_msgbox_informativelabel"):
|
||
w = msg.findChild(QWidget, obj_name)
|
||
if w is None:
|
||
continue
|
||
|
||
if hasattr(w, "setWordWrap"):
|
||
try:
|
||
w.setWordWrap(True) # type: ignore[attr-defined]
|
||
except Exception:
|
||
pass
|
||
if hasattr(w, "setTextFormat"):
|
||
try:
|
||
w.setTextFormat(Qt.TextFormat.PlainText) # type: ignore[attr-defined]
|
||
except Exception:
|
||
pass
|
||
if hasattr(w, "setWordWrapMode"):
|
||
try:
|
||
w.setWordWrapMode(QTextOption.WrapMode.WrapAnywhere) # type: ignore[attr-defined]
|
||
except Exception:
|
||
pass
|
||
if hasattr(w, "setTextElideMode"):
|
||
try:
|
||
w.setTextElideMode(Qt.TextElideMode.ElideNone) # type: ignore[attr-defined]
|
||
except Exception:
|
||
pass
|
||
if hasattr(w, "setMinimumWidth"):
|
||
try:
|
||
w.setMinimumWidth(0) # type: ignore[attr-defined]
|
||
except Exception:
|
||
pass
|
||
if hasattr(w, "setMaximumWidth"):
|
||
try:
|
||
w.setMaximumWidth(16777215) # type: ignore[attr-defined]
|
||
except Exception:
|
||
pass
|
||
if hasattr(w, "setSizePolicy"):
|
||
try:
|
||
w.setSizePolicy(
|
||
QSizePolicy.Policy.Expanding,
|
||
QSizePolicy.Policy.MinimumExpanding,
|
||
) # type: ignore[attr-defined]
|
||
except Exception:
|
||
pass
|
||
msg.setSizePolicy(
|
||
QSizePolicy.Policy.Preferred,
|
||
QSizePolicy.Policy.Preferred,
|
||
)
|
||
msg.setMinimumSize(0, 0)
|
||
msg.setMaximumSize(16777215, 16777215)
|
||
|
||
lay = msg.layout()
|
||
if lay is not None:
|
||
# QMessageBox 内部通常是 QGridLayout:第 1 列是文本列
|
||
# 某些主题/平台会让文本列偏窄从而触发省略,这里显式拉伸该列并设置最小宽度。
|
||
try:
|
||
lay.setColumnStretch(1, 1)
|
||
except Exception:
|
||
pass
|
||
lay.activate()
|
||
_tune_labels()
|
||
msg.adjustSize()
|
||
QTimer.singleShot(0, lambda: (_tune_labels(), msg.adjustSize()))
|
||
|
||
|
||
def question(
|
||
parent: QWidget | None,
|
||
title: str,
|
||
text: str,
|
||
*,
|
||
buttons: QMessageBox.StandardButton = (
|
||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||
),
|
||
default_button: QMessageBox.StandardButton | None = None,
|
||
icon: QMessageBox.Icon = QMessageBox.Icon.Question,
|
||
) -> QMessageBox.StandardButton:
|
||
msg = QMessageBox(parent)
|
||
msg.setWindowTitle(title)
|
||
msg.setText(text)
|
||
msg.setIcon(icon)
|
||
msg.setStandardButtons(buttons)
|
||
if default_button is not None:
|
||
msg.setDefaultButton(default_button)
|
||
_apply(msg)
|
||
_prepare_qmessagebox(msg)
|
||
return QMessageBox.StandardButton(msg.exec())
|
||
|
||
|
||
def warning(parent: QWidget | None, title: str, text: str) -> None:
|
||
msg = QMessageBox(parent)
|
||
msg.setWindowTitle(title)
|
||
msg.setText(text)
|
||
msg.setIcon(QMessageBox.Icon.Warning)
|
||
msg.setStandardButtons(QMessageBox.StandardButton.Ok)
|
||
_apply(msg)
|
||
_prepare_qmessagebox(msg)
|
||
msg.exec()
|
||
|
||
|
||
def information(parent: QWidget | None, title: str, text: str) -> None:
|
||
msg = QMessageBox(parent)
|
||
msg.setWindowTitle(title)
|
||
msg.setText(text)
|
||
msg.setIcon(QMessageBox.Icon.Information)
|
||
msg.setStandardButtons(QMessageBox.StandardButton.Ok)
|
||
_apply(msg)
|
||
_prepare_qmessagebox(msg)
|
||
msg.exec()
|
||
|
||
|
||
def get_text(
|
||
parent: QWidget | None,
|
||
title: str,
|
||
label: str,
|
||
text: str = "",
|
||
) -> tuple[str, bool]:
|
||
d = QInputDialog(parent)
|
||
d.setWindowTitle(title)
|
||
d.setLabelText(label)
|
||
d.setTextValue(text)
|
||
_apply(d)
|
||
ok = d.exec() == QDialog.DialogCode.Accepted
|
||
return d.textValue(), ok
|
||
|
||
|
||
def get_open_file_name(
|
||
parent: QWidget | None,
|
||
caption: str,
|
||
directory: str,
|
||
filter_str: str,
|
||
) -> tuple[str, str]:
|
||
fd = QFileDialog(parent, caption, directory, filter_str)
|
||
fd.setOption(QFileDialog.Option.DontUseNativeDialog, True)
|
||
fd.setFileMode(QFileDialog.FileMode.ExistingFile)
|
||
_apply(fd)
|
||
if fd.exec() == QDialog.DialogCode.Accepted:
|
||
files = fd.selectedFiles()
|
||
if files:
|
||
return files[0], fd.selectedNameFilter()
|
||
return "", ""
|
||
|
||
|
||
def get_existing_directory(
|
||
parent: QWidget | None,
|
||
caption: str,
|
||
directory: str = "",
|
||
) -> str:
|
||
fd = QFileDialog(parent, caption, directory)
|
||
fd.setOption(QFileDialog.Option.DontUseNativeDialog, True)
|
||
fd.setFileMode(QFileDialog.FileMode.Directory)
|
||
fd.setOption(QFileDialog.Option.ShowDirsOnly, True)
|
||
_apply(fd)
|
||
if fd.exec() == QDialog.DialogCode.Accepted:
|
||
files = fd.selectedFiles()
|
||
if files:
|
||
return files[0]
|
||
return ""
|