From eca3b8e93951b8ebf560c18edd9befa2647a7a9b Mon Sep 17 00:00:00 2001 From: Loren Eteval Date: Wed, 7 Oct 2026 12:25:04 +0800 Subject: [PATCH] Polish sing-tun settings presentation Signed-off-by: Loren Eteval --- Furious/Externals/AGENTS.md | 2 + Furious/Externals/GenTranslation.py | 212 +++++++++++++++--------- Furious/Window/SingTUNSettingsDialog.py | 139 ++++++++++++---- tests/test_sing_tun.py | 201 +++++++++++++++++++++- 4 files changed, 438 insertions(+), 116 deletions(-) diff --git a/Furious/Externals/AGENTS.md b/Furious/Externals/AGENTS.md index 9d76072b..6134c501 100644 --- a/Furious/Externals/AGENTS.md +++ b/Furious/Externals/AGENTS.md @@ -27,6 +27,8 @@ structure and human-reviewed translations. retranslation under explicit locales rather than treating collision diagnostics as cosmetic. A collision-free extraction report does not prove reverse lookup is unambiguous across every language; inspect the runtime index and language-switch behavior when two source keys share translated text. + In Chinese UI text, do not insert spaces between Chinese characters and Latin abbreviations or numerals. + Preserve spaces within English names or phrases. ## Extractable source text diff --git a/Furious/Externals/GenTranslation.py b/Furious/Externals/GenTranslation.py index ed64d785..a3e79f97 100644 --- a/Furious/Externals/GenTranslation.py +++ b/Furious/Externals/GenTranslation.py @@ -3368,8 +3368,8 @@ TRANSLATION = { "source": [ "Furious.Window.SettingsPage" ], - "RU": "Настроить sing-tun...", - "ZH": "自定义 sing-tun 设置...", + "RU": "Настроить параметры sing-tun...", + "ZH": "自定义sing-tun设置...", "isReviewed": "True" }, "Customize sing-tun Settings": { @@ -3377,7 +3377,7 @@ TRANSLATION = { "Furious.Window.SingTUNSettingsDialog" ], "RU": "Настройка sing-tun", - "ZH": "自定义 sing-tun 设置", + "ZH": "自定义sing-tun设置", "isReviewed": "True" }, "SOCKS Transit": { @@ -3385,7 +3385,7 @@ TRANSLATION = { "Furious.Window.SingTUNSettingsDialog" ], "RU": "Передача через SOCKS", - "ZH": "SOCKS 转发", + "ZH": "SOCKS转发", "isReviewed": "True" }, "Stack": { @@ -3404,14 +3404,6 @@ TRANSLATION = { "ZH": "日志级别", "isReviewed": "True" }, - "SOCKS Interface Binding (empty for automatic)": { - "source": [ - "Furious.Window.SingTUNSettingsDialog" - ], - "RU": "Интерфейс SOCKS (пусто — автоматически)", - "ZH": "SOCKS 绑定接口(留空自动选择)", - "isReviewed": "True" - }, "Maximum Sessions": { "source": [ "Furious.Window.SingTUNSettingsDialog" @@ -3428,62 +3420,6 @@ TRANSLATION = { "ZH": "接口与协议栈", "isReviewed": "True" }, - "Device Name (empty for automatic)": { - "source": [ - "Furious.Window.SingTUNSettingsDialog" - ], - "RU": "Имя устройства (пусто — автоматически)", - "ZH": "设备名称(留空自动选择)", - "isReviewed": "True" - }, - "MTU (bytes)": { - "source": [ - "Furious.Window.SingTUNSettingsDialog" - ], - "RU": "MTU (байты)", - "ZH": "MTU(字节)", - "isReviewed": "True" - }, - "IPv4 Address Prefixes (comma separated)": { - "source": [ - "Furious.Window.SingTUNSettingsDialog" - ], - "RU": "Префиксы IPv4 (через запятую)", - "ZH": "IPv4 地址前缀(以逗号分隔)", - "isReviewed": "True" - }, - "IPv6 Address Prefixes (comma separated)": { - "source": [ - "Furious.Window.SingTUNSettingsDialog" - ], - "RU": "Префиксы IPv6 (через запятую)", - "ZH": "IPv6 地址前缀(以逗号分隔)", - "isReviewed": "True" - }, - "Native UDP Timeout (Go duration, e.g. 1m)": { - "source": [ - "Furious.Window.SingTUNSettingsDialog" - ], - "RU": "Тайм-аут UDP стека (длительность Go, например 1m)", - "ZH": "原生 UDP 超时(Go 时长,例如 1m)", - "isReviewed": "True" - }, - "Native ICMP Timeout (Go duration, 0 for default)": { - "source": [ - "Furious.Window.SingTUNSettingsDialog" - ], - "RU": "Тайм-аут ICMP стека (длительность Go, 0 — по умолчанию)", - "ZH": "原生 ICMP 超时(Go 时长,0 使用默认值)", - "isReviewed": "True" - }, - "Native UDP NAT Limit (0 for default)": { - "source": [ - "Furious.Window.SingTUNSettingsDialog" - ], - "RU": "Лимит UDP NAT стека (0 — по умолчанию)", - "ZH": "原生 UDP NAT 上限(0 使用默认值)", - "isReviewed": "True" - }, "Advanced Options": { "source": [ "Furious.Window.SingTUNSettingsDialog" @@ -3497,7 +3433,7 @@ TRANSLATION = { "Furious.Window.SingTUNSettingsDialog" ], "RU": "Используйте точные имена полей JSON. Поддерживаются списки маршрутов и исключений, StrictRoute, InterfaceScope, ForwarderBindInterface и IncludeAllNetworks. AutoRoute, режим внешней конфигурации и настройки DNS управляются приложением. Неподдерживаемые поля отклоняются.", - "ZH": "请使用准确的原生 JSON 字段名。支持路由地址及排除列表、StrictRoute、InterfaceScope、ForwarderBindInterface 和 IncludeAllNetworks。AutoRoute、外部配置模式和 DNS 配置均由应用统一管理。不支持的字段会被拒绝。", + "ZH": "请使用准确的原生JSON字段名。支持路由地址及排除列表、StrictRoute、InterfaceScope、ForwarderBindInterface和IncludeAllNetworks。AutoRoute、外部配置模式和DNS配置均由应用统一管理。不支持的字段会被拒绝。", "isReviewed": "True" }, "Connect Timeout (seconds)": { @@ -3513,7 +3449,7 @@ TRANSLATION = { "Furious.Window.SingTUNSettingsDialog" ], "RU": "Тайм-аут UDP SOCKS (секунды)", - "ZH": "SOCKS UDP 超时(秒)", + "ZH": "SOCKS UDP超时(秒)", "isReviewed": "True" }, "TCP Idle Timeout (seconds)": { @@ -3521,7 +3457,7 @@ TRANSLATION = { "Furious.Window.SingTUNSettingsDialog" ], "RU": "Тайм-аут простоя TCP (секунды)", - "ZH": "TCP 空闲超时(秒)", + "ZH": "TCP空闲超时(秒)", "isReviewed": "True" }, "UDP Mapping": { @@ -3529,7 +3465,7 @@ TRANSLATION = { "Furious.Window.SingTUNSettingsDialog" ], "RU": "Сопоставление UDP", - "ZH": "UDP 映射方式", + "ZH": "UDP映射方式", "isReviewed": "True" }, "UDP Filtering": { @@ -3537,7 +3473,7 @@ TRANSLATION = { "Furious.Window.SingTUNSettingsDialog" ], "RU": "Фильтрация UDP", - "ZH": "UDP 过滤方式", + "ZH": "UDP过滤方式", "isReviewed": "True" }, "Automatic": { @@ -3592,7 +3528,7 @@ TRANSLATION = { "source": [ "Furious.Window.SettingsPage" ], - "RU": "Настройте интерфейс, стек, SOCKS-передачу и параметры хоста sing-tun.", + "RU": "Настройте интерфейс, стек, передачу через SOCKS и параметры хоста sing-tun.", "ZH": "配置 sing-tun 的接口、协议栈、SOCKS 转发和主机设置。", "isReviewed": "True" }, @@ -3604,14 +3540,6 @@ TRANSLATION = { "ZH": "主机设置", "isReviewed": "True" }, - "TCP Congestion Control (Go stack)": { - "source": [ - "Furious.Window.SingTUNSettingsDialog" - ], - "RU": "Управление перегрузкой TCP (стек Go)", - "ZH": "TCP 拥塞控制(Go 协议栈)", - "isReviewed": "True" - }, "TUN Adapter Interface DNS": { "source": [ "Furious.Window.SingTUNSettingsDialog", @@ -3709,5 +3637,125 @@ TRANSLATION = { "RU": "Показывать только избранные профили", "ZH": "仅显示已收藏的配置", "isReviewed": "True" + }, + "SOCKS Interface Binding": { + "source": [ + "Furious.Window.SingTUNSettingsDialog" + ], + "RU": "Интерфейс SOCKS", + "ZH": "SOCKS绑定接口", + "isReviewed": "True" + }, + "Device Name": { + "source": [ + "Furious.Window.SingTUNSettingsDialog" + ], + "RU": "Имя устройства", + "ZH": "设备名称", + "isReviewed": "True" + }, + "IPv4 Address Prefixes": { + "source": [ + "Furious.Window.SingTUNSettingsDialog" + ], + "RU": "Префиксы IPv4", + "ZH": "IPv4地址前缀", + "isReviewed": "True" + }, + "IPv6 Address Prefixes": { + "source": [ + "Furious.Window.SingTUNSettingsDialog" + ], + "RU": "Префиксы IPv6", + "ZH": "IPv6地址前缀", + "isReviewed": "True" + }, + "Native UDP Timeout": { + "source": [ + "Furious.Window.SingTUNSettingsDialog" + ], + "RU": "Тайм-аут UDP стека", + "ZH": "原生UDP超时", + "isReviewed": "True" + }, + "Native ICMP Timeout": { + "source": [ + "Furious.Window.SingTUNSettingsDialog" + ], + "RU": "Тайм-аут ICMP стека", + "ZH": "原生ICMP超时", + "isReviewed": "True" + }, + "Native UDP NAT Limit": { + "source": [ + "Furious.Window.SingTUNSettingsDialog" + ], + "RU": "Лимит UDP NAT стека", + "ZH": "原生UDP NAT上限", + "isReviewed": "True" + }, + "empty for automatic": { + "source": [ + "Furious.Window.SingTUNSettingsDialog" + ], + "RU": "пусто — автоматически", + "ZH": "留空自动配置", + "isReviewed": "True" + }, + "separated by commas": { + "source": [ + "Furious.Window.SingTUNSettingsDialog" + ], + "RU": "через запятую", + "ZH": "以逗号分隔", + "isReviewed": "True" + }, + "duration with units, e.g. 1m": { + "source": [ + "Furious.Window.SingTUNSettingsDialog" + ], + "RU": "длительность с единицей измерения, например 1m", + "ZH": "带单位的时长,例如1m", + "isReviewed": "True" + }, + "duration with units, or 0 for default": { + "source": [ + "Furious.Window.SingTUNSettingsDialog" + ], + "RU": "длительность с единицей измерения; 0 — по умолчанию", + "ZH": "带单位的时长,或填0使用默认值", + "isReviewed": "True" + }, + "0 for default": { + "source": [ + "Furious.Window.SingTUNSettingsDialog" + ], + "RU": "0 — по умолчанию", + "ZH": "0使用默认值", + "isReviewed": "True" + }, + "MTU": { + "source": [ + "Furious.Window.SingTUNSettingsDialog" + ], + "RU": "MTU", + "ZH": "MTU", + "isReviewed": "True" + }, + "TCP Congestion Control": { + "source": [ + "Furious.Window.SingTUNSettingsDialog" + ], + "RU": "Управление перегрузкой TCP", + "ZH": "TCP拥塞控制", + "isReviewed": "True" + }, + "Available when Stack is set to go or Automatic.": { + "source": [ + "Furious.Window.SingTUNSettingsDialog" + ], + "RU": "Доступно, если для «Стек» выбрано go или «Автоматически».", + "ZH": "协议栈设为go或自动时可用。", + "isReviewed": "True" } } diff --git a/Furious/Window/SingTUNSettingsDialog.py b/Furious/Window/SingTUNSettingsDialog.py index 88134c9b..05f4d9c9 100644 --- a/Furious/Window/SingTUNSettingsDialog.py +++ b/Furious/Window/SingTUNSettingsDialog.py @@ -37,7 +37,7 @@ from Furious.Qt import ( AppQDialogButtonBox, DraculaJSONTextEditor, ) -from Furious.Qt.Signals import connectWeakly +from Furious.Qt.Signals import connectWeakly, singleShotWeakly from Furious.Qt import gettext as _ from PySide6 import QtCore @@ -47,6 +47,8 @@ from PySide6.QtWidgets import ( QVBoxLayout, QWidget, QScrollArea, + QStyle, + QStyleOptionFrame, ) import copy @@ -113,8 +115,10 @@ class SingTUNSettingsDialog(AppQTransientDialog): self._text( bridge, 'network_interface', - _('SOCKS Interface Binding (empty for automatic)'), + _('SOCKS Interface Binding'), common.get('network_interface'), + placeholder=_('empty for automatic'), + maximumWidth=360, ) self._number( bridge, @@ -125,12 +129,27 @@ class SingTUNSettingsDialog(AppQTransientDialog): 16384, ) - for key, label in ( - ('connect_timeout', _('Connect Timeout (seconds)')), - ('udp_timeout', _('SOCKS UDP Timeout (seconds)')), - ('tcp_idle_timeout', _('TCP Idle Timeout (seconds)')), - ): - self._text(bridge, key, label, common.get(key)) + self._text( + bridge, + 'connect_timeout', + _('Connect Timeout (seconds)'), + common.get('connect_timeout'), + maximumWidth=160, + ) + self._text( + bridge, + 'udp_timeout', + _('SOCKS UDP Timeout (seconds)'), + common.get('udp_timeout'), + maximumWidth=160, + ) + self._text( + bridge, + 'tcp_idle_timeout', + _('TCP Idle Timeout (seconds)'), + common.get('tcp_idle_timeout'), + maximumWidth=160, + ) native = self._page(_('Interface and Stack')) tun = ( @@ -140,28 +159,37 @@ class SingTUNSettingsDialog(AppQTransientDialog): ) self._text( - native, 'Name', _('Device Name (empty for automatic)'), tun.get('Name', '') + native, + 'Name', + _('Device Name'), + tun.get('Name', ''), + placeholder=_('empty for automatic'), + maximumWidth=360, ) - self._number(native, 'MTU', _('MTU (bytes)'), tun.get('MTU', 1500), 68, 65535) + self._number(native, 'MTU', _('MTU'), tun.get('MTU', 1500), 68, 65535) self._text( native, 'Inet4Address', - _('IPv4 Address Prefixes (comma separated)'), + _('IPv4 Address Prefixes'), ( ', '.join(str(value) for value in tun.get('Inet4Address', [])) if isinstance(tun.get('Inet4Address'), list) else '' ), + placeholder=_('separated by commas'), + maximumWidth=600, ) self._text( native, 'Inet6Address', - _('IPv6 Address Prefixes (comma separated)'), + _('IPv6 Address Prefixes'), ( ', '.join(str(value) for value in tun.get('Inet6Address', [])) if isinstance(tun.get('Inet6Address'), list) else '' ), + placeholder=_('separated by commas'), + maximumWidth=600, ) stack = ( @@ -173,7 +201,7 @@ class SingTUNSettingsDialog(AppQTransientDialog): self._choice( native, 'TCPCongestionControl', - _('TCP Congestion Control (Go stack)'), + _('TCP Congestion Control'), [ (_('Automatic'), ''), ('cubic', 'cubic'), @@ -182,23 +210,33 @@ class SingTUNSettingsDialog(AppQTransientDialog): ], stack.get('TCPCongestionControl', ''), ) + self.fields['TCPCongestionControl'].setToolTip( + _('Available when Stack is set to go or Automatic.') + ) + + self._text( + native, + 'UDPNATMax', + _('Native UDP NAT Limit'), + stack.get('UDPNATMax', 0), + placeholder=_('0 for default'), + maximumWidth=200, + ) self._text( native, 'UDPTimeout', - _('Native UDP Timeout (Go duration, e.g. 1m)'), + _('Native UDP Timeout'), stack.get('UDPTimeout', '1m'), + placeholder=_('duration with units, e.g. 1m'), + maximumWidth=180, ) self._text( native, 'ICMPTimeout', - _('Native ICMP Timeout (Go duration, 0 for default)'), + _('Native ICMP Timeout'), stack.get('ICMPTimeout', '0'), - ) - self._text( - native, - 'UDPNATMax', - _('Native UDP NAT Limit (0 for default)'), - stack.get('UDPNATMax', 0), + placeholder=_('duration with units, or 0 for default'), + maximumWidth=180, ) for key, label in ( @@ -224,16 +262,17 @@ class SingTUNSettingsDialog(AppQTransientDialog): ) hostForm = self._page(_('Host Settings')) - for key, label in ( - ('primaryAdapterInterfaceName', _('Primary Adapter Interface Name')), - ('primaryAdapterInterfaceIP', _('Primary Adapter Interface IP')), - ('tunAdapterInterfaceDNS', _('TUN Adapter Interface DNS')), + for key, label, width in ( + ('primaryAdapterInterfaceName', _('Primary Adapter Interface Name'), 360), + ('primaryAdapterInterfaceIP', _('Primary Adapter Interface IP'), 360), + ('tunAdapterInterfaceDNS', _('TUN Adapter Interface DNS'), 360), ( 'bypassTUNAdapterInterfaceIP', _('Bypass TUN Adapter Interface IP (separated by commas)'), + 600, ), ): - self._text(hostForm, key, label, host.get(key, '')) + self._text(hostForm, key, label, host.get(key, ''), maximumWidth=width) self.disableDNS = AppQSwitch() self.disableDNS.setChecked( @@ -313,6 +352,38 @@ class SingTUNSettingsDialog(AppQTransientDialog): layout.addWidget(buttons) + def _fitDurationInputs(self): + for key in ('UDPTimeout', 'ICMPTimeout'): + widget = self.fields[key] + widget.ensurePolished() + + option = QStyleOptionFrame() + widget.initStyleOption(option) + margins = widget.textMargins() + textWidth = widget.fontMetrics().horizontalAdvance(widget.placeholderText()) + size = widget.style().sizeFromContents( + QStyle.ContentsType.CT_LineEdit, + option, + QtCore.QSize( + textWidth + margins.left() + margins.right() + 4, + widget.sizeHint().height(), + ), + widget, + ) + width = max(180, size.width() + 80) + widget.setMinimumWidth(width) + widget.setMaximumWidth(width) + + def showEvent(self, event): + super().showEvent(event) + + self._fitDurationInputs() + + def retranslate(self): + super().retranslate() + + singleShotWeakly(0, self, '_fitDurationInputs') + def _page(self, title): page = QWidget() @@ -327,22 +398,30 @@ class SingTUNSettingsDialog(AppQTransientDialog): return form - def _text(self, form, key, label, value): + def _addField(self, form, label, widget): + form.addRow(AppQLabel(label), widget) + + def _text(self, form, key, label, value, *, placeholder='', maximumWidth=None): widget = AppQLineEdit() widget.setText(str(value) if value is not None else '') + widget.setPlaceholderText(placeholder) + + if maximumWidth is not None: + widget.setMaximumWidth(maximumWidth) self.fields[key] = widget - form.addRow(AppQLabel(label), widget) + self._addField(form, label, widget) def _number(self, form, key, label, value, minimum, maximum): widget = AppQSpinBox() widget.setRange(minimum, maximum) widget.setValue(value if type(value) is int else minimum) + widget.setMaximumWidth(135) self.fields[key] = widget - form.addRow(AppQLabel(label), widget) + self._addField(form, label, widget) def _choice(self, form, key, label, choices, value): widget = AppQComboBox() @@ -354,7 +433,7 @@ class SingTUNSettingsDialog(AppQTransientDialog): self.fields[key] = widget - form.addRow(AppQLabel(label), widget) + self._addField(form, label, widget) def document(self): """Prepare sing-tun's complete candidate before its live collection changes.""" diff --git a/tests/test_sing_tun.py b/tests/test_sing_tun.py index eeaaf76f..8e46a889 100644 --- a/tests/test_sing_tun.py +++ b/tests/test_sing_tun.py @@ -25,7 +25,7 @@ from Furious.Core.MultiprocessingRuntime import ( MultiprocessingRuntime, ) from Furious.Interface import CoreRuntime, RuntimeState, RuntimeExitReason -from Furious.Frozenlib import AppSettings +from Furious.Frozenlib import AppSettings, Mixins from Furious.Models.SingTUN import prepareSingTUNSettings, addSingTUNExclusions from Furious.Frozenlib.SocksProxy import socksURL from Furious.Repository import Storage @@ -56,7 +56,8 @@ from Furious.Service.RuntimeLease import RuntimeEventRouter, RuntimeLeaseState from Furious.Service.LogManager import LogManager, TUN_LOG_CATEGORY from PySide6 import QtCore -from PySide6.QtWidgets import QLabel, QWidget +from PySide6.QtTest import QTest +from PySide6.QtWidgets import QLabel, QStyle, QStyleOptionFrame, QWidget from shiboken6 import isValid from tests.support import application, isolatedSettings, processQtEvents, waitFor @@ -564,8 +565,12 @@ class SingTUNUIAndStorageTest(unittest.TestCase): for language in ('ZH', 'RU'): self.assertNotEqual( - _('TCP Congestion Control (Go stack)', language), - 'TCP Congestion Control (Go stack)', + _('TCP Congestion Control', language), + 'TCP Congestion Control', + ) + self.assertNotEqual( + _('Available when Stack is set to go or Automatic.', language), + 'Available when Stack is set to go or Automatic.', ) dialog.reject() @@ -589,6 +594,153 @@ class SingTUNUIAndStorageTest(unittest.TestCase): processQtEvents() self.assertFalse(isValid(dialog)) + def testSelectiveRowsKeepInputsBoundedAndPreserveKeyboardOrderAndValues(self): + pages = ( + ( + 0, + (), + ( + 'stack', + 'log_level', + 'network_interface', + 'max_sessions', + 'connect_timeout', + 'udp_timeout', + 'tcp_idle_timeout', + ), + ), + ( + 1, + (), + ( + 'Name', + 'MTU', + 'Inet4Address', + 'Inet6Address', + 'TCPCongestionControl', + 'UDPNATMax', + 'UDPTimeout', + 'ICMPTimeout', + 'UDPMapping', + 'UDPFiltering', + ), + ), + ) + + for language in ('EN', 'RU', 'ZH'): + with self.subTest(language=language), isolatedSettings(): + AppSettings.set('Language', language) + dialog = self._dialog({}) + original = dialog.document() + dialog.open() + dialog.activateWindow() + processQtEvents() + + try: + for tab, pairs, keys in pages: + dialog.tabs.setCurrentIndex(tab) + processQtEvents() + + if tab == 0: + self.assertLess( + dialog.fields['stack'].geometry().bottom(), + dialog.fields['log_level'].geometry().top(), + ) + self.assertLess( + dialog.fields['connect_timeout'].geometry().bottom(), + dialog.fields['udp_timeout'].geometry().top(), + ) + + for left, right in pairs: + first, second = dialog.fields[left], dialog.fields[right] + self.assertLessEqual( + abs( + first.geometry().center().y() + - second.geometry().center().y() + ), + 2, + ) + self.assertLess( + first.geometry().right(), second.geometry().left() + ) + + ordered = [dialog.fields[key] for key in keys] + ordered[0].setFocus() + processQtEvents() + + for current, following in zip(ordered, ordered[1:]): + QTest.keyClick(current, QtCore.Qt.Key_Tab) + self.assertTrue(following.hasFocus()) + + for first, second in ( + ('Name', 'MTU'), + ('Inet4Address', 'Inet6Address'), + ('TCPCongestionControl', 'UDPNATMax'), + ('UDPTimeout', 'ICMPTimeout'), + ('UDPMapping', 'UDPFiltering'), + ): + self.assertLess( + dialog.fields[first].geometry().bottom(), + dialog.fields[second].geometry().top(), + ) + + for key, width in ( + ('Name', 360), + ('network_interface', 360), + ('UDPNATMax', 200), + ('connect_timeout', 160), + ('udp_timeout', 160), + ('tcp_idle_timeout', 160), + ): + self.assertLessEqual(dialog.fields[key].width(), width) + + for key in ('Inet4Address', 'Inet6Address'): + self.assertLessEqual(dialog.fields[key].width(), 600) + self.assertEqual(dialog.fields[key].maximumWidth(), 600) + + self.assertEqual(dialog.document(), original) + finally: + dialog.reject() + processQtEvents() + + def testDurationInputsFitTranslatedPlaceholdersWithoutChangingValues(self): + with isolatedSettings(): + AppSettings.set('Language', 'EN') + dialog = self._dialog({}) + original = dialog.document() + dialog.tabs.setCurrentIndex(1) + dialog.open() + processQtEvents() + + try: + for language in ('RU', 'ZH', 'EN'): + AppSettings.set('Language', language) + Mixins.QTranslatable.retranslateAll() + processQtEvents() + + for key in ('UDPTimeout', 'ICMPTimeout'): + field = dialog.fields[key] + option = QStyleOptionFrame() + field.initStyleOption(option) + content = field.style().subElementRect( + QStyle.SubElement.SE_LineEditContents, option, field + ) + margins = field.textMargins() + available = content.width() - margins.left() - margins.right() + self.assertGreaterEqual( + available, + field.fontMetrics().horizontalAdvance( + field.placeholderText() + ) + + 80, + ) + self.assertLess(field.width(), dialog.width()) + + self.assertEqual(dialog.document(), original) + finally: + dialog.reject() + processQtEvents() + def testDialogPreparesNativeAndHostOptionsInOneIndependentDocument(self): sing = {'host_options': {'tunAdapterInterfaceDNS': '9.9.9.9'}} dialog = self._dialog(sing) @@ -689,6 +841,10 @@ class SingTUNUIAndStorageTest(unittest.TestCase): form = dialog.disableDNS.parentWidget().layout() legacyLabels = {label.text() for label in legacy.findChildren(QLabel)} + singLabels = { + label.text() + for label in dialog.disableDNS.parentWidget().findChildren(QLabel) + } for key, source in ( ('primaryAdapterInterfaceName', 'Primary Adapter Interface Name'), @@ -700,6 +856,7 @@ class SingTUNUIAndStorageTest(unittest.TestCase): ), ): expected = _(source) + self.assertIn(expected, singLabels) self.assertEqual( form.labelForField(dialog.fields[key]).text(), expected ) @@ -722,6 +879,42 @@ class SingTUNUIAndStorageTest(unittest.TestCase): self.assertIn(dnsLabel, legacyLabels) self.assertEqual(form.rowCount(), 5) + dialog.tabs.setCurrentIndex(2) + dialog.open() + processQtEvents() + name = dialog.fields['primaryAdapterInterfaceName'] + address = dialog.fields['primaryAdapterInterfaceIP'] + self.assertLess(name.geometry().bottom(), address.geometry().top()) + for key, width in ( + ('primaryAdapterInterfaceName', 360), + ('primaryAdapterInterfaceIP', 360), + ('tunAdapterInterfaceDNS', 360), + ('bypassTUNAdapterInterfaceIP', 600), + ): + self.assertLessEqual(dialog.fields[key].width(), width) + + ordered = [ + dialog.fields[key] + for key in ( + 'primaryAdapterInterfaceName', + 'primaryAdapterInterfaceIP', + 'tunAdapterInterfaceDNS', + 'bypassTUNAdapterInterfaceIP', + ) + ] + ordered[0].setFocus() + for current, following in zip(ordered, ordered[1:]): + QTest.keyClick(current, QtCore.Qt.Key_Tab) + self.assertTrue(following.hasFocus()) + + if language == 'ZH': + self.assertEqual( + _('Customize sing-tun Settings'), '自定义sing-tun设置' + ) + self.assertEqual( + _('Customize sing-tun Settings...'), '自定义sing-tun设置...' + ) + legacy.reject() legacy.deleteLater() dialog.reject()