From c9f0c3ed3c53cdab0206dbd03492e65477d9ac03 Mon Sep 17 00:00:00 2001 From: Loren Eteval Date: Mon, 17 Aug 2026 21:13:56 +0800 Subject: [PATCH] Replace editor checkboxes with Fluent switches Signed-off-by: Loren Eteval --- Furious/Backends/ExternalCore/Editor.py | 4 +-- Furious/Backends/Hysteria1/Editor.py | 2 +- Furious/Backends/Hysteria2/Editor.py | 2 +- Furious/Backends/Xray/TlsEditor.py | 2 +- Furious/Qt/EditorWidgets.py | 16 +++++++---- Furious/Qt/QtWidgets.py | 18 ++---------- Furious/Qt/__init__.py | 6 ++-- Furious/Window/SubscriptionPage.py | 38 +++++++++++++++---------- Furious/Window/TunSettingsDialog.py | 10 +++---- tests/test_ui_behavior.py | 14 ++++++++- 10 files changed, 61 insertions(+), 51 deletions(-) diff --git a/Furious/Backends/ExternalCore/Editor.py b/Furious/Backends/ExternalCore/Editor.py index 20cf526..37001c1 100644 --- a/Furious/Backends/ExternalCore/Editor.py +++ b/Furious/Backends/ExternalCore/Editor.py @@ -29,7 +29,7 @@ from Furious.Qt import ( GuiEditorItemBasicRemark, GuiEditorItemProxyHttp, GuiEditorItemProxySocks, - GuiEditorItemTextCheckBox, + GuiEditorItemTextSwitch, GuiEditorItemTextSpinBox, GuiEditorWidgetQDialog, GuiEditorWidgetQGroupBox, @@ -290,7 +290,7 @@ class ExternalCoreShutdownTimeoutInput(GuiEditorItemTextSpinBox): self.setValue(value if isinstance(value, int) else 5) -class ExternalCoreApplicationTun2socksInput(GuiEditorItemTextCheckBox): +class ExternalCoreApplicationTun2socksInput(GuiEditorItemTextSwitch): """Bind this profile's host-managed tun2socks participation flag.""" def __init__(self): diff --git a/Furious/Backends/Hysteria1/Editor.py b/Furious/Backends/Hysteria1/Editor.py index df847fa..6c536f8 100644 --- a/Furious/Backends/Hysteria1/Editor.py +++ b/Furious/Backends/Hysteria1/Editor.py @@ -188,7 +188,7 @@ class GuiHy1ItemSpeedDownMbps(GuiEditorItemTextSpinBox): self.setValue(96) -class GuiHy1ItemTLSInsecure(GuiEditorItemTextCheckBox): +class GuiHy1ItemTLSInsecure(GuiEditorItemTextSwitch): """Represent GUI hy1 item TLS insecure.""" def __init__(self, *args, **kwargs): diff --git a/Furious/Backends/Hysteria2/Editor.py b/Furious/Backends/Hysteria2/Editor.py index 4b5b09b..2d7d4ba 100644 --- a/Furious/Backends/Hysteria2/Editor.py +++ b/Furious/Backends/Hysteria2/Editor.py @@ -607,7 +607,7 @@ class GuiHy2ItemTLSTextInput(GuiEditorItemTextInput): self.setText(value) -class GuiHy2ItemTLSInsecure(GuiEditorItemTextCheckBox): +class GuiHy2ItemTLSInsecure(GuiEditorItemTextSwitch): """Represent GUI hy2 item TLS insecure.""" def __init__(self, *args, **kwargs): diff --git a/Furious/Backends/Xray/TlsEditor.py b/Furious/Backends/Xray/TlsEditor.py index ab48302..267256c 100644 --- a/Furious/Backends/Xray/TlsEditor.py +++ b/Furious/Backends/Xray/TlsEditor.py @@ -374,7 +374,7 @@ class GuiVTLSItemTLSXXXTextInput(GuiEditorItemTextInput): self.setText('') -class GuiVTLSItemTLSAllowInsecure(GuiEditorItemTextCheckBox): +class GuiVTLSItemTLSAllowInsecure(GuiEditorItemTextSwitch): """Represent GUI vtls item TLS allow insecure.""" def __init__(self, *args, **kwargs): diff --git a/Furious/Qt/EditorWidgets.py b/Furious/Qt/EditorWidgets.py index 3fedbff..e2f99f0 100644 --- a/Furious/Qt/EditorWidgets.py +++ b/Furious/Qt/EditorWidgets.py @@ -34,7 +34,7 @@ __all__ = [ 'GuiEditorItemTextInput', 'GuiEditorItemTextSpinBox', 'GuiEditorItemTextComboBox', - 'GuiEditorItemTextCheckBox', + 'GuiEditorItemTextSwitch', 'GuiEditorItemBasicRemark', 'GuiEditorItemProxyHttp', 'GuiEditorItemProxySocks', @@ -146,18 +146,22 @@ class GuiEditorItemTextComboBox(EditorWidgetBinding): return self._title, self._input -class GuiEditorItemTextCheckBox(EditorWidgetBinding): - """Represent GUI editor item text check box.""" +class GuiEditorItemTextSwitch(EditorWidgetBinding): + """Represent one labeled, compact Fluent switch in an editor form.""" def __init__(self, *args, **kwargs): - """Initialize the GuiEditorItemTextCheckBox.""" + """Initialize the editor label and its form-sized switch.""" title = kwargs.pop('title', '') translatable = kwargs.pop('translatable', True) parent = kwargs.pop('parent', None) super().__init__(*args, **kwargs) - self._input = AppQCheckBox(_(title), translatable=translatable, parent=parent) + if translatable: + title = _(title) + + self._title = AppQLabel(title, translatable=translatable, parent=parent) + self._input = AppQSwitch(parent=parent, compact=True) def isChecked(self) -> bool: """Return whether checked.""" @@ -169,7 +173,7 @@ class GuiEditorItemTextCheckBox(EditorWidgetBinding): def widgets(self): """Return the widgets owned by this editor item.""" - return (self._input,) + return self._title, self._input class GuiEditorItemBasicRemark(GuiEditorItemTextInput): diff --git a/Furious/Qt/QtWidgets.py b/Furious/Qt/QtWidgets.py index 72cefe8..f5e86c0 100644 --- a/Furious/Qt/QtWidgets.py +++ b/Furious/Qt/QtWidgets.py @@ -36,7 +36,6 @@ import functools __all__ = [ 'moveToCenter', - 'AppQCheckBox', 'AppQSwitch', 'AppQComboBox', 'AppQComboBoxSeparatorDelegate', @@ -88,31 +87,20 @@ def moveToCenter(widget, parent=None): widget.move(geometry.topLeft()) -class AppQCheckBox(Mixins.QTranslatable, QCheckBox): - """Represent app q check box.""" - - def __init__(self, *args, **kwargs): - """Initialize the AppQCheckBox.""" - super().__init__(*args, **kwargs) - - def retranslate(self): - """Refresh translated text for the app q check box.""" - self.setText(_(self.text())) - - class AppQSwitch(Mixins.ThemeAware, QCheckBox): """Paint one compact, animated Fluent-style binary switch.""" ControlSize = QtCore.QSize(38, 22) + CompactControlSize = QtCore.QSize(34, 20) AnimationDuration = 160 - def __init__(self, parent=None): + def __init__(self, parent=None, *, compact=False): """Initialize the reusable switch and its owned thumb animation.""" super().__init__(parent) self.setObjectName('SettingsToggle') self.setCursor(QtCore.Qt.CursorShape.PointingHandCursor) - self.setFixedSize(self.ControlSize) + self.setFixedSize(self.CompactControlSize if compact else self.ControlSize) self._thumbPosition = 0.0 diff --git a/Furious/Qt/__init__.py b/Furious/Qt/__init__.py index fcc86c4..0db5253 100644 --- a/Furious/Qt/__init__.py +++ b/Furious/Qt/__init__.py @@ -31,7 +31,7 @@ from .EditorWidgets import ( GuiEditorItemBasicRemark, GuiEditorItemProxyHttp, GuiEditorItemProxySocks, - GuiEditorItemTextCheckBox, + GuiEditorItemTextSwitch, GuiEditorItemTextComboBox, GuiEditorItemTextInput, GuiEditorItemTextSpinBox, @@ -51,7 +51,6 @@ from .QtGui import ( ) from .QtNetwork import AppQNetworkAccessManager from .QtWidgets import ( - AppQCheckBox, AppQComboBox, AppQComboBoxSeparatorDelegate, AppQDialog, @@ -104,7 +103,6 @@ __all__ = [ 'AppHue', 'AppQAction', 'AppQActionGroup', - 'AppQCheckBox', 'AppQComboBox', 'AppQComboBoxSeparatorDelegate', 'AppQDialog', @@ -142,7 +140,7 @@ __all__ = [ 'GuiEditorItemBasicRemark', 'GuiEditorItemProxyHttp', 'GuiEditorItemProxySocks', - 'GuiEditorItemTextCheckBox', + 'GuiEditorItemTextSwitch', 'GuiEditorItemTextComboBox', 'GuiEditorItemTextInput', 'GuiEditorItemTextSpinBox', diff --git a/Furious/Window/SubscriptionPage.py b/Furious/Window/SubscriptionPage.py index e16ea21..b551146 100644 --- a/Furious/Window/SubscriptionPage.py +++ b/Furious/Window/SubscriptionPage.py @@ -65,8 +65,8 @@ class _SubscriptionEditorDialog(AppQTransientDialog): self.remarkEdit = QLineEdit(subscription.get('remark', '')) self.urlEdit = QLineEdit(subscription.get('webURL', '')) - self.enabledCheckBox = QCheckBox() - self.enabledCheckBox.setChecked(subscription.get('enabled', True)) + self.enabledSwitch = AppQSwitch() + self.enabledSwitch.syncChecked(subscription.get('enabled', True)) self.autoUpdateComboBox = AppQComboBox() self.proxyComboBox = AppQComboBox() self.userAgentEdit = QLineEdit(subscription.get('userAgent', '')) @@ -113,26 +113,34 @@ class _SubscriptionEditorDialog(AppQTransientDialog): form.setColumnStretch(1, 1) form.setColumnStretch(3, 1) - self.remarkLabel = AppQLabel(_('Remark')) - self.urlLabel = AppQLabel(_('URL')) - self.enabledLabel = AppQLabel(_('Enabled')) - self.autoUpdateLabel = AppQLabel(_('Auto Update')) - self.proxyLabel = AppQLabel(_('Auto Update Use Proxy')) - self.userAgentLabel = AppQLabel(_('User Agent')) - self.filterLabel = AppQLabel(_('Profile Filter (Regex)')) + ( + self.remarkLabel, + self.urlLabel, + self.enabledLabel, + self.autoUpdateLabel, + self.proxyLabel, + self.userAgentLabel, + self.filterLabel, + ) = ( + AppQLabel(_('Remark')), + AppQLabel(_('URL')), + AppQLabel(_('Enabled')), + AppQLabel(_('Auto Update')), + AppQLabel(_('Auto Update Use Proxy')), + AppQLabel(_('User Agent')), + AppQLabel(_('Profile Filter (Regex)')), + ) - enabledControl = QWidget() - - enabledLayout = QHBoxLayout(enabledControl) + enabledLayout = QHBoxLayout() enabledLayout.setContentsMargins(0, 0, 0, 0) enabledLayout.setSpacing(10) enabledLayout.addWidget(self.enabledLabel) - enabledLayout.addWidget(self.enabledCheckBox) + enabledLayout.addWidget(self.enabledSwitch) enabledLayout.addStretch(1) form.addWidget(self.remarkLabel, 0, 0) form.addWidget(self.remarkEdit, 0, 1) - form.addWidget(enabledControl, 0, 2, 1, 2) + form.addLayout(enabledLayout, 0, 2, 1, 2) form.addWidget(self.urlLabel, 1, 0) form.addWidget(self.urlEdit, 1, 1, 1, 3) form.addWidget(self.autoUpdateLabel, 2, 0) @@ -194,7 +202,7 @@ class _SubscriptionEditorDialog(AppQTransientDialog): return { 'remark': self.remarkEdit.text().strip(), 'webURL': self.urlEdit.text().strip(), - 'enabled': self.enabledCheckBox.isChecked(), + 'enabled': self.enabledSwitch.isChecked(), 'autoupdate': self.autoUpdateComboBox.currentData() or '', 'proxy': self.proxyComboBox.currentData() or '', 'userAgent': self.userAgentEdit.text().strip(), diff --git a/Furious/Window/TunSettingsDialog.py b/Furious/Window/TunSettingsDialog.py index b062de1..3a5acec 100644 --- a/Furious/Window/TunSettingsDialog.py +++ b/Furious/Window/TunSettingsDialog.py @@ -106,11 +106,11 @@ class GuiTUNSettingsItemSpinBoxBufferSizeXXX(GuiEditorItemTextSpinBox): self.setValue(oldValue) -class GuiTUNSettingsItemCheckBoxXXX(GuiEditorItemTextCheckBox): - """Represent GUI TUN settings item check box xxx.""" +class GuiTUNSettingsItemSwitchXXX(GuiEditorItemTextSwitch): + """Represent GUI TUN settings item switch xxx.""" def __init__(self, *args, **kwargs): - """Initialize the GuiTUNSettingsItemCheckBoxXXX.""" + """Initialize the GuiTUNSettingsItemSwitchXXX.""" self.key = kwargs.pop('key', '') self.default = kwargs.pop('default', 'False') @@ -245,7 +245,7 @@ class GuiTUNSettingsGroupBoxBasic(GuiEditorWidgetQGroupBox): title=_('Bypass Tun2socks Adapter Interface IP (separated by commas)'), key='bypassTUNAdapterInterfaceIP', ), - GuiTUNSettingsItemCheckBoxXXX( + GuiTUNSettingsItemSwitchXXX( title=_( 'Disable Primary Adapter Interface DNS (Mitigating DNS leaks on Windows)' ), @@ -280,7 +280,7 @@ class GuiTUNSettingsGroupBoxMemory(GuiEditorWidgetQGroupBox): start=1, end=4, ), - GuiTUNSettingsItemCheckBoxXXX( + GuiTUNSettingsItemSwitchXXX( title=_('TCP Receive Buffer Auto-tuning'), key='tcpAutoTuning', default='False', diff --git a/tests/test_ui_behavior.py b/tests/test_ui_behavior.py index 52c1d95..b6bc258 100644 --- a/tests/test_ui_behavior.py +++ b/tests/test_ui_behavior.py @@ -36,7 +36,7 @@ from Furious.Controllers.SettingsController import ( ) from Furious.Frozenlib import AppSettings from Furious.Models import ProfileMetadata, ServerProfile -from Furious.Qt import AppQMessageBox +from Furious.Qt import AppQMessageBox, AppQSwitch from Furious.Service import ( APPLICATION_LOG_CATEGORY, CORE_LOG_CATEGORY, @@ -108,6 +108,12 @@ class EditorMappingTest(unittest.TestCase): {'TOKEN': 'one=two', 'UNICODE': '测试'}, ) self.assertTrue(editor._applicationTun2socksInput.isChecked()) + + tunLabel, tunSwitch = editor._applicationTun2socksInput.widgets() + + self.assertTrue(tunLabel.text()) + self.assertIsInstance(tunSwitch, AppQSwitch) + 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) @@ -152,6 +158,12 @@ class EditorMappingTest(unittest.TestCase): values = dialog.subscription() + self.assertIsInstance(dialog.enabledSwitch, AppQSwitch) + self.assertEqual(dialog.enabledSwitch.size(), AppQSwitch.ControlSize) + self.assertEqual( + dialog.enabledSwitch.parentWidget().objectName(), + 'SubscriptionEditorForm', + ) self.assertEqual(values['remark'], 'Fixture subscription') self.assertEqual(values['webURL'], 'https://example.test/subscription') self.assertFalse(values['enabled'])