mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-10-05 13:28:03 +03:00
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:
@@ -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'])
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user