Refine Hysteria2 editor layout

Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
Loren Eteval
2026-08-22 21:28:19 +08:00
parent 1ae88d09e0
commit d57e920e4d
2 changed files with 154 additions and 6 deletions
+36 -6
View File
@@ -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 = (
+118
View File
@@ -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'})