Refine Fluent message box content hierarchy

Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
Loren Eteval
2026-08-21 15:13:16 +08:00
parent 1408dee387
commit 55869b6087
17 changed files with 262 additions and 144 deletions
+2
View File
@@ -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
+1 -1
View File
@@ -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:
-8
View File
@@ -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))
+4 -4
View File
@@ -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
+3 -3
View File
@@ -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
-4
View File
@@ -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))
+1 -52
View File
@@ -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"
+4
View File
@@ -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
View File
@@ -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
+9 -1
View File
@@ -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;
-1
View File
@@ -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':
-2
View File
@@ -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
+5 -8
View File
@@ -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:
+12 -19
View File
@@ -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."""
-5
View File
@@ -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())
+1
View File
@@ -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
View File
@@ -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 = (