From d57e920e4dfde807198a38ff5be06cd7e44dff34 Mon Sep 17 00:00:00 2001 From: Loren Eteval Date: Sat, 22 Aug 2026 20:04:52 +0800 Subject: [PATCH] Refine Hysteria2 editor layout Signed-off-by: Loren Eteval --- Furious/Backends/Hysteria2/Editor.py | 42 +++++++-- tests/test_hysteria2_compatibility.py | 118 ++++++++++++++++++++++++++ 2 files changed, 154 insertions(+), 6 deletions(-) diff --git a/Furious/Backends/Hysteria2/Editor.py b/Furious/Backends/Hysteria2/Editor.py index fb36320..ffa2e67 100644 --- a/Furious/Backends/Hysteria2/Editor.py +++ b/Furious/Backends/Hysteria2/Editor.py @@ -194,7 +194,10 @@ class GuiHy2InlineBindings(EditorWidgetBinding): def __init__(self, *bindings: EditorWidgetBinding, **kwargs): """Create one persistent row that owns the supplied labeled controls.""" - expandInputs = kwargs.pop('expandInputs', True) + expandInputs, spacing = ( + kwargs.pop('expandInputs', True), + kwargs.pop('spacing', 12), + ) super().__init__(**kwargs) @@ -203,14 +206,11 @@ class GuiHy2InlineBindings(EditorWidgetBinding): layout = QHBoxLayout(self._widget) layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(12) + layout.setSpacing(spacing) - for index, binding in enumerate(self.bindings): + for binding in self.bindings: title, inputWidget = binding.widgets() - if index: - layout.addSpacing(6) - layout.addWidget(title) layout.addWidget(inputWidget, 1 if expandInputs else 0) @@ -978,6 +978,7 @@ class GuiHy2GroupBoxAdvanced(GuiEditorWidgetQGroupBox): self.chromeParrotItem, self.mimicEnabledItem, expandInputs=False, + spacing=8, ) return [ @@ -1064,6 +1065,35 @@ class Hysteria2Editor(GuiEditorWidgetQDialog): self.setTabText(Protocol.Hysteria2.value) + def _fitAdvancedOptionsRow(self): + """Honor the styled editor and options row's natural widths.""" + row = self.advancedGroup.optionsRow._widget + trailingSpace = max(0, row.layout().spacing()) + rowWidthDeficit = max( + 0, + row.sizeHint().width() + trailingSpace - row.contentsRect().width(), + ) + requiredWidth = max( + self.minimumSizeHint().width(), + self.width() + rowWidthDeficit, + ) + + if requiredWidth <= self.width(): + return + + # The shared editor starts from a compact golden-ratio size. Font and + # style metrics can require more room for the complete form or this + # technical row; grow to that measured requirement instead of clipping + # a label or introducing a platform-specific width. + self.setFixedWidth(requiredWidth) + self.tabCentralWidgetLayout.activate() + + def showEvent(self, event): + """Resolve the styled section geometry before the first frame is drawn.""" + super().showEvent(event) + + self._fitAdvancedOptionsRow() + def createGroupBoxSequence(self): """Create the configuration group boxes in display order.""" self.basicGroup, self.advancedGroup = ( diff --git a/tests/test_hysteria2_compatibility.py b/tests/test_hysteria2_compatibility.py index 87c6a1d..0446620 100644 --- a/tests/test_hysteria2_compatibility.py +++ b/tests/test_hysteria2_compatibility.py @@ -30,8 +30,12 @@ from Furious.Backends.Hysteria2.Editor import ( ) from Furious.Backends.Hysteria2.Process import Hysteria2 from Furious.Backends.Hysteria2.Protocols import Hysteria2ProtocolHandler +from Furious.Backends.Hysteria2.TunSettingsDialog import ( + GuiHysteria2TUNSettingsGroupBoxInterface, +) from Furious.Frozenlib import AppSettings, Mixins from Furious.Models.Profile import ServerProfile +from Furious.Qt import AppStyleSheet from tests.support import ( application, @@ -272,6 +276,120 @@ class Hysteria2CompatibilityTest(unittest.TestCase): editor.close() + def testOriginalSectionInsetsContainNaturalAdvancedOptionsRow(self): + """Restore canonical editor insets while containing compact options.""" + app = application() + previousStyleSheet = app.styleSheet() + + try: + for theme in (AppStyleSheet.Light, AppStyleSheet.Dark): + with self.subTest(theme=theme): + app.setStyleSheet(AppStyleSheet.forTheme(theme)) + editor = Hysteria2Editor() + editor.show() + app.processEvents() + + self.assertGreaterEqual( + editor.width(), editor.minimumSizeHint().width() + ) + self.assertGreaterEqual( + editor.height(), editor.minimumSizeHint().height() + ) + + basic, proxyBandwidth, advanced, tls = editor.groupBoxSequence() + referenceGroup = GuiHysteria2TUNSettingsGroupBoxInterface( + parent=editor + ) + referenceSectionMargins = referenceGroup.layout().contentsMargins() + referencePageMargins = ( + referenceGroup._widget.currentWidget() + .layout() + .contentsMargins() + ) + firstWidgets = ( + basic._containers[0]._title, + proxyBandwidth.proxyFields.bindings[0]._title, + advanced.obfsItem.page(advanced.obfsItem.currentIndex()) + ._containers[0] + ._title, + tls._containers[0]._title, + ) + + for group in editor.groupBoxSequence(): + sectionMargins = group.layout().contentsMargins() + pageMargins = ( + group._widget.currentWidget().layout().contentsMargins() + ) + + self.assertEqual(sectionMargins, referenceSectionMargins) + self.assertEqual(pageMargins, referencePageMargins) + self.assertGreater(pageMargins.left(), 0) + self.assertEqual(pageMargins.left(), pageMargins.right()) + self.assertEqual(pageMargins.top(), pageMargins.bottom()) + + firstPositions = tuple( + widget.mapTo(group, widget.rect().topLeft()).x() + for group, widget in zip( + editor.groupBoxSequence(), firstWidgets + ) + ) + self.assertEqual(len(set(firstPositions)), 1) + + row = advanced.optionsRow._widget + rowLayout = row.layout() + widgets = tuple( + widget + for binding in advanced.optionsRow.bindings + for widget in binding.widgets() + ) + + self.assertEqual(rowLayout.contentsMargins().left(), 0) + self.assertEqual(rowLayout.contentsMargins().right(), 0) + self.assertEqual(rowLayout.spacing(), 8) + self.assertIsNotNone( + rowLayout.itemAt(rowLayout.count() - 1).spacerItem() + ) + self.assertLessEqual( + row.sizeHint().width() + rowLayout.spacing(), + row.contentsRect().width(), + ) + + previousRight = row.contentsRect().left() + + for widget in widgets: + topLeft = widget.mapTo(row, widget.rect().topLeft()) + right = topLeft.x() + widget.width() + requiredWidth = min( + widget.minimumSizeHint().width(), + widget.maximumWidth(), + ) + + self.assertTrue(widget.isVisible()) + self.assertGreaterEqual(widget.width(), requiredWidth) + self.assertGreaterEqual(topLeft.x(), previousRight) + self.assertLessEqual(right, row.contentsRect().right() + 1) + + previousRight = right + + self.assertGreaterEqual( + row.contentsRect().right() + 1 - previousRight, + rowLayout.spacing(), + ) + + mimicSwitch = advanced.mimicEnabledItem._input + mimicRight = ( + mimicSwitch.mapTo(advanced, mimicSwitch.rect().topLeft()).x() + + mimicSwitch.width() + ) + + self.assertLessEqual( + mimicRight, advanced.contentsRect().right() + 1 + ) + + editor.close() + finally: + app.setStyleSheet(previousStyleSheet) + def testEditorWritesSemanticEnumsAndPositiveChromeToggle(self): """Persist upstream values rather than translated display labels.""" profile = self.profile({'server': 'example.com:443', 'auth': 'secret'})