From c9b4900a32067f70a8fc01810a640439912f601f Mon Sep 17 00:00:00 2001 From: Loren Eteval Date: Fri, 28 Aug 2026 11:07:38 +0800 Subject: [PATCH] fix: align External Core timeout row Signed-off-by: Loren Eteval --- Furious/Backends/ExternalCore/Editor.py | 35 +++++++--- tests/test_ui_behavior.py | 85 +++++++++++++++++++++++-- 2 files changed, 108 insertions(+), 12 deletions(-) diff --git a/Furious/Backends/ExternalCore/Editor.py b/Furious/Backends/ExternalCore/Editor.py index b3db844..f40a1a6 100644 --- a/Furious/Backends/ExternalCore/Editor.py +++ b/Furious/Backends/ExternalCore/Editor.py @@ -416,32 +416,51 @@ class ExternalCoreOtherGroup(GuiEditorWidgetQGroupBox): def containerSequence(self): """Return secondary process, proxy, and TUN fields in display order.""" return ( - ExternalCoreShutdownTimeoutInput(), GuiEditorItemProxyHttp(title=_('HTTP Proxy')), GuiEditorItemProxySocks(title=_('SOCKS Proxy')), + ExternalCoreShutdownTimeoutInput(), self._applicationTun2socksInput, self._tunRemoteAddressInput, ) def setupPageLayout(self): - """Arrange secondary fields and keep the timeout compact like VLESS port.""" + """Arrange secondary fields with uniform compact control rows.""" layout = QGridLayout() layout.setColumnStretch(1, 1) - timeoutInput = self._containers[0].widgets()[1] + compactInputs = tuple( + container.widgets()[1] for container in self._containers[:3] + ) + + for inputWidget in compactInputs: + sizePolicy = inputWidget.sizePolicy() + sizePolicy.setVerticalPolicy(QSizePolicy.Policy.Minimum) + + inputWidget.setSizePolicy(sizePolicy) + + timeoutInput = compactInputs[2] if isinstance(timeoutInput, QSpinBox): - timeoutInput.setSizePolicy( - QSizePolicy.Policy.Fixed, - timeoutInput.sizePolicy().verticalPolicy(), - ) + sizePolicy = timeoutInput.sizePolicy() + sizePolicy.setHorizontalPolicy(QSizePolicy.Policy.Fixed) + + timeoutInput.setSizePolicy(sizePolicy) for row, container in enumerate(self._containers): - if row in (1, 2, 4): + if row in (0, 1, 4): addEditorGridFullRow(layout, container, row) else: addEditorGridBinding(layout, container, row, 0) + # QSpinBox has a larger style-derived minimum than QLineEdit, so let + # all three related rows adopt the largest legal child minimum. + uniformRowMinimum = max( + inputWidget.minimumSizeHint().height() for inputWidget in compactInputs + ) + + for row in range(len(compactInputs)): + layout.setRowMinimumHeight(row, uniformRowMinimum) + layout.setRowStretch(len(self._containers), 1) return layout diff --git a/tests/test_ui_behavior.py b/tests/test_ui_behavior.py index d112ce5..b508713 100644 --- a/tests/test_ui_behavior.py +++ b/tests/test_ui_behavior.py @@ -34,6 +34,7 @@ from Furious.Backends.ExternalCore.Editor import ( ExternalCoreEditor, ExternalCoreEnvironmentInput, ExternalCoreOtherGroup, + ExternalCoreShutdownTimeoutInput, ExternalCoreTunRemoteAddressInput, ) from Furious.Backends.Hysteria1.Editor import Hysteria1Editor @@ -1171,7 +1172,11 @@ class EditorMappingTest(unittest.TestCase): basicGroup, otherGroup = externalGroups basicLayout = basicGroup._widget.currentWidget().layout() otherLayout = otherGroup._widget.currentWidget().layout() - timeoutInput = otherGroup._containers[0].widgets()[1] + timeoutInput = otherGroup._containers[2].widgets()[1] + + self.assertIsInstance( + otherGroup._containers[2], ExternalCoreShutdownTimeoutInput + ) for row, container in enumerate(basicGroup._containers): self.assertBindingPosition(basicLayout, container, row, 0, 1, 3) @@ -1180,19 +1185,91 @@ class EditorMappingTest(unittest.TestCase): timeoutInput.sizePolicy().horizontalPolicy(), QSizePolicy.Policy.Fixed, ) - self.assertBindingPosition(otherLayout, otherGroup._containers[0], 0, 0, 1) + self.assertBindingPosition(otherLayout, otherGroup._containers[0], 0, 0, 1, 3) self.assertBindingPosition(otherLayout, otherGroup._containers[1], 1, 0, 1, 3) - self.assertBindingPosition(otherLayout, otherGroup._containers[2], 2, 0, 1, 3) + self.assertBindingPosition(otherLayout, otherGroup._containers[2], 2, 0, 1) self.assertBindingPosition(otherLayout, otherGroup._containers[4], 4, 0, 1, 3) self.assertGreater(basicGroup._containers[-1].widgets()[1].maximumWidth(), 520) + self.assertGreater(otherGroup._containers[0].widgets()[1].maximumWidth(), 360) self.assertGreater(otherGroup._containers[1].widgets()[1].maximumWidth(), 360) - self.assertGreater(otherGroup._containers[2].widgets()[1].maximumWidth(), 360) self.assertGreater(otherGroup._containers[4].widgets()[1].maximumWidth(), 420) self.assertEqual(editor.size(), QtCore.QSize(1400, 600)) editor.close() referenceGroup.deleteLater() + def testExternalCoreCompactRowsRemainUniformAndSeparated(self): + """Keep compact controls uniform without collapsing adjacent grid rows.""" + app = application() + originalStyleSheet = app.styleSheet() + + try: + for theme in (AppStyleSheet.Light, AppStyleSheet.Dark): + app.setStyleSheet(AppStyleSheet.forTheme(theme)) + editor = ExternalCoreEditor() + + try: + editor.show() + processQtEvents() + + otherGroup = editor.groupBoxSequence()[1] + layout = otherGroup._widget.currentWidget().layout() + compactInputs = tuple( + otherGroup._containers[index].widgets()[1] for index in range(3) + ) + naturalRowMinimum = max( + inputWidget.minimumSizeHint().height() + for inputWidget in compactInputs + ) + timeoutPolicy = compactInputs[2].sizePolicy() + + self.assertNotEqual( + timeoutPolicy.verticalPolicy(), QSizePolicy.Policy.Ignored + ) + self.assertEqual( + timeoutPolicy.controlType(), QSizePolicy.ControlType.SpinBox + ) + self.assertEqual( + tuple(layout.rowMinimumHeight(row) for row in range(3)), + (naturalRowMinimum,) * 3, + ) + + for size in ( + QtCore.QSize(1200, 520), + QtCore.QSize(1400, 600), + QtCore.QSize(1600, 700), + ): + editor.setFixedSize(size) + processQtEvents() + + self.assertEqual( + tuple( + inputWidget.height() for inputWidget in compactInputs + ), + (compactInputs[0].height(),) * 3, + ) + + rowRectangles = [] + + for container in otherGroup._containers: + label, inputWidget = container.widgets() + rowRectangles.append( + label.geometry().united(inputWidget.geometry()) + ) + + for currentRow, nextRow in zip( + rowRectangles, rowRectangles[1:] + ): + self.assertFalse(currentRow.intersects(nextRow)) + self.assertEqual( + nextRow.top() - currentRow.bottom() - 1, + layout.verticalSpacing(), + ) + finally: + editor.close() + finally: + app.setStyleSheet(originalStyleSheet) + def testExternalCoreRowsRemainTopAlignedAsSectionGrows(self): """Assign surplus editor height below the compact form rows.""" groups = (