Refine proxy editor layouts and option handling

Centralize grid placement helpers and use them to keep Xray and Hysteria editor forms compact and consistent. Preserve unknown combo-box values so loading and saving newer protocol options does not silently discard configuration data.

Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
Loren Eteval
2026-08-22 21:28:09 +08:00
parent 8b6f166ea4
commit b6a6fb0672
11 changed files with 324 additions and 257 deletions
+2
View File
@@ -83,6 +83,8 @@ class GuiHy1ItemBasicProtocol(GuiEditorItemTextComboBox):
def __init__(self, *args, **kwargs):
"""Initialize the GuiHy1ItemBasicProtocol."""
kwargs.setdefault('preserveUnknownValues', True)
super().__init__(*args, **kwargs)
self.addItems(['', 'udp', 'wechat-video', 'faketcp'])
+13 -96
View File
@@ -280,62 +280,6 @@ class GuiHy2FormBindings(EditorWidgetBinding):
binding.factoryToInput(config)
class GuiHy2ColumnBindings(EditorWidgetBinding):
"""Arrange independent compact form fields in equal-width columns."""
def __init__(self, *bindings: EditorWidgetBinding, **kwargs):
"""Create one persistent row of equal-width one-field forms."""
super().__init__(**kwargs)
self.bindings = tuple(bindings)
self._widget = QWidget()
layout = QHBoxLayout(self._widget)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(18)
columns = []
for binding in self.bindings:
column = QWidget(self._widget)
columnLayout = QFormLayout(column)
columnLayout.setContentsMargins(0, 0, 0, 0)
columnLayout.setFormAlignment(QtCore.Qt.AlignmentFlag.AlignLeft)
columnLayout.setFieldGrowthPolicy(
QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow
)
columnLayout.addRow(*binding.widgets())
layout.addWidget(column, 1)
columns.append(column)
minimumColumnWidth = max(
(column.minimumSizeHint().width() for column in columns), default=0
)
for column in columns:
column.setMinimumWidth(minimumColumnWidth)
def widgets(self):
"""Return the composed full-width column row."""
return (self._widget,)
def inputToFactory(self, config: CoreConfiguration) -> bool:
"""Apply every binding in the row to the same configuration."""
modified = False
for binding in self.bindings:
modified |= binding.inputToFactory(config)
return modified
def factoryToInput(self, config: CoreConfiguration):
"""Load every binding in the row from the same configuration."""
for binding in self.bindings:
binding.factoryToInput(config)
class GuiHy2ItemBasicServer(GuiEditorItemTextInput):
"""Represent GUI hy2 item basic server."""
@@ -716,6 +660,12 @@ class GuiHy2PageObfsGecko(GuiHy2PageObfsXXX):
translatable=False,
)
self.packetSizeRow = GuiHy2InlineBindings(
self.minPacketSizeItem,
self.maxPacketSizeItem,
expandInputs=False,
)
return [
GuiHy2ItemObfsType(title='obfs-type', translatable=False),
GuiHy2ItemObfsPassword(
@@ -723,44 +673,9 @@ class GuiHy2PageObfsGecko(GuiHy2PageObfsXXX):
obfsType='gecko',
translatable=False,
),
self.minPacketSizeItem,
self.maxPacketSizeItem,
self.packetSizeRow,
]
def setupLayout(self):
"""Align packet sizes with the full-width obfuscation fields."""
layout = QGridLayout()
layout.setColumnStretch(1, 1)
layout.setColumnStretch(3, 1)
layout.setHorizontalSpacing(18)
def addFullRow(index: int, row: int):
label, inputWidget = self._containers[index].widgets()
layout.addWidget(label, row, 0)
layout.addWidget(inputWidget, row, 1, 1, 3)
def addPair(index: int, column: int):
label, inputWidget = self._containers[index].widgets()
layout.addWidget(label, 2, column)
layout.addWidget(inputWidget, 2, column + 1)
packetSizeLabelWidth = max(
self.minPacketSizeItem._title.sizeHint().width(),
self.maxPacketSizeItem._title.sizeHint().width(),
)
layout.setColumnMinimumWidth(0, packetSizeLabelWidth)
layout.setColumnMinimumWidth(2, packetSizeLabelWidth)
addFullRow(0, 0)
addFullRow(1, 1)
addPair(2, 0)
addPair(3, 2)
self.setLayout(layout)
def handleMinPacketSizeChanged(self, value: int):
"""Handle min packet size changed."""
self.maxPacketSizeItem._input.setMinimum(value)
@@ -808,7 +723,7 @@ class GuiHy2GroupBoxBasic(GuiEditorWidgetQGroupBox):
GuiEditorItemBasicRemark(title=_('Remark')),
GuiHy2ItemBasicServer(title=_('Server')),
GuiHy2ItemBasicAuth(title='auth', translatable=False),
GuiHy2ColumnBindings(
GuiHy2InlineBindings(
GuiHy2ItemBasicCongestionComboBox(
title='congestion-type',
key='type',
@@ -819,6 +734,7 @@ class GuiHy2GroupBoxBasic(GuiEditorWidgetQGroupBox):
key='bbrProfile',
translatable=False,
),
expandInputs=False,
),
]
@@ -1056,15 +972,16 @@ class GuiHy2GroupBoxAdvanced(GuiEditorWidgetQGroupBox):
),
)
self.toggleRow = GuiHy2ColumnBindings(
self.optionsRow = GuiHy2InlineBindings(
self.ipModeItem,
self.chromeParrotItem,
self.mimicEnabledItem,
expandInputs=False,
)
return [
self.obfsItem,
self.ipModeItem,
self.toggleRow,
self.optionsRow,
]
def setupPageLayout(self):
+14 -40
View File
@@ -44,6 +44,8 @@ class GuiVTLSItemSecurity(GuiEditorItemTextComboBox):
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTLSItemSecurity."""
kwargs.setdefault('preserveUnknownValues', True)
super().__init__(*args, **kwargs)
self.addItems(STREAM_SECURITY)
@@ -594,20 +596,6 @@ class GuiVTLSPageTLS(GuiVTLSPageXXX):
basicLayout.setColumnStretch(1, 1)
basicLayout.setColumnStretch(3, 1)
def addPair(index: int, row: int, column: int):
"""Add pair."""
label, inputWidget = self._containers[index].widgets()
basicLayout.addWidget(label, row, column)
basicLayout.addWidget(inputWidget, row, column + 1)
def addBasicFullRow(index: int, row: int):
"""Add basic full row."""
label, inputWidget = self._containers[index].widgets()
basicLayout.addWidget(label, row, 0)
basicLayout.addWidget(inputWidget, row, 1, 1, 3)
def addFullRow(index: int):
"""Add full row."""
widgets = self._containers[index].widgets()
@@ -619,11 +607,11 @@ class GuiVTLSPageTLS(GuiVTLSPageXXX):
layout.addRow(label, inputWidget)
addPair(0, 0, 0)
addPair(2, 0, 2)
addPair(1, 1, 0)
addPair(3, 1, 2)
addBasicFullRow(4, 2)
addEditorGridBinding(basicLayout, self._containers[0], 0, 0)
addEditorGridBinding(basicLayout, self._containers[2], 0, 2)
addEditorGridBinding(basicLayout, self._containers[1], 1, 0)
addEditorGridBinding(basicLayout, self._containers[3], 1, 2)
addEditorGridFullRow(basicLayout, self._containers[4], 2)
layout.addRow(basicLayout)
@@ -673,27 +661,13 @@ class GuiVTLSPageReality(GuiVTLSPageXXX):
layout.setColumnStretch(3, 1)
layout.setHorizontalSpacing(12)
def addPair(index: int, row: int, column: int):
"""Add pair."""
label, inputWidget = self._containers[index].widgets()
layout.addWidget(label, row, column)
layout.addWidget(inputWidget, row, column + 1)
def addFullRow(index: int, row: int):
"""Add full row."""
label, inputWidget = self._containers[index].widgets()
layout.addWidget(label, row, 0)
layout.addWidget(inputWidget, row, 1, 1, 3)
addPair(0, 0, 0)
addPair(2, 0, 2)
addPair(1, 1, 0)
addPair(4, 1, 2)
addFullRow(3, 2)
addFullRow(5, 3)
addFullRow(6, 4)
addEditorGridBinding(layout, self._containers[0], 0, 0)
addEditorGridBinding(layout, self._containers[2], 0, 2)
addEditorGridBinding(layout, self._containers[1], 1, 0)
addEditorGridBinding(layout, self._containers[4], 1, 2)
addEditorGridFullRow(layout, self._containers[3], 2)
addEditorGridFullRow(layout, self._containers[5], 3)
addEditorGridFullRow(layout, self._containers[6], 4)
self.setLayout(layout)
+17
View File
@@ -51,6 +51,8 @@ class GuiVTransportItemNetwork(GuiEditorItemTextComboBox):
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportItemNetwork."""
kwargs.setdefault('preserveUnknownValues', True)
super().__init__(*args, **kwargs)
self.addItems(STREAM_NETWORK)
@@ -1758,6 +1760,21 @@ class GuiVTransportPageXHttp(GuiVTransportPageXXX):
GuiVTransportItemExtraXHttp(title='Extra', translatable=False),
]
def setupLayout(self):
"""Place Host and Path together while preserving the remaining rows."""
layout = QGridLayout()
layout.setColumnStretch(1, 1)
layout.setColumnStretch(3, 2)
addEditorGridBinding(layout, self._containers[0], 0, 0)
addEditorGridFullRow(layout, self._containers[1], 1)
addEditorGridBinding(layout, self._containers[2], 2, 0)
addEditorGridBinding(layout, self._containers[3], 2, 2)
addEditorGridBinding(layout, self._containers[4], 3, 0)
addEditorGridFullRow(layout, self._containers[5], 4)
self.setLayout(layout)
class GuiVTransportPageH2(GuiVTransportPageXXX):
"""Represent GUI v transport page h2."""
+10 -24
View File
@@ -238,6 +238,8 @@ class GuiVLESSItemBasicFlow(GuiEditorItemTextComboBox):
def __init__(self, *args, **kwargs):
"""Initialize the GuiVLESSItemBasicFlow."""
kwargs.setdefault('preserveUnknownValues', True)
super().__init__(*args, **kwargs)
self.addItems(
@@ -301,32 +303,16 @@ class GuiVLESSGroupBoxBasic(GuiEditorWidgetQGroupBox):
widget.sizePolicy().verticalPolicy(),
)
def addPair(index: int, row: int, column: int):
"""Add pair."""
label, inputWidget = self._containers[index].widgets()
keepSpinBoxCompact(self._containers[2].widgets()[1])
keepSpinBoxCompact(inputWidget)
addEditorGridFullRow(layout, self._containers[0], 0)
addEditorGridBinding(layout, self._containers[1], 1, 0)
addEditorGridBinding(layout, self._containers[2], 1, 2)
addEditorGridFullRow(layout, self._containers[3], 2)
addEditorGridBinding(layout, self._containers[4], 3, 0)
addEditorGridBinding(layout, self._containers[5], 3, 2)
layout.addWidget(label, row, column)
layout.addWidget(inputWidget, row, column + 1)
def addFullRow(index: int, row: int):
"""Add full row."""
label, inputWidget = self._containers[index].widgets()
keepSpinBoxCompact(inputWidget)
layout.addWidget(label, row, 0)
layout.addWidget(inputWidget, row, 1, 1, 3)
addFullRow(0, 0)
addFullRow(1, 1)
addFullRow(2, 2)
addFullRow(3, 3)
addPair(4, 4, 0)
addPair(5, 4, 2)
layout.setRowStretch(5, 1)
layout.setRowStretch(4, 1)
return layout
+14 -26
View File
@@ -237,6 +237,8 @@ class GuiVMessItemBasicSecurity(GuiEditorItemTextComboBox):
def __init__(self, *args, **kwargs):
"""Initialize the GuiVMessItemBasicSecurity."""
kwargs.setdefault('preserveUnknownValues', True)
super().__init__(*args, **kwargs)
self.addItems(
@@ -291,7 +293,8 @@ class GuiVMessGroupBoxBasic(GuiEditorWidgetQGroupBox):
def setupPageLayout(self):
"""Set up page layout."""
layout = QGridLayout()
layout.setColumnStretch(4, 1)
layout.setColumnStretch(1, 1)
layout.setColumnStretch(3, 1)
def keepSpinBoxCompact(widget: QWidget):
"""Handle keep spin box compact for the GUI v mess group box basic."""
@@ -309,33 +312,18 @@ class GuiVMessGroupBoxBasic(GuiEditorWidgetQGroupBox):
widget.sizePolicy().verticalPolicy(),
)
def addPair(index: int, row: int, column: int):
"""Add pair."""
label, inputWidget = self._containers[index].widgets()
keepSpinBoxCompact(inputWidget)
layout.addWidget(label, row, column)
layout.addWidget(inputWidget, row, column + 1)
def addFullRow(index: int, row: int):
"""Add full row."""
label, inputWidget = self._containers[index].widgets()
keepSpinBoxCompact(inputWidget)
layout.addWidget(label, row, 0)
layout.addWidget(inputWidget, row, 1, 1, 4)
addFullRow(0, 0)
addFullRow(1, 1)
addPair(2, 2, 0)
addPair(4, 2, 2)
addFullRow(3, 3)
keepSpinBoxCompact(self._containers[2].widgets()[1])
keepSpinBoxCompact(self._containers[4].widgets()[1])
keepComboBoxCompact(self._containers[5].widgets()[1])
addFullRow(5, 4)
layout.setRowStretch(5, 1)
addEditorGridFullRow(layout, self._containers[0], 0)
addEditorGridBinding(layout, self._containers[1], 1, 0)
addEditorGridBinding(layout, self._containers[2], 1, 2)
addEditorGridFullRow(layout, self._containers[3], 2)
addEditorGridBinding(layout, self._containers[5], 3, 0)
addEditorGridBinding(layout, self._containers[4], 3, 2)
layout.setRowStretch(4, 1)
return layout
+45 -1
View File
@@ -31,6 +31,8 @@ from PySide6.QtWidgets import *
from typing import Callable, Sequence
__all__ = [
'addEditorGridBinding',
'addEditorGridFullRow',
'GuiEditorItemTextInput',
'GuiEditorItemTextSpinBox',
'GuiEditorItemTextComboBox',
@@ -44,6 +46,31 @@ __all__ = [
]
def addEditorGridBinding(
layout: QGridLayout,
binding: EditorWidgetBinding,
row: int,
column: int,
):
"""Add one labeled editor binding to a two-field grid row."""
label, inputWidget = binding.widgets()
layout.addWidget(label, row, column)
layout.addWidget(inputWidget, row, column + 1)
def addEditorGridFullRow(
layout: QGridLayout,
binding: EditorWidgetBinding,
row: int,
):
"""Add one labeled editor binding across both fields of a grid row."""
label, inputWidget = binding.widgets()
layout.addWidget(label, row, 0)
layout.addWidget(inputWidget, row, 1, 1, 3)
class GuiEditorItemTextInput(EditorWidgetBinding):
"""Represent GUI editor item text input."""
@@ -113,6 +140,8 @@ class GuiEditorItemTextComboBox(EditorWidgetBinding):
def __init__(self, *args, **kwargs):
"""Initialize the GuiEditorItemTextComboBox."""
self.preserveUnknownValues = kwargs.pop('preserveUnknownValues', False)
title = kwargs.pop('title', '')
translatable = kwargs.pop('translatable', True)
parent = kwargs.pop('parent', None)
@@ -127,11 +156,26 @@ class GuiEditorItemTextComboBox(EditorWidgetBinding):
def text(self) -> str:
"""Return the text value."""
if self.preserveUnknownValues and self._input.currentIndex() < 0:
return self._input.placeholderText()
return self._input.currentText()
def setText(self, text: str):
"""Set text."""
self._input.setCurrentText(text)
if not self.preserveUnknownValues:
self._input.setCurrentText(text)
return
index = self._input.findText(text)
if index >= 0:
self._input.setPlaceholderText('')
self._input.setCurrentIndex(index)
else:
self._input.setPlaceholderText(text)
self._input.setCurrentIndex(-1)
def addItems(self, texts: Sequence[str]):
"""Add items."""
+4
View File
@@ -28,6 +28,8 @@ from .DynamicTranslate import (
gettext,
)
from .EditorWidgets import (
addEditorGridBinding,
addEditorGridFullRow,
GuiEditorItemBasicRemark,
GuiEditorItemProxyHttp,
GuiEditorItemProxySocks,
@@ -99,6 +101,8 @@ from .TextEditorTheme import (
from .HttpGetManager import HttpGetManager
__all__ = [
'addEditorGridBinding',
'addEditorGridFullRow',
'ABBR_TO_LANGUAGE',
'AppHue',
'AppQAction',
+18 -64
View File
@@ -216,8 +216,8 @@ class Hysteria2CompatibilityTest(unittest.TestCase):
('bandwidth.up', 'bandwidth.down'),
)
self.assertEqual(
tuple(binding._title.text() for binding in advanced.toggleRow.bindings),
('quic.disableChromeParrot', 'mimic.enabled'),
tuple(binding._title.text() for binding in advanced.optionsRow.bindings),
('ipMode', 'quic.disableChromeParrot', 'mimic.enabled'),
)
self.assertEqual(
proxyBandwidth.bandwidthFields._widget.layout().contentsMargins().top(),
@@ -248,73 +248,27 @@ class Hysteria2CompatibilityTest(unittest.TestCase):
geckoPage = advanced.obfsItem.page(HY2_OBFS_TYPES.index('gecko'))
advanced.obfsItem.setCurrentIndex(HY2_OBFS_TYPES.index('gecko'))
application().processEvents()
obfsInputPositions = tuple(
binding._input.mapTo(advanced, binding._input.rect().topLeft()).x()
for binding in geckoPage._containers[:2]
)
packetSizePositions = tuple(
binding._input.mapTo(advanced, binding._input.rect().topLeft())
for binding in (
self.assertEqual(
geckoPage.packetSizeRow.bindings,
(
geckoPage.minPacketSizeItem,
geckoPage.maxPacketSizeItem,
)
)
alignedLabelPositions = tuple(
label.mapTo(advanced, label.rect().topLeft()).x()
for label in (
geckoPage._containers[0]._title,
advanced.ipModeItem._title,
advanced.chromeParrotItem._title,
)
)
togglePositions = tuple(
binding._input.mapTo(advanced, binding._input.rect().topLeft())
for binding in advanced.toggleRow.bindings
),
)
self.assertIs(geckoPage._containers[-1], geckoPage.packetSizeRow)
self.assertIs(advanced._containers[-1], advanced.optionsRow)
self.assertEqual(len(advanced._containers), 2)
self.assertEqual(len(set(obfsInputPositions)), 1)
self.assertEqual(packetSizePositions[0].y(), packetSizePositions[1].y())
self.assertEqual(obfsInputPositions[1], packetSizePositions[0].x())
packetPairWidths = tuple(
binding._input.mapTo(
geckoPage,
binding._input.rect().topRight(),
).x()
- binding._title.mapTo(
geckoPage,
binding._title.rect().topLeft(),
).x()
+ 1
for binding in (
geckoPage.minPacketSizeItem,
geckoPage.maxPacketSizeItem,
for row in (
basic._containers[-1],
geckoPage.packetSizeRow,
advanced.optionsRow,
):
rowLayout = row._widget.layout()
self.assertTrue(
all(rowLayout.stretch(index) == 0 for index in range(rowLayout.count()))
)
)
self.assertLessEqual(abs(packetPairWidths[0] - packetPairWidths[1]), 1)
congestionColumns = tuple(
basic._containers[-1]._widget.layout().itemAt(index).widget().width()
for index in range(2)
)
self.assertLessEqual(abs(congestionColumns[0] - congestionColumns[1]), 1)
self.assertEqual(len(set(alignedLabelPositions)), 1)
ipModeInputPosition = advanced.ipModeItem._input.mapTo(
advanced,
advanced.ipModeItem._input.rect().topLeft(),
).x()
self.assertNotEqual(obfsInputPositions[0], ipModeInputPosition)
self.assertEqual(togglePositions[0].y(), togglePositions[1].y())
packetSizeBottom = geckoPage.maxPacketSizeItem._input.mapTo(
advanced,
geckoPage.maxPacketSizeItem._input.rect().bottomLeft(),
).y()
ipModeTop = advanced.ipModeItem._input.mapTo(
advanced,
advanced.ipModeItem._input.rect().topLeft(),
).y()
self.assertLessEqual(
ipModeTop - packetSizeBottom,
advanced.fontMetrics().height(),
)
editor.close()
+3 -3
View File
@@ -291,12 +291,12 @@ class QtLifetimeTest(unittest.TestCase):
factories = (
('external-core', ExternalCoreEditor, 35),
('socks-protocol', SocksEditor, 35),
('vless-protocol', VlessEditor, 12),
('vmess-protocol', VmessEditor, 12),
('vless-protocol', VlessEditor, 25),
('vmess-protocol', VmessEditor, 25),
('trojan-protocol', TrojanEditor, 12),
('shadowsocks-protocol', ShadowsocksEditor, 12),
('hysteria1-protocol', Hysteria1Editor, 12),
('hysteria2-protocol', Hysteria2Editor, 12),
('hysteria2-protocol', Hysteria2Editor, 25),
('xray-tun-settings', XrayTunSettingsDialog, 25),
('hysteria2-tun-settings', Hysteria2TunSettingsDialog, 25),
(
+184 -3
View File
@@ -19,6 +19,10 @@
from __future__ import annotations
from Furious.Backends.Configuration import (
ConfigXray,
configXrayEmptyProxyOutboundObject,
)
from Furious.Backends.ExternalCore.Configuration import (
BLANK_CONFIG_EXTERNAL_CORE,
ConfigExternalCore,
@@ -31,8 +35,19 @@ from Furious.Backends.Xray.RoutingWindow import RoutingRulesDialog
from Furious.Backends.Xray.ShadowsocksEditor import ShadowsocksEditor
from Furious.Backends.Xray.SocksEditor import SocksEditor
from Furious.Backends.Xray.TrojanEditor import TrojanEditor
from Furious.Backends.Xray.VlessEditor import VlessEditor
from Furious.Backends.Xray.VmessEditor import VmessEditor
from Furious.Backends.Xray.TransportEditor import (
GuiVTransportPageXHttp,
GuiVTransportQGroupBox,
STREAM_NETWORK,
)
from Furious.Backends.Xray.VlessEditor import (
GuiVLESSGroupBoxBasic,
VlessEditor,
)
from Furious.Backends.Xray.VmessEditor import (
GuiVMessGroupBoxBasic,
VmessEditor,
)
from Furious.Actions.Connection import ConnectAction
from Furious.Controllers.ConnectionController import (
ConnectionError,
@@ -44,9 +59,11 @@ from Furious.Controllers.SettingsController import (
LOG_AUTO_SCROLL_DOWN_SETTING,
)
from Furious.Frozenlib import APPLICATION_NAME, AppSettings, Mixins
from Furious.Models import ProfileMetadata, ServerProfile
from Furious.Models import ProfileMetadata, Protocol, ServerProfile
from Furious.Plugins.API import RoutingOption
from Furious.Qt import (
AppHue,
AppQComboBox,
AppQMessageBox,
AppQSwitch,
AppStyleSheet,
@@ -61,9 +78,11 @@ from Furious.Service import (
from Furious.Window.LogPage import LogPage
from Furious.Window.SubscriptionPage import _SubscriptionEditorDialog
from Furious.Widget.ConnectionButton import ConnectionButton
from Furious.Widget.RoutingSelector import RoutingSelector
from PySide6 import QtCore
from PySide6.QtTest import QTest
from PySide6.QtWidgets import QHBoxLayout, QWidget
from tests.support import (
application,
@@ -94,6 +113,168 @@ class EditorMappingTest(unittest.TestCase):
"""Finish every deferred transient deletion between tests."""
collectAtBoundary()
@staticmethod
def xrayProfile(protocol: Protocol, user: dict) -> ServerProfile:
"""Return one minimal Xray profile for a basic-editor round trip."""
outbound = configXrayEmptyProxyOutboundObject(protocol)
server = outbound['settings']['vnext'][0]
server['address'] = 'server.example.com'
server['port'] = 443
server['users'][0].update(user)
return ServerProfile.fromConfiguration(
ConfigXray({'outbounds': [outbound]}),
ProfileMetadata(displayName='Fixture profile'),
)
def assertBindingPosition(
self,
layout,
binding,
row: int,
labelColumn: int,
inputColumn: int,
inputColumnSpan: int = 1,
):
"""Assert one binding's logical grid placement without pixel geometry."""
label, inputWidget = binding.widgets()
self.assertEqual(
layout.getItemPosition(layout.indexOf(label)),
(row, labelColumn, 1, 1),
)
self.assertEqual(
layout.getItemPosition(layout.indexOf(inputWidget)),
(row, inputColumn, 1, inputColumnSpan),
)
def testVlessBasicUsesTlsGridRowsWithoutChangingMapping(self):
"""Pair address/port and encryption/flow around full-width fields."""
profile = self.xrayProfile(
Protocol.VLESS,
{
'id': 'fixture-vless-id',
'encryption': 'none',
'flow': 'future-vless-flow',
},
)
original = copy.deepcopy(profile.connection)
group = GuiVLESSGroupBoxBasic()
layout = group._widget.currentWidget().layout()
group.factoryToInput(profile)
self.assertFalse(group.inputToFactory(profile))
self.assertEqual(profile.connection, original)
self.assertBindingPosition(layout, group._containers[0], 0, 0, 1, 3)
self.assertBindingPosition(layout, group._containers[1], 1, 0, 1)
self.assertBindingPosition(layout, group._containers[2], 1, 2, 3)
self.assertBindingPosition(layout, group._containers[3], 2, 0, 1, 3)
self.assertBindingPosition(layout, group._containers[4], 3, 0, 1)
self.assertBindingPosition(layout, group._containers[5], 3, 2, 3)
self.assertEqual(group._containers[4].text(), 'none')
self.assertEqual(group._containers[5].text(), 'future-vless-flow')
group.deleteLater()
def testVmessBasicUsesTlsGridRowsWithoutChangingMapping(self):
"""Pair address/port and security/alterId around the UUID row."""
profile = self.xrayProfile(
Protocol.VMess,
{
'id': 'fixture-vmess-id',
'security': 'future-vmess-security',
'alterId': 17,
},
)
original = copy.deepcopy(profile.connection)
group = GuiVMessGroupBoxBasic()
layout = group._widget.currentWidget().layout()
group.factoryToInput(profile)
self.assertFalse(group.inputToFactory(profile))
self.assertEqual(profile.connection, original)
self.assertBindingPosition(layout, group._containers[0], 0, 0, 1, 3)
self.assertBindingPosition(layout, group._containers[1], 1, 0, 1)
self.assertBindingPosition(layout, group._containers[2], 1, 2, 3)
self.assertBindingPosition(layout, group._containers[3], 2, 0, 1, 3)
self.assertBindingPosition(layout, group._containers[5], 3, 0, 1)
self.assertBindingPosition(layout, group._containers[4], 3, 2, 3)
self.assertEqual(group._containers[5].text(), 'future-vmess-security')
self.assertEqual(group._containers[4].value(), 17)
group.deleteLater()
def testXHttpHostAndPathShareOneRowWithoutChangingMapping(self):
"""Pair xhttp Host/Path while preserving switching and configuration."""
profile = self.xrayProfile(
Protocol.VLESS,
{
'id': 'fixture-xhttp-id',
'encryption': 'none',
'flow': '',
},
)
streamSettings = ConfigXray.getProxyOutboundStream(profile.connection)
streamSettings.update(
{
'network': 'xhttp',
'xhttpSettings': {
'host': 'cdn.example.com',
'path': '/a/representative/xhttp/path',
'mode': 'auto',
'extra': {'noGRPCHeader': True},
},
}
)
original = copy.deepcopy(profile.connection)
group = GuiVTransportQGroupBox()
group.factoryToInput(profile.connection)
xhttpIndex = STREAM_NETWORK.index('xhttp')
wsIndex = STREAM_NETWORK.index('ws')
page = group.page(xhttpIndex)
self.assertIsInstance(page, GuiVTransportPageXHttp)
layout = page.layout()
host = page._containers[2]
path = page._containers[3]
hostInput = host.widgets()[1]
pathInput = path.widgets()[1]
self.assertBindingPosition(layout, page._containers[0], 0, 0, 1)
self.assertBindingPosition(layout, page._containers[1], 1, 0, 1, 3)
self.assertBindingPosition(layout, page._containers[4], 3, 0, 1)
self.assertBindingPosition(layout, page._containers[5], 4, 0, 1, 3)
self.assertBindingPosition(layout, host, 2, 0, 1)
self.assertBindingPosition(layout, path, 2, 2, 3)
self.assertEqual(host.text(), 'cdn.example.com')
self.assertEqual(path.text(), '/a/representative/xhttp/path')
self.assertEqual(profile.connection, original)
group.handleActivated(wsIndex)
group.handleActivated(xhttpIndex)
self.assertIs(group.page(xhttpIndex), page)
self.assertIs(host.widgets()[1], hostInput)
self.assertIs(path.widgets()[1], pathInput)
self.assertFalse(group.inputToFactory(profile.connection))
self.assertEqual(profile.connection, original)
host.setText('edge.example.net')
path.setText('/updated/path')
self.assertTrue(group.inputToFactory(profile.connection))
self.assertEqual(
streamSettings['xhttpSettings']['host'],
'edge.example.net',
)
self.assertEqual(streamSettings['xhttpSettings']['path'], '/updated/path')
group.deleteLater()
def testExternalCoreEditorRoundTripsStructuredFields(self):
"""Keep arguments, environment, process paths, and TUN data distinct."""
configuration = ConfigExternalCore(copy.deepcopy(BLANK_CONFIG_EXTERNAL_CORE))