mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-10-12 01:08:17 +03:00
Refine Fluent message box content hierarchy
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
@@ -11,6 +11,8 @@
|
||||
- Long or batched work must yield safely or use an owned worker/progress dialog. Do not sleep the GUI thread.
|
||||
- Asynchronous prompts/editors use managed `open()` lifetime and connect completion before opening. Rebuilt dynamic
|
||||
menus must release obsolete `QMenu`/`QAction` objects rather than accumulating them.
|
||||
- Preserve structured error semantics in message boxes: error title becomes the visible `heading`, the main message
|
||||
becomes `text`, and diagnostics become `informativeText`; native `windowTitle` remains metadata.
|
||||
|
||||
## Code review rules
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ class ConnectAction(AppQAction):
|
||||
return
|
||||
|
||||
mbox = AppQMessageBox(icon=AppQMessageBox.Icon.Critical)
|
||||
mbox.setWindowTitle(error.title)
|
||||
mbox.setHeading(error.title)
|
||||
mbox.setText(error.message)
|
||||
|
||||
if error.details:
|
||||
|
||||
@@ -303,11 +303,8 @@ class MBoxImportError(AppQMessageBox):
|
||||
"""Initialize the MBoxImportError."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setWindowTitle(_('Import'))
|
||||
|
||||
def retranslate(self):
|
||||
"""Refresh translated text for the m box import error."""
|
||||
self.setWindowTitle(_(self.windowTitle()))
|
||||
self.setText(_(self.text()))
|
||||
|
||||
# Ignore informative text, buttons
|
||||
@@ -325,7 +322,6 @@ class MBoxImportMultiSuccess(AppQMessageBox):
|
||||
self.imported = list()
|
||||
self.rowIndex = 0
|
||||
|
||||
self.setWindowTitle(_('Import'))
|
||||
self.setIcon(AppQMessageBox.Icon.Information)
|
||||
|
||||
def customText(self):
|
||||
@@ -351,7 +347,6 @@ class MBoxImportMultiSuccess(AppQMessageBox):
|
||||
|
||||
def retranslate(self):
|
||||
"""Refresh translated text for the m box import multi success."""
|
||||
self.setWindowTitle(_(self.windowTitle()))
|
||||
self.setText(self.customText())
|
||||
|
||||
# Ignore informative text, buttons
|
||||
@@ -367,7 +362,6 @@ class MBoxImportSuccess(AppQMessageBox):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.remark = ''
|
||||
self.setWindowTitle(_('Import'))
|
||||
|
||||
def customText(self):
|
||||
"""Return the user-facing message text for the m box import success."""
|
||||
@@ -378,7 +372,6 @@ class MBoxImportSuccess(AppQMessageBox):
|
||||
|
||||
def retranslate(self):
|
||||
"""Refresh translated text for the m box import success."""
|
||||
self.setWindowTitle(_(self.windowTitle()))
|
||||
self.setText(self.customText())
|
||||
|
||||
# Ignore informative text, buttons
|
||||
@@ -413,7 +406,6 @@ class ImportFromFileAction(AppQAction):
|
||||
# Any non-exit exceptions
|
||||
|
||||
mbox = AppQMessageBox(icon=AppQMessageBox.Icon.Critical)
|
||||
mbox.setWindowTitle(_('Error opening file'))
|
||||
mbox.setText(_('Invalid configuration file'))
|
||||
mbox.setInformativeText(str(ex))
|
||||
|
||||
|
||||
@@ -499,13 +499,13 @@ class ExternalCoreEditor(GuiEditorWidgetQDialog):
|
||||
)
|
||||
|
||||
if validationMessage:
|
||||
messageBox = AppQMessageBox(
|
||||
mbox = AppQMessageBox(
|
||||
icon=AppQMessageBox.Icon.Warning,
|
||||
parent=self,
|
||||
)
|
||||
messageBox.setWindowTitle(_('Invalid data'))
|
||||
messageBox.setText(validationMessage)
|
||||
messageBox.open()
|
||||
mbox.setHeading(_('Invalid data'))
|
||||
mbox.setText(validationMessage)
|
||||
mbox.open()
|
||||
|
||||
return
|
||||
|
||||
|
||||
@@ -1165,13 +1165,13 @@ class Hysteria2Editor(GuiEditorWidgetQDialog):
|
||||
)
|
||||
|
||||
if mimicEnabled and ConfigHysteria2({'server': server}).usesPortHopping():
|
||||
messageBox = AppQMessageBox(
|
||||
mbox = AppQMessageBox(
|
||||
icon=AppQMessageBox.Icon.Warning,
|
||||
windowTitle=_('Invalid Configuration'),
|
||||
heading=_('Invalid Configuration'),
|
||||
text=_('Mimic cannot be used with port hopping.'),
|
||||
parent=self,
|
||||
)
|
||||
messageBox.open()
|
||||
mbox.open()
|
||||
|
||||
return
|
||||
|
||||
|
||||
@@ -51,7 +51,6 @@ class MBoxAssetExists(AppQMessageBox):
|
||||
|
||||
def retranslate(self):
|
||||
"""Refresh translated text for the m box asset exists."""
|
||||
self.setWindowTitle(_(self.windowTitle()))
|
||||
self.setText(_(self.text()))
|
||||
|
||||
# Ignore informative text, buttons
|
||||
@@ -177,7 +176,6 @@ class XrayAssetListView(Mixins.ThemeAware, AppQListView):
|
||||
# Any non-exit exception
|
||||
|
||||
_mbox = AppQMessageBox(icon=AppQMessageBox.Icon.Critical)
|
||||
_mbox.setWindowTitle(_('Import'))
|
||||
_mbox.setText(_('Error import asset file'))
|
||||
_mbox.setInformativeText(str(ex))
|
||||
|
||||
@@ -187,7 +185,6 @@ class XrayAssetListView(Mixins.ThemeAware, AppQListView):
|
||||
self.flushItem()
|
||||
|
||||
_mbox = AppQMessageBox(icon=AppQMessageBox.Icon.Information)
|
||||
_mbox.setWindowTitle(_('Import'))
|
||||
_mbox.setText(_('Import asset file success'))
|
||||
|
||||
# Show the MessageBox asynchronously
|
||||
@@ -208,7 +205,6 @@ class XrayAssetListView(Mixins.ThemeAware, AppQListView):
|
||||
pass
|
||||
|
||||
mbox = MBoxAssetExists(icon=AppQMessageBox.Icon.Question)
|
||||
mbox.setWindowTitle(_('Import'))
|
||||
mbox.setText(_('Asset file already exists. Overwrite?'))
|
||||
mbox.setInformativeText(basename)
|
||||
mbox.finished.connect(functools.partial(handleResultCode, filename))
|
||||
|
||||
Vendored
+1
-52
@@ -3,7 +3,6 @@ TRANSLATION = {
|
||||
"source": [
|
||||
"Furious.Backends.Xray.AssetWindow",
|
||||
"Furious.Backends.Xray.RoutingWindow",
|
||||
"Furious.Qt.QtWidgets",
|
||||
"Furious.Widget.ServerTableView",
|
||||
"Furious.Widget.SubscriptionTableView",
|
||||
"Furious.Window.SubscriptionPage"
|
||||
@@ -104,8 +103,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Import": {
|
||||
"source": [
|
||||
"Furious.Actions.Import",
|
||||
"Furious.Backends.Xray.AssetListView"
|
||||
"Furious.Actions.Import"
|
||||
],
|
||||
"RU": "Импорт",
|
||||
"ZH": "导入",
|
||||
@@ -162,14 +160,6 @@ TRANSLATION = {
|
||||
"ZH": "文本文件 (*.json);;所有文件 (*)",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Error opening file": {
|
||||
"source": [
|
||||
"Furious.Actions.Import"
|
||||
],
|
||||
"RU": "Ошибка открытия файла",
|
||||
"ZH": "打开文件出错",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Invalid configuration file": {
|
||||
"source": [
|
||||
"Furious.Actions.Import"
|
||||
@@ -234,14 +224,6 @@ TRANSLATION = {
|
||||
"ZH": "取消",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Save Changes": {
|
||||
"source": [
|
||||
"Furious.Window.TextEditorWindow"
|
||||
],
|
||||
"RU": "Сохранить изменения",
|
||||
"ZH": "保存更改",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"The content has been modified. Save changes?": {
|
||||
"source": [
|
||||
"Furious.Window.TextEditorWindow"
|
||||
@@ -401,14 +383,6 @@ TRANSLATION = {
|
||||
"ZH": "显示",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Error saving configuration": {
|
||||
"source": [
|
||||
"Furious.Window.TextEditorWindow"
|
||||
],
|
||||
"RU": "Ошибка сохранения конфигурации",
|
||||
"ZH": "保存配置出错",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Save File": {
|
||||
"source": [
|
||||
"Furious.Window.LogPage",
|
||||
@@ -418,14 +392,6 @@ TRANSLATION = {
|
||||
"ZH": "保存文件",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Error Saving File": {
|
||||
"source": [
|
||||
"Furious.Window.TextEditorWindow"
|
||||
],
|
||||
"RU": "Ошибка сохранения файла",
|
||||
"ZH": "保存文件出错",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Invalid server configuration": {
|
||||
"source": [
|
||||
"Furious.Controllers.ConnectionController",
|
||||
@@ -435,14 +401,6 @@ TRANSLATION = {
|
||||
"ZH": "无效的服务器配置",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Error setting indent": {
|
||||
"source": [
|
||||
"Furious.Window.TextEditorWindow"
|
||||
],
|
||||
"RU": "Ошибка при установке отступов",
|
||||
"ZH": "设置缩进出错",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Remark": {
|
||||
"source": [
|
||||
"Furious.Backends.ExternalCore.Editor",
|
||||
@@ -642,7 +600,6 @@ TRANSLATION = {
|
||||
"Connecting": {
|
||||
"source": [
|
||||
"Furious.Actions.Connection",
|
||||
"Furious.Widget.ServerTableView",
|
||||
"Furious.Window.HomePage"
|
||||
],
|
||||
"RU": "Подключение",
|
||||
@@ -714,14 +671,6 @@ TRANSLATION = {
|
||||
"ZH": "文本文件 (*.txt);;所有文件 (*)",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Error saving log": {
|
||||
"source": [
|
||||
"Furious.Window.LogPage"
|
||||
],
|
||||
"RU": "Ошибка сохранения журнала",
|
||||
"ZH": "保存日志出错",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Asset file already exists. Overwrite?": {
|
||||
"source": [
|
||||
"Furious.Backends.Xray.AssetListView"
|
||||
|
||||
@@ -8,6 +8,10 @@ Use the `manage-qt-pyside6-lifetimes` skill for any Qt ownership or lifecycle ch
|
||||
styling raw Qt controls at call sites.
|
||||
- Construction-time source text should be retained by the control for retranslation. Do not duplicate manual
|
||||
`retranslate()` code where an `AppQ*` widget/action/menu already supports it.
|
||||
- `AppQMessageBox.windowTitle()` is native window-manager/accessibility metadata and is not rendered by the frameless
|
||||
Fluent surface. Put an optional visible semantic title in `heading`, the primary explanation in `text`, and supporting
|
||||
details in `informativeText`. Never rely on `setWindowTitle()` alone for user-visible information, and do not promote a
|
||||
generic application-name title into a visible heading.
|
||||
- Preserve public exports and wildcard-import compatibility carefully; keep optional/heavy facilities lazily imported
|
||||
where practical.
|
||||
- `AppStyleSheet` remains the sole public stylesheet authority. Internal `StyleSheets` modules are data-oriented QSS
|
||||
|
||||
+81
-34
@@ -847,7 +847,11 @@ class _AppMessageBoxMask(QFrame):
|
||||
|
||||
|
||||
class AppQMessageBox(AppQTransientDialog):
|
||||
"""Present a responsive Fluent dialog with QMessageBox-compatible APIs."""
|
||||
"""Present a Fluent message box with separate metadata and visible layers.
|
||||
|
||||
``title``/``windowTitle`` remain native window metadata for compatibility;
|
||||
``heading``, ``text``, and ``informativeText`` form the visible hierarchy.
|
||||
"""
|
||||
|
||||
Icon = QMessageBox.Icon
|
||||
StandardButton = QMessageBox.StandardButton
|
||||
@@ -897,11 +901,17 @@ class AppQMessageBox(AppQTransientDialog):
|
||||
AppQMessageBox._openMessageBoxes.pop(key, None)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the AppQMessageBox."""
|
||||
icon, parent, title, text, buttons = (
|
||||
"""Initialize the message box while preserving QMessageBox arguments."""
|
||||
windowTitle = kwargs.pop('windowTitle', None)
|
||||
|
||||
icon, parent, title, heading, text, buttons = (
|
||||
kwargs.pop('icon', self.Icon.NoIcon),
|
||||
kwargs.pop('parent', None),
|
||||
kwargs.pop('title', ''),
|
||||
kwargs.pop(
|
||||
'title',
|
||||
APPLICATION_NAME if windowTitle is None else windowTitle,
|
||||
),
|
||||
kwargs.pop('heading', ''),
|
||||
kwargs.pop('text', ''),
|
||||
kwargs.pop('buttons', self.StandardButton.NoButton),
|
||||
)
|
||||
@@ -921,6 +931,7 @@ class AppQMessageBox(AppQTransientDialog):
|
||||
self._lifetimeKey = object()
|
||||
self._windowMask = None
|
||||
self._icon = self.Icon.NoIcon
|
||||
self._heading = ''
|
||||
self._text = ''
|
||||
self._informativeText = ''
|
||||
self._minimumContentWidth = 0
|
||||
@@ -941,13 +952,14 @@ class AppQMessageBox(AppQTransientDialog):
|
||||
self.destroyed.connect(release)
|
||||
|
||||
self.setObjectName('AppMessageBox')
|
||||
|
||||
windowFlags = (
|
||||
QtCore.Qt.WindowType.Window
|
||||
| QtCore.Qt.WindowType.FramelessWindowHint
|
||||
| QtCore.Qt.WindowType.NoDropShadowWindowHint
|
||||
)
|
||||
|
||||
self.setWindowFlags(windowFlags)
|
||||
|
||||
self.setAttribute(
|
||||
QtCore.Qt.WidgetAttribute.WA_TranslucentBackground,
|
||||
True,
|
||||
@@ -986,10 +998,18 @@ class AppQMessageBox(AppQTransientDialog):
|
||||
self.textWidget = QWidget()
|
||||
self.textWidget.setObjectName('AppMessageBoxTextWidget')
|
||||
|
||||
self.titleLabel = QLabel(self.textWidget)
|
||||
self.titleLabel.setObjectName('AppMessageBoxTitle')
|
||||
self.titleLabel.setWordWrap(True)
|
||||
self.titleLabel.setTextInteractionFlags(
|
||||
self.headingLabel = QLabel(self.textWidget)
|
||||
self.headingLabel.setObjectName('AppMessageBoxHeading')
|
||||
self.headingLabel.setWordWrap(False)
|
||||
self.headingLabel.setTextFormat(QtCore.Qt.TextFormat.PlainText)
|
||||
self.headingLabel.setTextInteractionFlags(
|
||||
QtCore.Qt.TextInteractionFlag.TextSelectableByMouse
|
||||
)
|
||||
|
||||
self.textLabel = QLabel(self.textWidget)
|
||||
self.textLabel.setObjectName('AppMessageBoxText')
|
||||
self.textLabel.setWordWrap(True)
|
||||
self.textLabel.setTextInteractionFlags(
|
||||
QtCore.Qt.TextInteractionFlag.TextSelectableByMouse
|
||||
)
|
||||
|
||||
@@ -1003,7 +1023,8 @@ class AppQMessageBox(AppQTransientDialog):
|
||||
self.textLayout = QVBoxLayout(self.textWidget)
|
||||
self.textLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.textLayout.setSpacing(8)
|
||||
self.textLayout.addWidget(self.titleLabel)
|
||||
self.textLayout.addWidget(self.headingLabel)
|
||||
self.textLayout.addWidget(self.textLabel)
|
||||
self.textLayout.addWidget(self.informativeLabel)
|
||||
|
||||
self.textViewport.setWidget(self.textWidget)
|
||||
@@ -1036,6 +1057,7 @@ class AppQMessageBox(AppQTransientDialog):
|
||||
self.dialogLayout.addWidget(self.surface)
|
||||
|
||||
self.setWindowTitle(title)
|
||||
self.setHeading(heading)
|
||||
self.setText(text)
|
||||
self.setIcon(icon)
|
||||
|
||||
@@ -1198,10 +1220,24 @@ class AppQMessageBox(AppQTransientDialog):
|
||||
if not self.buttons():
|
||||
self.setStandardButtons(self.StandardButton.Ok)
|
||||
|
||||
def setHeading(self, heading):
|
||||
"""Set the optional visible semantic heading."""
|
||||
self._heading = str(heading)
|
||||
self.headingLabel.setText(self._heading)
|
||||
self.headingLabel.setVisible(bool(self._heading))
|
||||
|
||||
if self.isVisible():
|
||||
self._updateDialogSize()
|
||||
|
||||
def heading(self):
|
||||
"""Return the visible semantic heading."""
|
||||
return self._heading
|
||||
|
||||
def setText(self, text):
|
||||
"""Set the primary message text."""
|
||||
self._text = str(text)
|
||||
self.titleLabel.setText(self._text)
|
||||
self.textLabel.setText(self._text)
|
||||
self.textLabel.setVisible(bool(self._text))
|
||||
|
||||
if self.isVisible():
|
||||
self._updateDialogSize()
|
||||
@@ -1484,6 +1520,23 @@ class AppQMessageBox(AppQTransientDialog):
|
||||
|
||||
return height
|
||||
|
||||
def _textContentHeight(self, width):
|
||||
"""Measure visible heading, primary, and supporting text blocks."""
|
||||
blocks = (
|
||||
(self.headingLabel, self._heading),
|
||||
(self.textLabel, self._text),
|
||||
(self.informativeLabel, self._informativeText),
|
||||
)
|
||||
heights = []
|
||||
|
||||
for label, value in blocks:
|
||||
if value:
|
||||
heights.append(self._wrappedHeight(label, width))
|
||||
else:
|
||||
label.setFixedHeight(0)
|
||||
|
||||
return sum(heights) + self.textLayout.spacing() * max(0, len(heights) - 1)
|
||||
|
||||
def _updateDialogSize(self):
|
||||
"""Fit short content compactly and bound translated/long content."""
|
||||
# QSS frame widths and button size hints participate in the geometry
|
||||
@@ -1541,9 +1594,13 @@ class AppQMessageBox(AppQTransientDialog):
|
||||
comfortableTextWidth = min(260, maximumTextWidth)
|
||||
|
||||
textWidths = [
|
||||
self.titleLabel.fontMetrics().horizontalAdvance(line)
|
||||
for line in self._text.splitlines() or ['']
|
||||
self.headingLabel.fontMetrics().horizontalAdvance(line)
|
||||
for line in self._heading.splitlines()
|
||||
]
|
||||
textWidths.extend(
|
||||
self.textLabel.fontMetrics().horizontalAdvance(line)
|
||||
for line in self._text.splitlines() or ['']
|
||||
)
|
||||
textWidths.extend(
|
||||
self.informativeLabel.fontMetrics().horizontalAdvance(line)
|
||||
for line in self._informativeText.splitlines()
|
||||
@@ -1556,16 +1613,7 @@ class AppQMessageBox(AppQTransientDialog):
|
||||
naturalTextWidth,
|
||||
)
|
||||
textWidth = min(maximumTextWidth, preferredTextWidth)
|
||||
|
||||
titleHeight = self._wrappedHeight(self.titleLabel, textWidth)
|
||||
bodyHeight = 0
|
||||
|
||||
if self._informativeText:
|
||||
bodyHeight = self._wrappedHeight(self.informativeLabel, textWidth) + 8
|
||||
else:
|
||||
self.informativeLabel.setFixedHeight(0)
|
||||
|
||||
textHeight = titleHeight + bodyHeight
|
||||
textHeight = self._textContentHeight(textWidth)
|
||||
maximumTextHeight = max(
|
||||
72,
|
||||
min(
|
||||
@@ -1583,13 +1631,7 @@ class AppQMessageBox(AppQTransientDialog):
|
||||
self.textViewport,
|
||||
)
|
||||
textWidth = max(220, textWidth - scrollBarWidth)
|
||||
titleHeight = self._wrappedHeight(self.titleLabel, textWidth)
|
||||
bodyHeight = 0
|
||||
|
||||
if self._informativeText:
|
||||
bodyHeight = self._wrappedHeight(self.informativeLabel, textWidth) + 8
|
||||
|
||||
textHeight = titleHeight + bodyHeight
|
||||
textHeight = self._textContentHeight(textWidth)
|
||||
else:
|
||||
scrollBarWidth = 0
|
||||
|
||||
@@ -1701,6 +1743,13 @@ class AppQMessageBox(AppQTransientDialog):
|
||||
def retranslate(self):
|
||||
"""Refresh translated text for the app q message box."""
|
||||
self.setWindowTitle(_(self.windowTitle()))
|
||||
|
||||
try:
|
||||
self.setHeading(_(self.heading()))
|
||||
except KeyError:
|
||||
# Runtime headings may already be localized by their owning layer.
|
||||
pass
|
||||
|
||||
self.setText(_(self.text()))
|
||||
|
||||
try:
|
||||
@@ -2114,7 +2163,6 @@ class MBoxQuestionDelete(AppQMessageBox):
|
||||
self.isMulti = False
|
||||
self.possibleRemark = ''
|
||||
|
||||
self.setWindowTitle(_('Delete'))
|
||||
self.setStandardButtons(
|
||||
AppQMessageBox.StandardButton.Yes | AppQMessageBox.StandardButton.No
|
||||
)
|
||||
@@ -2128,7 +2176,6 @@ class MBoxQuestionDelete(AppQMessageBox):
|
||||
|
||||
def retranslate(self):
|
||||
"""Refresh translated text for the m box question delete."""
|
||||
self.setWindowTitle(_(self.windowTitle()))
|
||||
self.setText(self.customText())
|
||||
|
||||
# Ignore informative text, buttons
|
||||
@@ -2210,7 +2257,7 @@ class MBoxDirectRulesNotAllowed(AppQMessageBox):
|
||||
"""Initialize the MBoxDirectRulesNotAllowed."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setWindowTitle(_('Unable to connect'))
|
||||
self.setHeading(_('Unable to connect'))
|
||||
self.setIcon(AppQMessageBox.Icon.Critical)
|
||||
self.setStandardButtons(
|
||||
AppQMessageBox.StandardButton.Yes | AppQMessageBox.StandardButton.No
|
||||
@@ -2228,7 +2275,7 @@ class MBoxDirectRulesNotAllowed(AppQMessageBox):
|
||||
|
||||
def retranslate(self):
|
||||
"""Refresh translated text for the m box direct rules not allowed."""
|
||||
self.setWindowTitle(_(self.windowTitle()))
|
||||
self.setHeading(_(self.heading()))
|
||||
self.setText(self.customText())
|
||||
|
||||
# Ignore informative text, buttons
|
||||
|
||||
@@ -91,7 +91,15 @@ def baseStyleSheet(palette):
|
||||
background-color: transparent;
|
||||
}}
|
||||
|
||||
QLabel#AppMessageBoxTitle {{
|
||||
QLabel#AppMessageBoxHeading {{
|
||||
padding: 0;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
color: {palette['text_strong']};
|
||||
font-weight: 600;
|
||||
}}
|
||||
|
||||
QLabel#AppMessageBoxText {{
|
||||
padding: 0;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
|
||||
@@ -1575,7 +1575,6 @@ class ServerTableView(
|
||||
|
||||
if AppConnectionController().isConnecting():
|
||||
mbox = AppQMessageBox(icon=AppQMessageBox.Icon.Information)
|
||||
mbox.setWindowTitle(_('Connecting'))
|
||||
mbox.setText(_('Connecting. Please wait...'))
|
||||
|
||||
if PLATFORM != 'Darwin':
|
||||
|
||||
@@ -89,7 +89,6 @@ class MBoxSaveError(AppQMessageBox):
|
||||
|
||||
def retranslate(self):
|
||||
"""Refresh translated text for the m box save error."""
|
||||
self.setWindowTitle(_(self.windowTitle()))
|
||||
self.setText(self.customText())
|
||||
|
||||
# Ignore informative text, buttons
|
||||
@@ -112,7 +111,6 @@ def saveAsFile(content: str):
|
||||
|
||||
mbox = MBoxSaveError(icon=AppQMessageBox.Icon.Critical)
|
||||
mbox.saveError = str(ex)
|
||||
mbox.setWindowTitle(_('Error saving log'))
|
||||
mbox.setText(mbox.customText())
|
||||
|
||||
# Show the MessageBox asynchronously
|
||||
|
||||
@@ -76,19 +76,16 @@ def _endpointPrivacyParagraphs():
|
||||
|
||||
def _endpointPrivacyMessageBox(parent=None):
|
||||
"""Build one transient Fluent data-usage disclosure."""
|
||||
title = _('Proxy Endpoint Information & Privacy')
|
||||
|
||||
messageBox = AppQMessageBox(
|
||||
mbox = AppQMessageBox(
|
||||
icon=AppQMessageBox.Icon.Information,
|
||||
parent=parent,
|
||||
title=title,
|
||||
text=title,
|
||||
text=_('Proxy Endpoint Information & Privacy'),
|
||||
buttons=AppQMessageBox.StandardButton.Ok,
|
||||
)
|
||||
messageBox.informativeLabel.setTextFormat(QtCore.Qt.TextFormat.RichText)
|
||||
messageBox.setInformativeText('<br><br>\n'.join(_endpointPrivacyParagraphs()))
|
||||
mbox.informativeLabel.setTextFormat(QtCore.Qt.TextFormat.RichText)
|
||||
mbox.setInformativeText('<br><br>\n'.join(_endpointPrivacyParagraphs()))
|
||||
|
||||
return messageBox
|
||||
return mbox
|
||||
|
||||
|
||||
def _tunModeTitle() -> str:
|
||||
|
||||
@@ -191,13 +191,10 @@ class _SubscriptionEditorDialog(AppQTransientDialog):
|
||||
|
||||
def _showValidationError(self, message: str):
|
||||
"""Show one non-blocking validation message owned by this dialog."""
|
||||
messageBox = AppQMessageBox(
|
||||
icon=AppQMessageBox.Icon.Warning,
|
||||
parent=self,
|
||||
)
|
||||
messageBox.setWindowTitle(_('Invalid data'))
|
||||
messageBox.setText(message)
|
||||
messageBox.open()
|
||||
mbox = AppQMessageBox(icon=AppQMessageBox.Icon.Warning, parent=self)
|
||||
mbox.setHeading(_('Invalid data'))
|
||||
mbox.setText(message)
|
||||
mbox.open()
|
||||
|
||||
def accept(self):
|
||||
"""Validate required fields before accepting the definition."""
|
||||
@@ -381,18 +378,14 @@ class SubscriptionPage(Mixins.QTranslatable, Mixins.ThemeAware, QMainWindow):
|
||||
|
||||
return
|
||||
|
||||
messageBox = AppQMessageBox(
|
||||
icon=AppQMessageBox.Icon.Question,
|
||||
parent=self,
|
||||
)
|
||||
messageBox.setWindowTitle(_('Add Subscription'))
|
||||
messageBox.setText(_('Use the subscription URL from the clipboard?'))
|
||||
messageBox.setInformativeText(url)
|
||||
messageBox.setStandardButtons(
|
||||
mbox = AppQMessageBox(icon=AppQMessageBox.Icon.Question, parent=self)
|
||||
mbox.setText(_('Use the subscription URL from the clipboard?'))
|
||||
mbox.setInformativeText(url)
|
||||
mbox.setStandardButtons(
|
||||
AppQMessageBox.StandardButton.Yes | AppQMessageBox.StandardButton.No
|
||||
)
|
||||
messageBox.setDefaultButton(AppQMessageBox.StandardButton.Yes)
|
||||
messageBox.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
|
||||
mbox.setDefaultButton(AppQMessageBox.StandardButton.Yes)
|
||||
mbox.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
|
||||
|
||||
def finished(code):
|
||||
"""Continue with clipboard assistance or the regular workflow."""
|
||||
@@ -403,8 +396,8 @@ class SubscriptionPage(Mixins.QTranslatable, Mixins.ThemeAware, QMainWindow):
|
||||
else:
|
||||
self._openEditor()
|
||||
|
||||
messageBox.finished.connect(finished)
|
||||
messageBox.open()
|
||||
mbox.finished.connect(finished)
|
||||
mbox.open()
|
||||
|
||||
def addFromClipboard(self, clipboardURL=None):
|
||||
"""Seed a subscription from an HTTP(S) URL on the clipboard."""
|
||||
|
||||
@@ -45,7 +45,6 @@ class MBoxQuestionSave(AppQMessageBox):
|
||||
"""Initialize the MBoxQuestionSave."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setWindowTitle(_('Save Changes'))
|
||||
self.setText(_('The content has been modified. Save changes?'))
|
||||
|
||||
self.button0 = self.addButton(_('Save'), AppQMessageBox.ButtonRole.AcceptRole)
|
||||
@@ -75,7 +74,6 @@ class MBoxJSONDecodeError(AppQMessageBox):
|
||||
|
||||
def retranslate(self):
|
||||
"""Refresh translated text for the m box JSON decode error."""
|
||||
self.setWindowTitle(_(self.windowTitle()))
|
||||
self.setText(self.customText())
|
||||
|
||||
# Ignore informative text, buttons
|
||||
@@ -313,7 +311,6 @@ class TextEditorWindow(AppQMainWindow):
|
||||
|
||||
mbox = MBoxJSONDecodeError(icon=AppQMessageBox.Icon.Critical, parent=self)
|
||||
mbox.error = str(ex)
|
||||
mbox.setWindowTitle(_('Error saving configuration'))
|
||||
mbox.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
|
||||
mbox.setText(mbox.customText())
|
||||
|
||||
@@ -358,7 +355,6 @@ class TextEditorWindow(AppQMainWindow):
|
||||
# Any non-exit exceptions
|
||||
|
||||
mbox = AppQMessageBox(icon=AppQMessageBox.Icon.Critical, parent=self)
|
||||
mbox.setWindowTitle(_('Error Saving File'))
|
||||
mbox.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
|
||||
mbox.setText(_('Invalid server configuration'))
|
||||
mbox.setInformativeText(str(ex))
|
||||
@@ -394,7 +390,6 @@ class TextEditorWindow(AppQMainWindow):
|
||||
|
||||
mbox = MBoxJSONDecodeError(icon=AppQMessageBox.Icon.Critical, parent=self)
|
||||
mbox.error = str(ex)
|
||||
mbox.setWindowTitle(_('Error setting indent'))
|
||||
mbox.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
|
||||
mbox.setText(mbox.customText())
|
||||
|
||||
|
||||
@@ -227,6 +227,7 @@ class QtLifetimeTest(unittest.TestCase):
|
||||
messageBox = AppQMessageBox(
|
||||
icon=AppQMessageBox.Icon.Information,
|
||||
parent=owner,
|
||||
heading='Fixture heading',
|
||||
text='Fixture information',
|
||||
buttons=AppQMessageBox.StandardButton.Ok,
|
||||
)
|
||||
|
||||
+139
-2
@@ -35,6 +35,7 @@ from Furious.Backends.Xray.VlessEditor import VlessEditor
|
||||
from Furious.Backends.Xray.VmessEditor import VmessEditor
|
||||
from Furious.Actions.Connection import ConnectAction
|
||||
from Furious.Controllers.ConnectionController import (
|
||||
ConnectionError,
|
||||
ConnectionController,
|
||||
ConnectionState,
|
||||
)
|
||||
@@ -42,9 +43,15 @@ from Furious.Controllers.SettingsController import (
|
||||
LOG_AUTO_CLEAR_SETTING,
|
||||
LOG_AUTO_SCROLL_DOWN_SETTING,
|
||||
)
|
||||
from Furious.Frozenlib import AppSettings, Mixins
|
||||
from Furious.Frozenlib import APPLICATION_NAME, AppSettings, Mixins
|
||||
from Furious.Models import ProfileMetadata, ServerProfile
|
||||
from Furious.Qt import AppHue, AppQMessageBox, AppQSwitch, gettext as _
|
||||
from Furious.Qt import (
|
||||
AppHue,
|
||||
AppQMessageBox,
|
||||
AppQSwitch,
|
||||
AppStyleSheet,
|
||||
gettext as _,
|
||||
)
|
||||
from Furious.Service import (
|
||||
APPLICATION_LOG_CATEGORY,
|
||||
CORE_LOG_CATEGORY,
|
||||
@@ -816,6 +823,136 @@ class DialogBehaviorTest(unittest.TestCase):
|
||||
self.assertEqual(finished, [int(AppQMessageBox.StandardButton.Yes)])
|
||||
self.assertIs(messageBox.clickedButton(), yesButton)
|
||||
|
||||
def testMessageBoxSeparatesNativeTitleHeadingAndBody(self):
|
||||
"""Keep native metadata independent from visible semantic content."""
|
||||
messageBox = AppQMessageBox(
|
||||
title='Native window metadata',
|
||||
heading='Unable to connect',
|
||||
text='Xray-core: Invalid server configuration',
|
||||
)
|
||||
messageBox.setInformativeText('The outbound server address is missing.')
|
||||
messageBox.show()
|
||||
|
||||
processQtEvents()
|
||||
|
||||
self.assertEqual(messageBox.windowTitle(), 'Native window metadata')
|
||||
self.assertEqual(messageBox.heading(), 'Unable to connect')
|
||||
self.assertEqual(messageBox.text(), 'Xray-core: Invalid server configuration')
|
||||
self.assertEqual(
|
||||
messageBox.informativeText(),
|
||||
'The outbound server address is missing.',
|
||||
)
|
||||
self.assertEqual(messageBox.headingLabel.text(), 'Unable to connect')
|
||||
self.assertEqual(
|
||||
messageBox.textLabel.text(),
|
||||
'Xray-core: Invalid server configuration',
|
||||
)
|
||||
self.assertFalse(messageBox.headingLabel.isHidden())
|
||||
|
||||
messageBox.setWindowTitle('Changed native metadata')
|
||||
|
||||
self.assertEqual(messageBox.heading(), 'Unable to connect')
|
||||
|
||||
messageBox.close()
|
||||
|
||||
def testMessageBoxKeepsGenericApplicationTitleAsMetadataOnly(self):
|
||||
"""Do not promote the default application name into visible content."""
|
||||
messageBox = AppQMessageBox(text='Operation completed')
|
||||
messageBox.show()
|
||||
|
||||
processQtEvents()
|
||||
|
||||
self.assertEqual(messageBox.windowTitle(), APPLICATION_NAME)
|
||||
self.assertEqual(messageBox.heading(), '')
|
||||
self.assertTrue(messageBox.headingLabel.isHidden())
|
||||
self.assertEqual(messageBox.textLabel.y(), 0)
|
||||
|
||||
positional = AppQMessageBox(
|
||||
AppQMessageBox.Icon.Information,
|
||||
'Native compatibility title',
|
||||
'Compatibility body',
|
||||
AppQMessageBox.StandardButton.Ok,
|
||||
)
|
||||
|
||||
self.assertEqual(positional.windowTitle(), 'Native compatibility title')
|
||||
self.assertEqual(positional.heading(), '')
|
||||
self.assertEqual(positional.text(), 'Compatibility body')
|
||||
|
||||
messageBox.close()
|
||||
positional.deleteLater()
|
||||
|
||||
def testConnectionErrorPreservesHeadingMessageAndDetails(self):
|
||||
"""Map every structured connection-error field to visible dialog content."""
|
||||
error = ConnectionError(
|
||||
'Unable to connect',
|
||||
'Xray-core: Invalid server configuration',
|
||||
'The outbound server address is missing.',
|
||||
)
|
||||
opened = []
|
||||
|
||||
with mock.patch.object(
|
||||
AppQMessageBox,
|
||||
'open',
|
||||
lambda messageBox: opened.append(messageBox),
|
||||
):
|
||||
ConnectAction.showError(error)
|
||||
|
||||
self.assertEqual(len(opened), 1)
|
||||
|
||||
messageBox = opened[0]
|
||||
|
||||
self.assertEqual(messageBox.windowTitle(), APPLICATION_NAME)
|
||||
self.assertEqual(messageBox.heading(), error.title)
|
||||
self.assertEqual(messageBox.text(), error.message)
|
||||
self.assertEqual(messageBox.informativeText(), error.details)
|
||||
|
||||
messageBox.close()
|
||||
messageBox.deleteLater()
|
||||
|
||||
def testMessageBoxHeadingSupportsLongContentIconsAndThemes(self):
|
||||
"""Keep the semantic stack responsive across themes and icon variants."""
|
||||
app = application()
|
||||
originalStyleSheet = app.styleSheet()
|
||||
longMessage = (
|
||||
'Не удалось применить конфигурацию прокси-сервера. '
|
||||
'Проверьте адрес, порт и параметры подключения, затем повторите попытку.'
|
||||
)
|
||||
|
||||
try:
|
||||
for theme in (AppStyleSheet.Light, AppStyleSheet.Dark):
|
||||
app.setStyleSheet(AppStyleSheet.forTheme(theme))
|
||||
|
||||
for icon in (
|
||||
AppQMessageBox.Icon.Information,
|
||||
AppQMessageBox.Icon.Warning,
|
||||
AppQMessageBox.Icon.Critical,
|
||||
AppQMessageBox.Icon.Question,
|
||||
):
|
||||
with self.subTest(theme=theme, icon=icon):
|
||||
messageBox = AppQMessageBox(
|
||||
icon=icon,
|
||||
heading='Unable to connect',
|
||||
text=longMessage,
|
||||
)
|
||||
messageBox.show()
|
||||
|
||||
processQtEvents()
|
||||
|
||||
self.assertFalse(messageBox.iconPixmap().isNull())
|
||||
self.assertGreaterEqual(
|
||||
messageBox.textLabel.height(),
|
||||
messageBox.textLabel.fontMetrics().height(),
|
||||
)
|
||||
self.assertLessEqual(
|
||||
messageBox.width(),
|
||||
messageBox.MaximumSurfaceWidth,
|
||||
)
|
||||
|
||||
messageBox.close()
|
||||
messageBox.deleteLater()
|
||||
finally:
|
||||
app.setStyleSheet(originalStyleSheet)
|
||||
|
||||
def testMessageBoxButtonsHaveAdaptiveFluentLayoutAndRoles(self):
|
||||
"""Keep one, two, and three actions slim, separated, and content-driven."""
|
||||
configurations = (
|
||||
|
||||
Reference in New Issue
Block a user