diff --git a/Furious/Qt/QtWidgets.py b/Furious/Qt/QtWidgets.py index c63d889..6f496bb 100644 --- a/Furious/Qt/QtWidgets.py +++ b/Furious/Qt/QtWidgets.py @@ -284,6 +284,22 @@ class AppQComboBox(Mixins.QTranslatable, QComboBox): self._themedSeparatorDelegate = None + def setContentWidthAdjustable(self, adjustable: bool = True): + """Opt into content-aware width hints without imposing a fixed width.""" + policy = ( + QComboBox.SizeAdjustPolicy.AdjustToContents + if adjustable + else QComboBox.SizeAdjustPolicy.AdjustToContentsOnFirstShow + ) + + self.setSizeAdjustPolicy(policy) + self.refreshContentGeometry() + + def refreshContentGeometry(self): + """Notify the owning layout that changed item text may need more width.""" + self.updateGeometry() + self.view().updateGeometry() + def enableThemedSeparators(self): """Theme real insertSeparator() rows while preserving native items.""" if self._themedSeparatorDelegate is not None: @@ -299,6 +315,8 @@ class AppQComboBox(Mixins.QTranslatable, QComboBox): for index in range(self.count()): self.setItemText(index, _(self.itemText(index))) + self.refreshContentGeometry() + class AppQDialog(Mixins.QTranslatable, Mixins.ConnectionAware, QDialog): """Present the app Qt dialog.""" diff --git a/Furious/Widget/RoutingSelector.py b/Furious/Widget/RoutingSelector.py index 92e5b01..014a38f 100644 --- a/Furious/Widget/RoutingSelector.py +++ b/Furious/Widget/RoutingSelector.py @@ -43,8 +43,8 @@ class RoutingSelector(AppQComboBox): self.setObjectName('HomeRoutingSelector') self.enableThemedSeparators() + self.setContentWidthAdjustable() self.setMinimumWidth(180) - self.setMaximumWidth(280) self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) self.setAccessibleName(_('Routing')) self.setToolTip(_('Routing')) @@ -76,6 +76,7 @@ class RoutingSelector(AppQComboBox): self.setCurrentIndex(index if index >= 0 else 0) + self.refreshContentGeometry() self.setVisible(bool(options)) self._applyInteractionEnabled(self.controller.interactionEnabled) diff --git a/tests/test_ui_behavior.py b/tests/test_ui_behavior.py index 4d5c874..7a64df7 100644 --- a/tests/test_ui_behavior.py +++ b/tests/test_ui_behavior.py @@ -101,6 +101,129 @@ import weakref from unittest import mock +class RoutingControllerFixture(QtCore.QObject): + """Provide routing state without persistence or connection side effects.""" + + stateChanged = QtCore.Signal(object, str) + interactionEnabledChanged = QtCore.Signal(bool) + + def __init__(self, options, routing): + """Initialize one immutable selector state.""" + super().__init__() + + self._options = tuple(options) + self._routing = routing + self.interactionEnabled = True + self.selected = [] + + def state(self): + """Return the current options and semantic selection.""" + return self._options, self._routing + + def refresh(self, *, force=False): + """Return state without consulting plugins or reconnecting.""" + if force: + self.stateChanged.emit(*self.state()) + + return self.state() + + def selectRouting(self, routing): + """Record explicit user selection attempts.""" + self.selected.append(routing) + + +class ComboTranslationLayoutTest(unittest.TestCase): + """Verify translated content can republish useful combo width hints.""" + + @classmethod + def setUpClass(cls): + """Create the process-wide headless QApplication.""" + application() + + def tearDown(self): + """Finish deferred widget deletion between tests.""" + collectAtBoundary() + + def testContentAwareComboRefreshesHintWithoutChangingSemanticData(self): + """Grow for longer translated text while retaining item identity.""" + parent = QWidget() + layout = QHBoxLayout(parent) + combo = AppQComboBox(parent=parent) + combo.setContentWidthAdjustable() + combo.addItem('Short', 'semantic-route') + layout.addWidget(combo) + + shortHint = combo.sizeHint().width() + translated = 'A significantly longer translated routing option' + + with mock.patch( + 'Furious.Qt.QtWidgets._', + side_effect=lambda text: translated if text == 'Short' else text, + ): + combo.retranslate() + + parent.resize(combo.sizeHint().width() + 100, 100) + parent.show() + processQtEvents() + + self.assertEqual(combo.currentData(), 'semantic-route') + self.assertEqual(combo.currentText(), translated) + self.assertGreater(combo.sizeHint().width(), shortHint) + self.assertGreater( + combo.sizeHint().width(), + combo.fontMetrics().horizontalAdvance(translated), + ) + self.assertGreaterEqual(combo.width(), combo.sizeHint().width()) + + parent.close() + parent.deleteLater() + + def testRoutingRetranslationKeepsSelectionAndDoesNotReconnect(self): + """Rebuild display text without forwarding a synthetic selection.""" + option = RoutingOption( + 'bypass-mainland', + 'Bypass Mainland China', + translatable=True, + ) + controller = RoutingControllerFixture((option,), option.id) + parent = QWidget() + layout = QHBoxLayout(parent) + + with mock.patch( + 'Furious.Widget.RoutingSelector.AppRoutingController', + return_value=controller, + ): + selector = RoutingSelector(parent=parent) + + layout.addWidget(selector) + initialHint = selector.sizeHint().width() + translated = 'Обходить подключения к серверам материкового Китая' + + with mock.patch( + 'Furious.Widget.RoutingSelector._', + side_effect=lambda text: ( + translated if text == option.displayName else text + ), + ): + selector.retranslate() + + parent.resize(selector.sizeHint().width() + 100, 100) + parent.show() + processQtEvents() + + self.assertEqual(selector.currentData(), option.id) + self.assertEqual(selector.currentText(), translated) + self.assertGreater(selector.sizeHint().width(), initialHint) + self.assertGreater( + selector.sizeHint().width(), + selector.fontMetrics().horizontalAdvance(translated), + ) + self.assertEqual(controller.selected, []) + + parent.close() + parent.deleteLater() + + class EditorMappingTest(unittest.TestCase): """Verify editor fields preserve structured configuration semantics."""