mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-09-22 23:08:08 +03:00
Refine External Core editor layout
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
@@ -19,7 +19,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import GOLDEN_RATIO
|
||||
from Furious.Interface import EditorWidgetBinding
|
||||
from Furious.Models import CoreConfiguration, ServerProfile
|
||||
from Furious.Qt import (
|
||||
@@ -34,6 +33,8 @@ from Furious.Qt import (
|
||||
GuiEditorItemTextSpinBox,
|
||||
GuiEditorWidgetQDialog,
|
||||
GuiEditorWidgetQGroupBox,
|
||||
addEditorGridBinding,
|
||||
addEditorGridFullRow,
|
||||
)
|
||||
from Furious.Qt import gettext as _
|
||||
from Furious.Qt.Signals import connectWeakly
|
||||
@@ -41,11 +42,11 @@ from Furious.Qt.Signals import connectWeakly
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtWidgets import (
|
||||
QFileDialog,
|
||||
QFormLayout,
|
||||
QGridLayout,
|
||||
QHBoxLayout,
|
||||
QPlainTextEdit,
|
||||
QSizePolicy,
|
||||
QVBoxLayout,
|
||||
QSpinBox,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
@@ -211,7 +212,6 @@ class ExternalCoreEnvironmentInput(EditorWidgetBinding):
|
||||
|
||||
self._title = AppQLabel(_('Environment Variables'), translatable=True)
|
||||
self._input = QPlainTextEdit()
|
||||
self._input.setMaximumWidth(520)
|
||||
self._input.setStyleSheet('min-height: 120px; max-height: 136px;')
|
||||
self._input.setPlaceholderText(_('KEY=VALUE, one per line'))
|
||||
|
||||
@@ -315,15 +315,12 @@ class ExternalCoreApplicationTun2socksInput(GuiEditorItemTextSwitch):
|
||||
class ExternalCoreTunRemoteAddressInput(EditorWidgetBinding):
|
||||
"""Bind the remote destination used only by application-managed TUN."""
|
||||
|
||||
MaximumWidth = 420
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize a hostname-or-IP input independent of process paths."""
|
||||
super().__init__()
|
||||
|
||||
self._title = AppQLabel(_('TUN Remote Address'), translatable=True)
|
||||
self._input = AppQLineEdit()
|
||||
self._input.setMaximumWidth(self.MaximumWidth)
|
||||
self._input.setPlaceholderText(_('Hostname, IPv4, or IPv6 address'))
|
||||
|
||||
def widgets(self):
|
||||
@@ -349,39 +346,23 @@ class ExternalCoreTunRemoteAddressInput(EditorWidgetBinding):
|
||||
|
||||
|
||||
class ExternalCoreConfigurationGroup(GuiEditorWidgetQGroupBox):
|
||||
"""Present all External Core settings as one compact form."""
|
||||
|
||||
RemarkWidth = 420
|
||||
ProxyEndpointWidth = 360
|
||||
"""Present External Core profile and process settings."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
argumentsInput: ExternalCoreArgumentsInput,
|
||||
environmentInput: ExternalCoreEnvironmentInput,
|
||||
applicationTun2socksInput: ExternalCoreApplicationTun2socksInput,
|
||||
tunRemoteAddressInput: ExternalCoreTunRemoteAddressInput,
|
||||
):
|
||||
"""Initialize the form around its shared validated inputs."""
|
||||
"""Initialize the process form around its shared validated inputs."""
|
||||
self._argumentsInput = argumentsInput
|
||||
self._environmentInput = environmentInput
|
||||
self._applicationTun2socksInput = applicationTun2socksInput
|
||||
self._tunRemoteAddressInput = tunRemoteAddressInput
|
||||
|
||||
super().__init__(_('Basic Configuration'))
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return related profile, process, and endpoint fields in order."""
|
||||
remarkInput = GuiEditorItemBasicRemark(title=_('Remark'))
|
||||
remarkInput.widgets()[1].setMaximumWidth(self.RemarkWidth)
|
||||
|
||||
httpProxyInput = GuiEditorItemProxyHttp(title=_('HTTP Proxy'))
|
||||
httpProxyInput.widgets()[1].setMaximumWidth(self.ProxyEndpointWidth)
|
||||
|
||||
socksProxyInput = GuiEditorItemProxySocks(title=_('SOCKS Proxy'))
|
||||
socksProxyInput.widgets()[1].setMaximumWidth(self.ProxyEndpointWidth)
|
||||
|
||||
self._processContainers = (
|
||||
remarkInput,
|
||||
"""Return profile and process fields in display order."""
|
||||
return (
|
||||
GuiEditorItemBasicRemark(title=_('Remark')),
|
||||
ExternalCorePathInput(_('Executable'), 'executable'),
|
||||
ExternalCorePathInput(
|
||||
_('Working Directory'),
|
||||
@@ -392,35 +373,64 @@ class ExternalCoreConfigurationGroup(GuiEditorWidgetQGroupBox):
|
||||
self._argumentsInput,
|
||||
self._environmentInput,
|
||||
)
|
||||
self._runtimeContainers = (
|
||||
|
||||
def setupPageLayout(self):
|
||||
"""Arrange process fields as full rows like the VLESS remark row."""
|
||||
layout = QGridLayout()
|
||||
layout.setColumnStretch(1, 1)
|
||||
|
||||
for row, container in enumerate(self._containers):
|
||||
addEditorGridFullRow(layout, container, row)
|
||||
|
||||
layout.setRowStretch(len(self._containers), 1)
|
||||
|
||||
return layout
|
||||
|
||||
|
||||
class ExternalCoreOtherGroup(GuiEditorWidgetQGroupBox):
|
||||
"""Present the remaining External Core process and network settings."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
applicationTun2socksInput: ExternalCoreApplicationTun2socksInput,
|
||||
tunRemoteAddressInput: ExternalCoreTunRemoteAddressInput,
|
||||
):
|
||||
"""Initialize the secondary form around its shared validated inputs."""
|
||||
self._applicationTun2socksInput = applicationTun2socksInput
|
||||
self._tunRemoteAddressInput = tunRemoteAddressInput
|
||||
|
||||
super().__init__(_('Other'))
|
||||
|
||||
def containerSequence(self):
|
||||
"""Return secondary process, proxy, and TUN fields in display order."""
|
||||
return (
|
||||
ExternalCoreShutdownTimeoutInput(),
|
||||
httpProxyInput,
|
||||
socksProxyInput,
|
||||
GuiEditorItemProxyHttp(title=_('HTTP Proxy')),
|
||||
GuiEditorItemProxySocks(title=_('SOCKS Proxy')),
|
||||
self._applicationTun2socksInput,
|
||||
self._tunRemoteAddressInput,
|
||||
)
|
||||
|
||||
return [*self._processContainers, *self._runtimeContainers]
|
||||
|
||||
@staticmethod
|
||||
def _formLayout(containers):
|
||||
"""Align one related section independently from the other section."""
|
||||
layout = QFormLayout()
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setFormAlignment(QtCore.Qt.AlignmentFlag.AlignLeft)
|
||||
layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
|
||||
|
||||
for container in containers:
|
||||
layout.addRow(*container.widgets())
|
||||
|
||||
return layout
|
||||
|
||||
def setupPageLayout(self):
|
||||
"""Keep process fields independent from runtime-network label widths."""
|
||||
layout = QVBoxLayout()
|
||||
layout.setSpacing(18)
|
||||
layout.addLayout(self._formLayout(self._processContainers))
|
||||
layout.addLayout(self._formLayout(self._runtimeContainers))
|
||||
"""Arrange secondary fields and keep the timeout compact like VLESS port."""
|
||||
layout = QGridLayout()
|
||||
layout.setColumnStretch(1, 1)
|
||||
|
||||
timeoutInput = self._containers[0].widgets()[1]
|
||||
|
||||
if isinstance(timeoutInput, QSpinBox):
|
||||
timeoutInput.setSizePolicy(
|
||||
QSizePolicy.Policy.Fixed,
|
||||
timeoutInput.sizePolicy().verticalPolicy(),
|
||||
)
|
||||
|
||||
for row, container in enumerate(self._containers):
|
||||
if row in (1, 2, 4):
|
||||
addEditorGridFullRow(layout, container, row)
|
||||
else:
|
||||
addEditorGridBinding(layout, container, row, 0)
|
||||
|
||||
layout.setRowStretch(len(self._containers), 1)
|
||||
|
||||
return layout
|
||||
|
||||
@@ -440,24 +450,22 @@ class ExternalCoreEditor(GuiEditorWidgetQDialog):
|
||||
)
|
||||
self._tunRemoteAddressInput.setEnabled(False)
|
||||
|
||||
kwargs.setdefault('style', 'portrait')
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
dialogWidth = 540
|
||||
|
||||
self.setFixedSize(dialogWidth, int(dialogWidth * GOLDEN_RATIO))
|
||||
self.setFixedSize(1400, 600)
|
||||
self.setTabText(_('External Core'))
|
||||
|
||||
def createGroupBoxSequence(self):
|
||||
"""Create the editor's single compact configuration form."""
|
||||
"""Create separate basic and secondary configuration groups."""
|
||||
return [
|
||||
ExternalCoreConfigurationGroup(
|
||||
self._argumentsInput,
|
||||
self._environmentInput,
|
||||
),
|
||||
ExternalCoreOtherGroup(
|
||||
self._applicationTun2socksInput,
|
||||
self._tunRemoteAddressInput,
|
||||
)
|
||||
),
|
||||
]
|
||||
|
||||
def inputToFactory(self, config: CoreConfiguration) -> bool:
|
||||
|
||||
Vendored
+1
@@ -1114,6 +1114,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Other": {
|
||||
"source": [
|
||||
"Furious.Backends.ExternalCore.Editor",
|
||||
"Furious.Backends.Hysteria1.Editor",
|
||||
"Furious.Backends.Hysteria2.Editor"
|
||||
],
|
||||
|
||||
+97
-15
@@ -27,7 +27,15 @@ from Furious.Backends.ExternalCore.Configuration import (
|
||||
BLANK_CONFIG_EXTERNAL_CORE,
|
||||
ConfigExternalCore,
|
||||
)
|
||||
from Furious.Backends.ExternalCore.Editor import ExternalCoreEditor
|
||||
from Furious.Backends.ExternalCore.Editor import (
|
||||
ExternalCoreApplicationTun2socksInput,
|
||||
ExternalCoreArgumentsInput,
|
||||
ExternalCoreConfigurationGroup,
|
||||
ExternalCoreEditor,
|
||||
ExternalCoreEnvironmentInput,
|
||||
ExternalCoreOtherGroup,
|
||||
ExternalCoreTunRemoteAddressInput,
|
||||
)
|
||||
from Furious.Backends.Hysteria1.Editor import Hysteria1Editor
|
||||
from Furious.Backends.Hysteria2.Editor import Hysteria2Editor
|
||||
from Furious.Backends.Xray.AssetListView import XrayAssetListView
|
||||
@@ -104,6 +112,7 @@ from PySide6.QtTest import QTest
|
||||
from PySide6.QtWidgets import (
|
||||
QComboBox,
|
||||
QHBoxLayout,
|
||||
QSizePolicy,
|
||||
QStyle,
|
||||
QStyleOptionComboBox,
|
||||
QVBoxLayout,
|
||||
@@ -1007,8 +1016,7 @@ class EditorMappingTest(unittest.TestCase):
|
||||
self.assertEqual(tunSwitch.size(), AppQSwitch.CompactControlSize)
|
||||
self.assertTrue(editor._tunRemoteAddressInput.widgets()[1].isEnabled())
|
||||
self.assertEqual(editor._tunRemoteAddressInput.text(), '2001:db8::42')
|
||||
self.assertEqual(len(editor.groupBoxSequence()), 1)
|
||||
self.assertAlmostEqual(editor.height() / editor.width(), 1.618, places=2)
|
||||
self.assertEqual(len(editor.groupBoxSequence()), 2)
|
||||
|
||||
editor._argumentsInput._input.setText(
|
||||
'--mode direct --label "a value with spaces"'
|
||||
@@ -1037,28 +1045,102 @@ class EditorMappingTest(unittest.TestCase):
|
||||
editor.close()
|
||||
|
||||
def testExternalCoreSectionUsesCanonicalEditorInset(self):
|
||||
"""Let the section page own the same outer inset as Xray editors."""
|
||||
"""Use canonical editor grids and insets for both External Core groups."""
|
||||
editor = ExternalCoreEditor()
|
||||
externalGroup = editor.groupBoxSequence()[0]
|
||||
externalLayout = externalGroup._widget.currentWidget().layout()
|
||||
externalGroups = editor.groupBoxSequence()
|
||||
referenceGroup = GuiVLESSGroupBoxBasic()
|
||||
referenceLayout = referenceGroup._widget.currentWidget().layout()
|
||||
|
||||
self.assertEqual(len(externalGroups), 2)
|
||||
|
||||
for group in externalGroups:
|
||||
externalLayout = group._widget.currentWidget().layout()
|
||||
|
||||
self.assertEqual(
|
||||
externalLayout.contentsMargins(),
|
||||
referenceLayout.contentsMargins(),
|
||||
)
|
||||
self.assertFalse(externalLayout.contentsMargins().isNull())
|
||||
self.assertIsInstance(externalLayout, type(referenceLayout))
|
||||
self.assertEqual(externalLayout.columnStretch(1), 1)
|
||||
self.assertEqual(
|
||||
externalLayout.verticalSpacing(),
|
||||
referenceLayout.verticalSpacing(),
|
||||
)
|
||||
self.assertEqual(
|
||||
externalLayout.rowStretch(externalLayout.rowCount() - 1),
|
||||
1,
|
||||
)
|
||||
|
||||
basicGroup, otherGroup = externalGroups
|
||||
basicLayout = basicGroup._widget.currentWidget().layout()
|
||||
otherLayout = otherGroup._widget.currentWidget().layout()
|
||||
timeoutInput = otherGroup._containers[0].widgets()[1]
|
||||
|
||||
for row, container in enumerate(basicGroup._containers):
|
||||
self.assertBindingPosition(basicLayout, container, row, 0, 1, 3)
|
||||
|
||||
self.assertEqual(
|
||||
externalLayout.contentsMargins(),
|
||||
referenceLayout.contentsMargins(),
|
||||
timeoutInput.sizePolicy().horizontalPolicy(),
|
||||
QSizePolicy.Policy.Fixed,
|
||||
)
|
||||
self.assertFalse(externalLayout.contentsMargins().isNull())
|
||||
|
||||
for index in range(externalLayout.count()):
|
||||
childLayout = externalLayout.itemAt(index).layout()
|
||||
|
||||
self.assertIsNotNone(childLayout)
|
||||
self.assertTrue(childLayout.contentsMargins().isNull())
|
||||
self.assertBindingPosition(otherLayout, otherGroup._containers[0], 0, 0, 1)
|
||||
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[4], 4, 0, 1, 3)
|
||||
self.assertGreater(basicGroup._containers[-1].widgets()[1].maximumWidth(), 520)
|
||||
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 testExternalCoreRowsRemainTopAlignedAsSectionGrows(self):
|
||||
"""Assign surplus editor height below the compact form rows."""
|
||||
groups = (
|
||||
ExternalCoreConfigurationGroup(
|
||||
ExternalCoreArgumentsInput(),
|
||||
ExternalCoreEnvironmentInput(),
|
||||
),
|
||||
ExternalCoreOtherGroup(
|
||||
ExternalCoreApplicationTun2socksInput(),
|
||||
ExternalCoreTunRemoteAddressInput(),
|
||||
),
|
||||
)
|
||||
|
||||
for group in groups:
|
||||
containers = group._containers
|
||||
|
||||
group.resize(760, 900)
|
||||
group.show()
|
||||
processQtEvents()
|
||||
|
||||
initialPositions = tuple(
|
||||
container.widgets()[0].geometry().top() for container in containers
|
||||
)
|
||||
page = group._widget.currentWidget()
|
||||
initialBlankHeight = (
|
||||
page.height() - containers[-1].widgets()[0].geometry().bottom()
|
||||
)
|
||||
|
||||
group.resize(760, 1200)
|
||||
processQtEvents()
|
||||
|
||||
grownPositions = tuple(
|
||||
container.widgets()[0].geometry().top() for container in containers
|
||||
)
|
||||
grownBlankHeight = (
|
||||
page.height() - containers[-1].widgets()[0].geometry().bottom()
|
||||
)
|
||||
|
||||
self.assertEqual(grownPositions, initialPositions)
|
||||
self.assertGreater(grownBlankHeight, initialBlankHeight)
|
||||
|
||||
group.close()
|
||||
group.deleteLater()
|
||||
|
||||
def testEveryProtocolEditorRetranslatesItsDedicatedWindowTitle(self):
|
||||
"""Retain title source text when switching from Chinese to English."""
|
||||
editorTypes = (
|
||||
|
||||
Reference in New Issue
Block a user