mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-10-08 22:59:48 +03:00
Improve widget retranslation
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
@@ -304,8 +304,8 @@ class ImportURIsProgressDialog(AppQTransientDialog):
|
||||
|
||||
def retranslate(self):
|
||||
"""Refresh translated text for the import ur is progress dialog."""
|
||||
self.setWindowTitle(_(self.windowTitle()))
|
||||
self.cancelButton.setText(_(self.cancelButton.text()))
|
||||
super().retranslate()
|
||||
|
||||
self.updateStatus()
|
||||
|
||||
|
||||
@@ -316,14 +316,10 @@ class MBoxImportError(AppQMessageBox):
|
||||
"""Initialize the MBoxImportError."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def retranslate(self):
|
||||
def _retranslateContent(self):
|
||||
"""Refresh translated text for the m box import error."""
|
||||
self.setText(_(self.text()))
|
||||
|
||||
# Ignore informative text, buttons
|
||||
|
||||
self.moveToCenter()
|
||||
|
||||
|
||||
class MBoxImportMultiSuccess(AppQMessageBox):
|
||||
"""Represent m box import multi success."""
|
||||
@@ -358,14 +354,10 @@ class MBoxImportMultiSuccess(AppQMessageBox):
|
||||
# Limited
|
||||
return _('Import share link success') + f'\n\n...'
|
||||
|
||||
def retranslate(self):
|
||||
def _retranslateContent(self):
|
||||
"""Refresh translated text for the m box import multi success."""
|
||||
self.setText(self.customText())
|
||||
|
||||
# Ignore informative text, buttons
|
||||
|
||||
self.moveToCenter()
|
||||
|
||||
|
||||
class MBoxImportSuccess(AppQMessageBox):
|
||||
"""Represent m box import success."""
|
||||
@@ -383,14 +375,10 @@ class MBoxImportSuccess(AppQMessageBox):
|
||||
else:
|
||||
return _('Import success')
|
||||
|
||||
def retranslate(self):
|
||||
def _retranslateContent(self):
|
||||
"""Refresh translated text for the m box import success."""
|
||||
self.setText(self.customText())
|
||||
|
||||
# Ignore informative text, buttons
|
||||
|
||||
self.moveToCenter()
|
||||
|
||||
|
||||
class ImportFromFileAction(AppQAction):
|
||||
"""Handle the import from file action."""
|
||||
|
||||
@@ -49,14 +49,10 @@ class MBoxAssetExists(AppQMessageBox):
|
||||
AppQMessageBox.StandardButton.Yes | AppQMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
def retranslate(self):
|
||||
def _retranslateContent(self):
|
||||
"""Refresh translated text for the m box asset exists."""
|
||||
self.setText(_(self.text()))
|
||||
|
||||
# Ignore informative text, buttons
|
||||
|
||||
self.moveToCenter()
|
||||
|
||||
|
||||
class XrayAssetListView(Mixins.ThemeAware, AppQListView):
|
||||
"""Provide the model-based Xray asset viewer list."""
|
||||
|
||||
@@ -15,6 +15,14 @@ behavior, and lifetime primitives; pages and services consume them without creat
|
||||
object-name rule even when the widget's enabled state is correct.
|
||||
- Controls that retranslate retain source text; semantic/user-defined values stay untranslated. Preserve keyboard focus,
|
||||
shortcut scope, accessibility, translated-text growth, responsive layout, high-DPI behavior, and both themes.
|
||||
Extend a concrete base retranslator when its standard fields still apply; retain intentional state/content
|
||||
overrides instead of calling the abstract mixin. Registered child controls translate themselves, including their
|
||||
opt-out flags; owners translate plain Qt controls that have no registration. Message boxes customize
|
||||
`_retranslateContent()` for composite text or preserved diagnostics, leaving title, heading, buttons and geometry
|
||||
with the shared retranslator. The locale-switch cases in `tests/test_ui_behavior.py` cover both button policies
|
||||
and diagnostic preservation; progress and metrics cases cover one child refresh per global pass.
|
||||
Visible message boxes refit after registered children finish translating, through a weak continuation that
|
||||
ignores hidden or destroyed dialogs. Preserve their bounded button widths and transient destruction policy.
|
||||
- Application-owned theme transitions commit destination state immediately; snapshots are non-interactive presentation
|
||||
objects that are interrupted/disposed on replacement, geometry change, window destruction, or application cleanup.
|
||||
Geometry/destruction event filters retire snapshots immediately but defer completion observers through an owned
|
||||
|
||||
+9
-3
@@ -304,12 +304,18 @@ class AppQAction(Mixins.QTranslatable, Mixins.ThemeAware, QAction):
|
||||
|
||||
def recursiveTranslate(action, memo):
|
||||
"""Handle recursive translate for the app q action."""
|
||||
if action not in memo and not action.isSeparator() and action.translatable:
|
||||
action.setText(_(action.text()))
|
||||
action.setStatusTip(_(action.statusTip()))
|
||||
if action in memo:
|
||||
return
|
||||
|
||||
memo[action] = True
|
||||
|
||||
if action is not self and isinstance(action, Mixins.QTranslatable):
|
||||
return
|
||||
|
||||
if not action.isSeparator() and getattr(action, 'translatable', True):
|
||||
action.setText(_(action.text()))
|
||||
action.setStatusTip(_(action.statusTip()))
|
||||
|
||||
# Some old version PySide6 does not have menu() method
|
||||
# for QAction. Protect it
|
||||
if hasattr(action, 'menu'):
|
||||
|
||||
+33
-28
@@ -522,7 +522,9 @@ class AppQDialogButtonBox(Mixins.QTranslatable, QDialogButtonBox):
|
||||
def retranslate(self):
|
||||
"""Refresh translated text for the app Qt dialog button box."""
|
||||
for button in self.buttons():
|
||||
button.setText(_(button.text()))
|
||||
if not isinstance(button, Mixins.QTranslatable):
|
||||
# Manage the translation explicitly
|
||||
button.setText(_(button.text()))
|
||||
|
||||
|
||||
class AppQGroupBox(Mixins.QTranslatable, QGroupBox):
|
||||
@@ -1995,7 +1997,7 @@ class AppQMessageBox(AppQTransientDialog):
|
||||
|
||||
def retranslate(self):
|
||||
"""Refresh translated text for the app q message box."""
|
||||
self.setWindowTitle(_(self.windowTitle()))
|
||||
super().retranslate()
|
||||
|
||||
try:
|
||||
self.setHeading(_(self.heading()))
|
||||
@@ -2003,6 +2005,32 @@ class AppQMessageBox(AppQTransientDialog):
|
||||
# Runtime headings may already be localized by their owning layer.
|
||||
pass
|
||||
|
||||
self._retranslateContent()
|
||||
|
||||
for button in self.buttons():
|
||||
if isinstance(button, Mixins.QTranslatable):
|
||||
continue
|
||||
|
||||
if button.text().find('OK') != -1:
|
||||
# &OK...
|
||||
pass
|
||||
else:
|
||||
button.setText(_(button.text()))
|
||||
|
||||
if self.isVisible():
|
||||
# Registered child buttons finish translating later in the global pass.
|
||||
singleShotWeakly(0, self, '_refreshTranslatedGeometry')
|
||||
else:
|
||||
self.moveToCenter()
|
||||
|
||||
def _refreshTranslatedGeometry(self):
|
||||
"""Fit current translated controls after the registered children update."""
|
||||
if self.isVisible():
|
||||
self._updateDialogSize()
|
||||
self.moveToCenter()
|
||||
|
||||
def _retranslateContent(self):
|
||||
"""Translate ordinary content; subclasses own composite or diagnostic text."""
|
||||
self.setText(_(self.text()))
|
||||
|
||||
try:
|
||||
@@ -2011,15 +2039,6 @@ class AppQMessageBox(AppQTransientDialog):
|
||||
# Any translatable informative text
|
||||
pass
|
||||
|
||||
for button in self.buttons():
|
||||
if button.text().find('OK') != -1:
|
||||
# &OK...
|
||||
pass
|
||||
else:
|
||||
button.setText(_(button.text()))
|
||||
|
||||
self.moveToCenter()
|
||||
|
||||
def disconnectedCallback(self):
|
||||
"""Update the app q message box for a disconnected state."""
|
||||
self.setWindowIcon(AppHue.disconnectedWindowIcon())
|
||||
@@ -2466,14 +2485,10 @@ class MBoxQuestionDelete(AppQMessageBox):
|
||||
else:
|
||||
return _('Delete this item?') + f'\n\n{self.possibleRemark}'
|
||||
|
||||
def retranslate(self):
|
||||
def _retranslateContent(self):
|
||||
"""Refresh translated text for the m box question delete."""
|
||||
self.setText(self.customText())
|
||||
|
||||
# Ignore informative text, buttons
|
||||
|
||||
self.moveToCenter()
|
||||
|
||||
|
||||
class MBoxNewChangesNextTime(AppQMessageBox):
|
||||
"""Represent m box new changes next time."""
|
||||
@@ -2496,15 +2511,10 @@ class MBoxNewChangesNextTime(AppQMessageBox):
|
||||
_('New changes will take effect next time') + '\n\n' + _('Reconnect now?')
|
||||
)
|
||||
|
||||
def retranslate(self):
|
||||
def _retranslateContent(self):
|
||||
"""Refresh translated text for the m box new changes next time."""
|
||||
self.setWindowTitle(_(self.windowTitle()))
|
||||
self.setText(self.customText())
|
||||
|
||||
# Ignore informative text, buttons
|
||||
|
||||
self.moveToCenter()
|
||||
|
||||
|
||||
def showMBoxNewChangesNextTime(**kwargs):
|
||||
"""Show m box new changes next time."""
|
||||
@@ -2565,15 +2575,10 @@ class MBoxDirectRulesNotAllowed(AppQMessageBox):
|
||||
+ _('Switch to global and reconnect?')
|
||||
)
|
||||
|
||||
def retranslate(self):
|
||||
def _retranslateContent(self):
|
||||
"""Refresh translated text for the m box direct rules not allowed."""
|
||||
self.setHeading(_(self.heading()))
|
||||
self.setText(self.customText())
|
||||
|
||||
# Ignore informative text, buttons
|
||||
|
||||
self.moveToCenter()
|
||||
|
||||
|
||||
def showMBoxDirectRulesNotAllowed(**kwargs):
|
||||
"""Show m box direct rules not allowed."""
|
||||
|
||||
@@ -54,16 +54,11 @@ class MBoxQuestionUpdate(AppQMessageBox):
|
||||
"""Return the user-facing message text for the m box question update."""
|
||||
return _('New version available') + f': {self.version}'
|
||||
|
||||
def retranslate(self):
|
||||
def _retranslateContent(self):
|
||||
"""Refresh translated text for the m box question update."""
|
||||
self.setText(self.customText())
|
||||
self.setWindowTitle(_(self.windowTitle()))
|
||||
self.setInformativeText(_(self.informativeText()))
|
||||
|
||||
# Ignore button text
|
||||
|
||||
self.moveToCenter()
|
||||
|
||||
|
||||
class UpdateManager(HttpGetManager):
|
||||
"""Coordinate updates operations."""
|
||||
|
||||
@@ -122,16 +122,11 @@ class MBoxUpdateSubsInfo(AppQMessageBox):
|
||||
* self.fontMetrics().averageCharWidth(),
|
||||
)
|
||||
|
||||
def retranslate(self):
|
||||
def _retranslateContent(self):
|
||||
"""Refresh translated text for the m box update subs info."""
|
||||
self.setWindowTitle(_(self.windowTitle()))
|
||||
self.setText(self.customText())
|
||||
self.setColumnMinWidth()
|
||||
|
||||
# Ignore informative text, buttons
|
||||
|
||||
self.moveToCenter()
|
||||
|
||||
|
||||
class DeleteServersProgressDialog(AppQTransientDialog):
|
||||
"""Present progress and cancellation controls for delete servers."""
|
||||
@@ -279,8 +274,8 @@ class DeleteServersProgressDialog(AppQTransientDialog):
|
||||
|
||||
def retranslate(self):
|
||||
"""Refresh translated text for the delete servers progress dialog."""
|
||||
self.setWindowTitle(_(self.windowTitle()))
|
||||
self.cancelButton.setText(_(self.cancelButton.text()))
|
||||
super().retranslate()
|
||||
|
||||
self.updateStatus()
|
||||
|
||||
|
||||
@@ -320,6 +315,7 @@ class DuplicateServersProgressDialog(AppQTransientDialog):
|
||||
|
||||
def open(self):
|
||||
result = super().open()
|
||||
|
||||
singleShotWeakly(0, self, 'duplicateNext')
|
||||
|
||||
return result
|
||||
@@ -1501,6 +1497,7 @@ class ServerTableView(
|
||||
for row, profile in enumerate(Storage.UserServers())
|
||||
if profile.metadata.profileId in changed
|
||||
]
|
||||
|
||||
self.sourceModel.dataChanged.emit(
|
||||
self.sourceModel.index(rows[0], 0),
|
||||
self.sourceModel.index(rows[-1], self.sourceModel.columnCount() - 1),
|
||||
|
||||
@@ -131,14 +131,10 @@ class MBoxSaveError(AppQMessageBox):
|
||||
else:
|
||||
return _('Unable to save log')
|
||||
|
||||
def retranslate(self):
|
||||
def _retranslateContent(self):
|
||||
"""Refresh translated text for the m box save error."""
|
||||
self.setText(self.customText())
|
||||
|
||||
# Ignore informative text, buttons
|
||||
|
||||
self.moveToCenter()
|
||||
|
||||
|
||||
def saveAsFile(content: str):
|
||||
"""Save as file."""
|
||||
|
||||
@@ -458,7 +458,5 @@ class MetricsPage(Mixins.QTranslatable, Mixins.ThemeAware, QMainWindow):
|
||||
_('Upload Speed'),
|
||||
_('Upload Traffic Usage'),
|
||||
)
|
||||
self.endpointInfoWidget.retranslate()
|
||||
|
||||
self._dirty = True
|
||||
self._scheduleRender()
|
||||
|
||||
@@ -72,14 +72,10 @@ class MBoxJSONDecodeError(AppQMessageBox):
|
||||
+ f'\n\n{self.error}'
|
||||
)
|
||||
|
||||
def retranslate(self):
|
||||
def _retranslateContent(self):
|
||||
"""Refresh translated text for the m box JSON decode error."""
|
||||
self.setText(self.customText())
|
||||
|
||||
# Ignore informative text, buttons
|
||||
|
||||
self.moveToCenter()
|
||||
|
||||
|
||||
class TextEditorWindow(AppQMainWindow):
|
||||
"""Present the text editor window."""
|
||||
|
||||
+2
-2
@@ -102,7 +102,7 @@ worker. Choose tests by the changed contract rather than by filename alone.
|
||||
| [test_service_runtime.py](test_service_runtime.py) | Update-response validation, HTTP timeouts/context release/reentrant destruction, duplicate completion, plugin-page registration, bounded connectivity requests, blocked statistics-worker callback lifetime. |
|
||||
| [test_xray_asset_download.py](test_xray_asset_download.py) | Checksum validation, atomic asset replacement, real pool delivery, early owner destruction, callback release, plugin shutdown. |
|
||||
| [test_endpoint_info.py](test_endpoint_info.py) | Opt-in proxy-only discovery using fake HTTP responses, fallback/cache/session invalidation, privacy/presentation controls, local map styling and persistent-scene contracts. |
|
||||
| [test_metrics_behavior.py](test_metrics_behavior.py) | Immutable time-series history, pruning/buckets, usage versus speed clearing, hidden-page rendering, graph hover lookup. |
|
||||
| [test_metrics_behavior.py](test_metrics_behavior.py) | Immutable time-series history, pruning/buckets, usage versus speed clearing, hidden-page rendering, graph hover lookup, and independent endpoint retranslation without changing history. |
|
||||
| [test_log_manager_generation.py](test_log_manager_generation.py) | Reference-model state transitions, concurrency, live/retired ownership, bounded FIFO cleanup, structural complexity, incremental/multiline LogPage rendering; also contains an opt-in generation campaign. |
|
||||
| [test_log_manager_benchmark.py](test_log_manager_benchmark.py) | Data-only report validation, workload compatibility, timing ratios, and real standalone CLI JSON round trips; no performance gates or historical imports. |
|
||||
|
||||
@@ -114,7 +114,7 @@ worker. Choose tests by the changed contract rather than by filename alone.
|
||||
| [test_main_window_geometry.py](test_main_window_geometry.py) | First-show lifecycle, restored/default/legacy geometry, main/routing window reuse, session-only navigation state, protection of never-shown windows' saved geometry. |
|
||||
| [test_dialog_geometry.py](test_dialog_geometry.py) | Dialog show/open/exec preparation, sizing/centering, failure cleanup, specialized message-box geometry and transient destruction. |
|
||||
| [test_layout_matrix.py](test_layout_matrix.py) | Navigation and message-box layouts in fresh processes at scale factors 1, 1.25, 1.5, and 2, under both themes. |
|
||||
| [test_ui_behavior.py](test_ui_behavior.py) | Translation and editor mappings, settings construction visibility/organization/search/platform gates, stable server/routing moves, real QR rendering/decoding, incremental log filtering/tail behavior, connected-routing change notices, message boxes and connection-error copy/log navigation, shared Home/tray state. |
|
||||
| [test_ui_behavior.py](test_ui_behavior.py) | Translation and editor mappings, settings construction visibility/organization/search/platform gates, stable server/routing moves, real QR rendering/decoding, incremental log filtering/tail behavior, connected-routing change notices, message boxes and connection-error copy/log navigation, shared Home/tray state, preserved message payloads and managed-versus-plain button retranslation. |
|
||||
| [test_qt_interactions.py](test_qt_interactions.py) | Real keyboard/mouse/focus and proxy mapping, scoped shortcuts, sorting/selection, stable subscription deletion confirmations, debounced Home search, favorite actions and intersecting filters, Tests-menu selection color, shared settings, batched import/delete/duplication, cancellation, preparation failure, and owner destruction. |
|
||||
| [test_qr_export_scalability.py](test_qr_export_scalability.py) | Production capture cap, immediate single export, incremental yielding, failure/cancel/close paths, immutable snapshots, window-owned state destruction. |
|
||||
| [test_stylesheet_states.py](test_stylesheet_states.py) | Targeted rendering/alpha/geometry assertions for table/list insets, popup corners, clear buttons, focus/disabled states, and stylesheet composition. |
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import AppSettings, Mixins
|
||||
from Furious.Qt import gettext as _
|
||||
from Furious.Service.MetricsHistory import (
|
||||
DOWNLOAD_SPEED_METRIC,
|
||||
@@ -33,7 +34,15 @@ from Furious.Window.MetricsPage import MetricsPage
|
||||
|
||||
from PySide6 import QtCore
|
||||
|
||||
from tests.support import application, collectAtBoundary, processQtEvents, waitFor
|
||||
from tests.support import (
|
||||
application,
|
||||
collectAtBoundary,
|
||||
isolatedSettings,
|
||||
processQtEvents,
|
||||
waitFor,
|
||||
)
|
||||
|
||||
from unittest import mock
|
||||
|
||||
import math
|
||||
import time
|
||||
@@ -190,6 +199,39 @@ class MetricsPageAndGraphTest(unittest.TestCase):
|
||||
"""Drain deferred widget destruction after every UI case."""
|
||||
collectAtBoundary()
|
||||
|
||||
def testGlobalRetranslationRefreshesEndpointOnceWithoutChangingHistory(self):
|
||||
with isolatedSettings():
|
||||
AppSettings.set('Language', 'EN')
|
||||
manager = MetricsHistory()
|
||||
page = MetricsPage(manager)
|
||||
manager.recordSample({DOWNLOAD_SPEED_METRIC: 32})
|
||||
samples = manager.rawSamples()
|
||||
|
||||
try:
|
||||
for locale in ('RU', 'ZH', 'EN'):
|
||||
AppSettings.set('Language', locale)
|
||||
with mock.patch.object(
|
||||
page.endpointInfoWidget,
|
||||
'retranslate',
|
||||
wraps=page.endpointInfoWidget.retranslate,
|
||||
) as translate:
|
||||
Mixins.QTranslatable.retranslateAll()
|
||||
self.assertEqual(translate.call_count, 1)
|
||||
self.assertEqual(
|
||||
page.metricsCard.downloadTitleLabel.text(), _('Download')
|
||||
)
|
||||
self.assertEqual(
|
||||
page.endpointInfoWidget.titleLabel.text(),
|
||||
_('Proxy Endpoint Information'),
|
||||
)
|
||||
self.assertEqual(manager.rawSamples(), samples)
|
||||
self.assertEqual(page._renderRevision, 0)
|
||||
finally:
|
||||
page.close()
|
||||
page.deleteLater()
|
||||
manager.deleteLater()
|
||||
processQtEvents()
|
||||
|
||||
def testTrafficGraphsAndSelectorsShareOneCard(self):
|
||||
"""Keep the page title separate while grouping all metrics controls."""
|
||||
manager = MetricsHistory()
|
||||
|
||||
@@ -2343,6 +2343,59 @@ class ProfileMutationBatchTest(unittest.TestCase):
|
||||
self.assertFalse(isValid(dialog))
|
||||
self.assertEqual(len(Storage.UserServers()), 1000)
|
||||
|
||||
def testProgressDialogsUseSharedRetranslationWithoutLosingProgress(self):
|
||||
with self.table(10) as (table, controller):
|
||||
AppSettings.set('Language', 'EN')
|
||||
dialogs = (
|
||||
(
|
||||
ImportURIsProgressDialog(('fixture',) * 10, parent=table),
|
||||
'Import',
|
||||
'Importing',
|
||||
'currentIndex',
|
||||
),
|
||||
(
|
||||
DeleteServersProgressDialog(table, range(10), parent=table),
|
||||
'Delete',
|
||||
'Deleting',
|
||||
'deletedCount',
|
||||
),
|
||||
(
|
||||
DuplicateServersProgressDialog(table, table._visibleProfileIds()),
|
||||
'Duplicate',
|
||||
'Duplicating',
|
||||
'copiedCount',
|
||||
),
|
||||
)
|
||||
|
||||
for dialog, title, status, counter in dialogs:
|
||||
setattr(dialog, counter, 7)
|
||||
|
||||
for locale in ('RU', 'ZH', 'EN'):
|
||||
AppSettings.set('Language', locale)
|
||||
with ExitStack() as stack:
|
||||
translations = [
|
||||
stack.enter_context(
|
||||
mock.patch.object(
|
||||
dialog.cancelButton,
|
||||
'retranslate',
|
||||
wraps=dialog.cancelButton.retranslate,
|
||||
)
|
||||
)
|
||||
for dialog, *_fields in dialogs
|
||||
]
|
||||
Mixins.QTranslatable.retranslateAll()
|
||||
|
||||
for (dialog, title, status, counter), translated in zip(
|
||||
dialogs, translations
|
||||
):
|
||||
self.assertEqual(translated.call_count, 1)
|
||||
self.assertEqual(dialog.windowTitle(), gettext(title))
|
||||
self.assertEqual(dialog.cancelButton.text(), gettext('Cancel'))
|
||||
self.assertEqual(
|
||||
dialog.statusLabel.text(), gettext(status) + '... 7/10'
|
||||
)
|
||||
self.assertEqual(getattr(dialog, counter), 7)
|
||||
|
||||
def testImportCoalescesRowsAndProgressWithoutLosingInvalidInputPositions(self):
|
||||
profiles = [self.profile(str(index)) for index in range(600)]
|
||||
invalid = mock.Mock()
|
||||
|
||||
+120
-1
@@ -66,6 +66,7 @@ from Furious.Backends.Xray.VmessEditor import (
|
||||
VmessEditor,
|
||||
)
|
||||
from Furious.Actions.Connection import ConnectAction, ConnectionErrorMessageBox
|
||||
from Furious.Actions.Import import MBoxImportError
|
||||
from Furious.Controllers.ConnectionController import (
|
||||
ConnectionError,
|
||||
ConnectionController,
|
||||
@@ -99,9 +100,13 @@ from Furious.Repository import Storage
|
||||
from Furious.Repository.Routings import UserRoutings
|
||||
from Furious.Qt import (
|
||||
AppHue,
|
||||
AppQAction,
|
||||
AppQComboBox,
|
||||
AppQDialog,
|
||||
AppQDialogButtonBox,
|
||||
AppQMessageBox,
|
||||
AppQMenu,
|
||||
AppQPushButton,
|
||||
AppQSwitch,
|
||||
AppStyleSheet,
|
||||
gettext as _,
|
||||
@@ -130,9 +135,10 @@ from Furious.Widget.RoutingSelector import RoutingSelector
|
||||
from Furious.Widget.ServerTableView import ServerTableView
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtGui import QImage
|
||||
from PySide6.QtGui import QAction, QImage
|
||||
from PySide6.QtTest import QAbstractItemModelTester, QSignalSpy, QTest
|
||||
from PySide6.QtWidgets import (
|
||||
QPushButton,
|
||||
QStackedWidget,
|
||||
QToolButton,
|
||||
QComboBox,
|
||||
@@ -2972,6 +2978,119 @@ class DialogBehaviorTest(unittest.TestCase):
|
||||
"""Finish every deferred transient deletion between tests."""
|
||||
collectAtBoundary()
|
||||
|
||||
def testCustomMessageRetranslationKeepsPayloadAndUpdatesDialogControls(self):
|
||||
with isolatedSettings():
|
||||
AppSettings.set('Language', 'EN')
|
||||
box = MBoxImportError()
|
||||
box.setWindowTitle(_('Duplicate'))
|
||||
box.setHeading(_('Delete'))
|
||||
box.setText(_('Invalid data'))
|
||||
box.setInformativeText('Save')
|
||||
button = box.addButton('Cancel', box.ButtonRole.RejectRole)
|
||||
|
||||
try:
|
||||
for locale in ('RU', 'ZH', 'EN'):
|
||||
AppSettings.set('Language', locale)
|
||||
Mixins.QTranslatable.retranslateAll()
|
||||
self.assertEqual(box.windowTitle(), _('Duplicate'))
|
||||
self.assertEqual(box.heading(), _('Delete'))
|
||||
self.assertEqual(box.text(), _('Invalid data'))
|
||||
self.assertEqual(box.informativeText(), 'Save')
|
||||
self.assertEqual(button.text(), _('Cancel'))
|
||||
finally:
|
||||
box.deleteLater()
|
||||
processQtEvents()
|
||||
|
||||
def testButtonContainersRespectManagedTranslationAndPlainQtFallback(self):
|
||||
with isolatedSettings():
|
||||
AppSettings.set('Language', 'EN')
|
||||
|
||||
for containerType in (AppQMessageBox, AppQDialogButtonBox):
|
||||
with self.subTest(container=containerType.__name__):
|
||||
owner = containerType()
|
||||
protected = AppQPushButton('Cancel', translatable=False)
|
||||
managed = AppQPushButton('Save')
|
||||
plain = QPushButton('Cancel')
|
||||
|
||||
for button in (protected, managed, plain):
|
||||
owner.addButton(button, owner.ButtonRole.ActionRole)
|
||||
|
||||
try:
|
||||
for locale in ('RU', 'ZH', 'EN'):
|
||||
AppSettings.set('Language', locale)
|
||||
with mock.patch.object(
|
||||
managed, 'retranslate', wraps=managed.retranslate
|
||||
) as translate:
|
||||
Mixins.QTranslatable.retranslateAll()
|
||||
self.assertEqual(translate.call_count, 1)
|
||||
self.assertEqual(protected.text(), 'Cancel')
|
||||
self.assertEqual(managed.text(), _('Save'))
|
||||
self.assertEqual(plain.text(), _('Cancel'))
|
||||
finally:
|
||||
owner.deleteLater()
|
||||
processQtEvents()
|
||||
|
||||
def testActionRetranslationLeavesManagedChildrenToTheirOwnPass(self):
|
||||
with isolatedSettings():
|
||||
AppSettings.set('Language', 'EN')
|
||||
owner = QWidget()
|
||||
menu = AppQMenu(parent=owner)
|
||||
managed = AppQAction('Save', parent=menu)
|
||||
protected = AppQAction('Cancel', parent=menu, translatable=False)
|
||||
menu.addActions([managed, protected])
|
||||
parent = AppQAction('Server', parent=owner, menu=menu)
|
||||
|
||||
try:
|
||||
for locale in ('RU', 'ZH', 'EN'):
|
||||
AppSettings.set('Language', locale)
|
||||
with mock.patch.object(
|
||||
managed, 'setText', wraps=managed.setText
|
||||
) as update:
|
||||
Mixins.QTranslatable.retranslateAll()
|
||||
self.assertEqual(update.call_count, 1)
|
||||
self.assertEqual(parent.text(), _('Server'))
|
||||
self.assertEqual(managed.text(), _('Save'))
|
||||
self.assertEqual(protected.text(), 'Cancel')
|
||||
|
||||
plain = QAction('Cancel', menu)
|
||||
menu.addAction(plain)
|
||||
AppSettings.set('Language', 'ZH')
|
||||
Mixins.QTranslatable.retranslateAll()
|
||||
self.assertEqual(plain.text(), _('Cancel'))
|
||||
finally:
|
||||
owner.deleteLater()
|
||||
processQtEvents()
|
||||
|
||||
def testMessageGeometryRefreshesAfterManagedButtonTranslation(self):
|
||||
with isolatedSettings():
|
||||
AppSettings.set('Language', 'EN')
|
||||
box = AppQMessageBox()
|
||||
button = AppQPushButton(_('Copy Error'))
|
||||
box.addButton(button, box.ButtonRole.ActionRole)
|
||||
box.open()
|
||||
processQtEvents()
|
||||
|
||||
try:
|
||||
for locale in ('RU', 'ZH', 'EN'):
|
||||
AppSettings.set('Language', locale)
|
||||
Mixins.QTranslatable.retranslateAll()
|
||||
processQtEvents()
|
||||
self.assertEqual(button.text(), _('Copy Error'))
|
||||
self.assertGreaterEqual(
|
||||
button.width(), box._preferredButtonWidth(button)
|
||||
)
|
||||
self.assertEqual(box.surface.size(), box.size())
|
||||
|
||||
AppSettings.set('Language', 'RU')
|
||||
Mixins.QTranslatable.retranslateAll()
|
||||
deleteQObject(box)
|
||||
processQtEvents()
|
||||
self.assertFalse(isValid(box))
|
||||
finally:
|
||||
if isValid(box):
|
||||
box.close()
|
||||
processQtEvents()
|
||||
|
||||
def testThemeFallbackWorksBeforeConnectionControllerExists(self):
|
||||
"""Allow startup error dialogs to use the disconnected theme safely."""
|
||||
with mock.patch(
|
||||
|
||||
Reference in New Issue
Block a user