Repository: spesmilo/electrum
Pull request #9507: qt: refactor NetworkChoiceLayout to ProxyWidget+ServerWidget
Author: login: accumulator
Created: 2025-01-30T13:54:58Z | Merged: 2025-03-05T15:07:57Z

The wizard was already using `ProxyWidget` and `ServerWidget`, which are a refactor of `NetworkChoiceLayout`.

This commit refactors the 'regular' network settings dialog to the same components.

Proxy settings are refactored to `ProxySettings` class, replacing the dict and encapsulating most proxy settings related code.

The Tor checkbox is removed, as it was somewhat confusing and not kept in UI-sync with actual Tor use. It is replaced with a 'detect tor proxy' button. Tor probing is moved to backend code in `electrum/network.py`. QML now also gained Tor probe functionality.





Diff:
diff --git a/electrum/base_crash_reporter.py b/electrum/base_crash_reporter.py
index a840e6e4ab4c..4574fb6fe741 100644
--- a/electrum/base_crash_reporter.py
+++ b/electrum/base_crash_reporter.py
@@ -25,7 +25,7 @@
 import traceback
 import sys
 import queue
-from typing import NamedTuple, Optional
+from typing import TYPE_CHECKING, NamedTuple, Optional
 
 from .version import ELECTRUM_VERSION
 from . import constants
@@ -33,6 +33,9 @@
 from .util import make_aiohttp_session, error_text_str_to_safe_str
 from .logging import describe_os_version, Logger, get_git_version
 
+if TYPE_CHECKING:
+    from .network import ProxySettings
+
 
 class CrashReportResponse(NamedTuple):
     status: Optional[str]
@@ -69,7 +72,7 @@ def __init__(self, exctype, value, tb):
         Logger.__init__(self)
         self.exc_args = (exctype, value, tb)
 
-    def send_report(self, asyncio_loop, proxy, *, timeout=None) -> CrashReportResponse:
+    def send_report(self, asyncio_loop, proxy: 'ProxySettings', *, timeout=None) -> CrashReportResponse:
         # FIXME the caller needs to catch generic "Exception", as this method does not have a well-defined API...
         if constants.net.GENESIS[-4:] not in ["4943", "e26f"] and ".electrum.org" in BaseCrashReporter.report_server:
             # Gah! Some kind of altcoin wants to send us crash reports.
@@ -98,7 +101,7 @@ def send_report(self, asyncio_loop, proxy, *, timeout=None) -> CrashReportRespon
         )
         return ret
 
-    async def do_post(self, proxy, url, data) -> str:
+    async def do_post(self, proxy: 'ProxySettings', url, data) -> str:
         async with make_aiohttp_session(proxy) as session:
             async with session.post(url, data=data, raise_for_status=True) as resp:
                 return await resp.text()
diff --git a/electrum/gui/qml/components/NetworkOverview.qml b/electrum/gui/qml/components/NetworkOverview.qml
index 187dd544667d..9c41609a0e4b 100644
--- a/electrum/gui/qml/components/NetworkOverview.qml
+++ b/electrum/gui/qml/components/NetworkOverview.qml
@@ -227,21 +227,21 @@ Pane {
                     color: Material.accentColor
                 }
                 Label {
-                    text: 'mode' in Network.proxy ? qsTr('enabled') : qsTr('disabled')
+                    text: Network.proxy.enabled ? qsTr('enabled') : qsTr('disabled')
                 }
 
                 Label {
-                    visible: 'mode' in Network.proxy
+                    visible: Network.proxy.enabled
                     text: qsTr('Proxy server:');
                     color: Material.accentColor
                 }
                 Label {
-                    visible: 'mode' in Network.proxy
-                    text: Network.proxy['host'] ? Network.proxy['host'] + ':' + Network.proxy['port'] : ''
+                    visible: Network.proxy.enabled
+                    text: Network.proxy.host ? Network.proxy.host + ':' + Network.proxy.port : ''
                 }
 
                 Label {
-                    visible: 'mode' in Network.proxy
+                    visible: Network.proxy.enabled
                     text: qsTr('Proxy type:');
                     color: Material.accentColor
                 }
@@ -253,8 +253,8 @@ Pane {
                         source: '../../icons/tor_logo.png'
                     }
                     Label {
-                        visible: 'mode' in Network.proxy
-                        text: Network.isProxyTor ? 'TOR' : (Network.proxy['mode'] || '')
+                        visible: Network.proxy.enabled
+                        text: Network.isProxyTor ? 'TOR' : (Network.proxy.mode || '')
                     }
                 }
 
diff --git a/electrum/gui/qml/components/ProxyConfigDialog.qml b/electrum/gui/qml/components/ProxyConfigDialog.qml
index 93e6d86d5676..c3d6bf31b183 100644
--- a/electrum/gui/qml/components/ProxyConfigDialog.qml
+++ b/electrum/gui/qml/components/ProxyConfigDialog.qml
@@ -37,12 +37,7 @@ ElDialog {
             text: qsTr('Ok')
             icon.source: '../../icons/confirmed.png'
             onClicked: {
-                var proxy = proxyconfig.toProxyDict()
-                if (proxy && proxy['enabled'] == true) {
-                    Network.proxy = proxy
-                } else {
-                    Network.proxy = {'enabled': false}
-                }
+                Network.proxy = proxyconfig.toProxyDict()
                 rootItem.close()
             }
         }
@@ -52,17 +47,13 @@ ElDialog {
     Component.onCompleted: {
         var p = Network.proxy
 
-        if ('mode' in p) {
-            proxyconfig.proxy_enabled = true
-            proxyconfig.proxy_address = p['host']
-            proxyconfig.proxy_port = p['port']
-            proxyconfig.username = p['user']
-            proxyconfig.password = p['password']
-            proxyconfig.proxy_type = proxyconfig.proxy_type_map.map(function(x) {
-                return x.value
-            }).indexOf(p['mode'])
-        } else {
-            proxyconfig.proxy_enabled = false
-        }
+        proxyconfig.proxy_enabled = p['enabled']
+        proxyconfig.proxy_address = p['host']
+        proxyconfig.proxy_port = p['port']
+        proxyconfig.username = p['user']
+        proxyconfig.password = p['password']
+        proxyconfig.proxy_type = proxyconfig.proxy_type_map.map(function(x) {
+            return x.value
+        }).indexOf(p['mode'])
     }
 }
diff --git a/electrum/gui/qml/components/controls/OnchainNetworkStatusIndicator.qml b/electrum/gui/qml/components/controls/OnchainNetworkStatusIndicator.qml
index 1c3bc4c1e4cc..18f2c2b68870 100644
--- a/electrum/gui/qml/components/controls/OnchainNetworkStatusIndicator.qml
+++ b/electrum/gui/qml/components/controls/OnchainNetworkStatusIndicator.qml
@@ -10,7 +10,7 @@ Image {
     property bool lagging: connected && Network.isLagging
     property bool fork: connected && Network.chaintips > 1
     property bool syncing: connected && Daemon.currentWallet && Daemon.currentWallet.synchronizing
-    property bool proxy: connected && 'mode' in Network.proxy && Network.proxy.mode
+    property bool proxy: connected && Network.proxy.enabled
 
     // ?: in order to keep this a binding..
     source: Qt.resolvedUrl(!connected
diff --git a/electrum/gui/qml/components/controls/ProxyConfig.qml b/electrum/gui/qml/components/controls/ProxyConfig.qml
index 711d759dbf63..05bc05b2b305 100644
--- a/electrum/gui/qml/components/controls/ProxyConfig.qml
+++ b/electrum/gui/qml/components/controls/ProxyConfig.qml
@@ -19,17 +19,17 @@ Item {
         { text: qsTr('SOCKS4'), value: 'socks4' }
     ]
 
+    property bool _probing: false
+
     function toProxyDict() {
         var p = {}
         p['enabled'] = pc.proxy_enabled
-        if (pc.proxy_enabled) {
-            var type = proxy_type_map[pc.proxy_type]['value']
-            p['mode'] = type
-            p['host'] = pc.proxy_address
-            p['port'] = pc.proxy_port
-            p['user'] = pc.username
-            p['password'] = pc.password
-        }
+        var type = proxy_type_map[pc.proxy_type]['value']
+        p['mode'] = type
+        p['host'] = pc.proxy_address
+        p['port'] = pc.proxy_port
+        p['user'] = pc.username
+        p['password'] = pc.password
         return p
     }
 
@@ -51,15 +51,6 @@ Item {
             textRole: 'text'
             valueRole: 'value'
             model: proxy_type_map
-
-            onCurrentIndexChanged: {
-                if (currentIndex == 0) {
-                    if (address.text == '' || port.text == '') {
-                        address.text = "127.0.0.1"
-                        port.text = "9050"
-                    }
-                }
-            }
         }
 
         GridLayout {
@@ -109,5 +100,44 @@ Item {
                 enabled: proxy_enabled_cb.checked
             }
         }
+
+        Pane {
+            Layout.alignment: Qt.AlignHCenter
+            Layout.topMargin: constants.paddingLarge
+            padding: 0
+            background: Rectangle {
+                color: constants.darkerDialogBackground
+            }
+            FlatButton {
+                enabled: proxy_enabled_cb.checked && !_probing
+                text: qsTr('Detect Tor proxy')
+                onClicked: {
+                    _probing = true
+                    Network.probeTor()
+                }
+            }
+        }
+
+        BusyIndicator {
+            id: spinner
+            Layout.alignment: Qt.AlignHCenter
+            Layout.topMargin: constants.paddingSmall
+            Layout.preferredWidth: constants.iconSizeXLarge
+            Layout.preferredHeight: constants.iconSizeXLarge
+            running: visible
+            visible: _probing
+        }
+    }
+
+    Connections {
+        target: Network
+        function onTorProbeFinished(host, port) {
+            _probing = false
+            if (host && port) {
+                proxytype.currentIndex = 0
+                proxy_port = ""+port
+                proxy_address = host
+            }
+        }
     }
 }
diff --git a/electrum/gui/qml/qenetwork.py b/electrum/gui/qml/qenetwork.py
index ced8f27be5c2..ec638133cce4 100644
--- a/electrum/gui/qml/qenetwork.py
+++ b/electrum/gui/qml/qenetwork.py
@@ -1,9 +1,10 @@
 from typing import TYPE_CHECKING
 
-from PyQt6.QtCore import pyqtProperty, pyqtSignal, QObject
+from PyQt6.QtCore import pyqtProperty, pyqtSignal, QObject, pyqtSlot
 
 from electrum.logging import get_logger
 from electrum import constants
+from electrum.network import ProxySettings
 from electrum.interface import ServerAddr
 from electrum.simple_config import FEERATE_DEFAULT_RELAY
 
@@ -24,6 +25,7 @@ class QENetwork(QObject, QtEventListener):
     serverHeightChanged = pyqtSignal([int], arguments=['height'])
     proxySet = pyqtSignal()
     proxyChanged = pyqtSignal()
+    torProbeFinished = pyqtSignal([str, int], arguments=['host', 'port'])
     statusChanged = pyqtSignal()
     feeHistogramUpdated = pyqtSignal()
     chaintipsChanged = pyqtSignal()
@@ -253,14 +255,14 @@ def networkName(self):
     @pyqtProperty('QVariantMap', notify=proxyChanged)
     def proxy(self):
         net_params = self.network.get_parameters()
-        return net_params.proxy if net_params.proxy else {}
+        proxy = net_params.proxy
+        return proxy.to_dict()
 
     @proxy.setter
-    def proxy(self, proxy_settings):
+    def proxy(self, proxy_dict):
         net_params = self.network.get_parameters()
-        if not proxy_settings['enabled']:
-            proxy_settings = None
-        net_params = net_params._replace(proxy=proxy_settings)
+        proxy = ProxySettings.from_dict(proxy_dict)
+        net_params = net_params._replace(proxy=proxy)
         self.network.run_from_another_thread(self.network.set_parameters(net_params))
         self.proxyChanged.emit()
 
@@ -289,3 +291,7 @@ def serverListModel(self):
         if self._serverListModel is None:
             self._serverListModel = QEServerListModel(self.network)
         return self._serverListModel
+
+    @pyqtSlot()
+    def probeTor(self):
+        ProxySettings.probe_tor(self.torProbeFinished.emit)  # via signal
diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py
index 53cd67f168b4..93cea0652fc7 100644
--- a/electrum/gui/qt/__init__.py
+++ b/electrum/gui/qt/__init__.py
@@ -256,7 +256,6 @@ def _cleanup_before_exit(self):
             window.clean_up()
         if self.network_dialog:
             self.network_dialog.close()
-            self.network_dialog.clean_up()
             self.network_dialog = None
         if self.lightning_dialog:
             self.lightning_dialog.close()
@@ -294,16 +293,13 @@ def show_lightning_dialog(self):
             self.lightning_dialog = LightningDialog(self)
         self.lightning_dialog.bring_to_top()
 
-    def show_network_dialog(self):
+    def show_network_dialog(self, proxy_tab=False):
         if self.network_dialog:
-            self.network_dialog.on_event_network_updated()
-            self.network_dialog.show()
+            self.network_dialog.show(proxy_tab=proxy_tab)
             self.network_dialog.raise_()
             return
-        self.network_dialog = NetworkDialog(
-            network=self.daemon.network,
-            config=self.config)
-        self.network_dialog.show()
+        self.network_dialog = NetworkDialog(network=self.daemon.network)
+        self.network_dialog.show(proxy_tab=proxy_tab)
 
     def _create_window_for_wallet(self, wallet):
         w = ElectrumWindow(self, wallet)
diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
index 6d9a1e9a4bf2..8e31be801620 100644
--- a/electrum/gui/qt/main_window.py
+++ b/electrum/gui/qt/main_window.py
@@ -1004,12 +1004,12 @@ def update_status(self):
                 if self.fx.is_enabled():
                     balance_text += self.fx.get_fiat_status_text(balance,
                         self.base_unit(), self.get_decimal_point()) or ''
-                if not self.network.proxy:
+                if not self.network.proxy or not self.network.proxy.enabled:
                     icon = read_QIcon("status_connected%s.png"%fork_str)
                 else:
                     icon = read_QIcon("status_connected_proxy%s.png"%fork_str)
         else:
-            if self.network.proxy:
+            if self.network.proxy and self.network.proxy.enabled:
                 network_text = "{} ({})".format(_("Not connected"), _("proxy enabled"))
             else:
                 network_text = _("Not connected")
@@ -1760,7 +1760,7 @@ def create_status_bar(self):
             self.tor_button = StatusBarButton(
                 read_QIcon("tor_logo.png"),
                 _("Tor"),
-                self.gui_object.show_network_dialog,
+                partial(self.gui_object.show_network_dialog, proxy_tab=True),
                 sb_height,
             )
             sb.addPermanentWidget(self.tor_button)
diff --git a/electrum/gui/qt/network_dialog.py b/electrum/gui/qt/network_dialog.py
index 8bc3c032c097..43ce7a71661a 100644
--- a/electrum/gui/qt/network_dialog.py
+++ b/electrum/gui/qt/network_dialog.py
@@ -24,25 +24,24 @@
 # SOFTWARE.
 
 from enum import IntEnum
-import threading
 
-from PyQt6.QtCore import Qt, pyqtSignal, QThread
-from PyQt6.QtWidgets import (QTreeWidget, QTreeWidgetItem, QMenu, QGridLayout, QComboBox,
-                             QLineEdit, QDialog, QVBoxLayout, QHeaderView, QCheckBox,
-                             QTabWidget, QWidget, QLabel)
+from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot
+from PyQt6.QtWidgets import (
+    QTreeWidget, QTreeWidgetItem, QMenu, QGridLayout, QComboBox, QLineEdit, QDialog, QVBoxLayout, QHeaderView,
+    QCheckBox, QTabWidget, QWidget, QLabel, QPushButton, QHBoxLayout
+)
 from PyQt6.QtGui import QIntValidator
 
 from electrum.i18n import _
 from electrum import blockchain
 from electrum.interface import ServerAddr, PREFERRED_NETWORK_PROTOCOL
-from electrum.network import Network
+from electrum.network import Network, ProxySettings, is_valid_host, is_valid_port
 from electrum.logging import get_logger
-from electrum.util import detect_tor_socks_proxy
-from electrum.simple_config import SimpleConfig
 
-from .util import (Buttons, CloseButton, HelpButton, read_QIcon, char_width_in_lineedit,
-                   PasswordLineEdit)
-from .util import QtEventListener, qt_event_listener
+from .util import (
+    Buttons, CloseButton, HelpButton, read_QIcon, char_width_in_lineedit, PasswordLineEdit, QtEventListener,
+    qt_event_listener, Spinner
+)
 
 
 _logger = get_logger(__name__)
@@ -52,32 +51,23 @@
 
 
 class NetworkDialog(QDialog, QtEventListener):
-    def __init__(self, *, network: Network, config: 'SimpleConfig'):
+    def __init__(self, *, network: Network):
         QDialog.__init__(self)
         self.setWindowTitle(_('Network'))
         self.setMinimumSize(500, 500)
-        self.nlayout = NetworkChoiceLayout(network, config)
+        self.tabs = tabs = QTabWidget()
+        self._blockchain_tab = blockchain_tab = ServerWidget(network)
+        self._proxy_tab = proxy_tab = ProxyWidget(network)
+        tabs.addTab(blockchain_tab, _('Overview'))
+        tabs.addTab(proxy_tab, _('Proxy'))
+
         vbox = QVBoxLayout(self)
-        vbox.addLayout(self.nlayout.layout())
+        vbox.addWidget(self.tabs)
         vbox.addLayout(Buttons(CloseButton(self)))
-        self.register_callbacks()
-        self._cleaned_up = False
 
-    def show(self):
+    def show(self, *, proxy_tab: bool = False):
         super().show()
-        if td := self.nlayout.td:
-            td.trigger_rescan()
-
-    @qt_event_listener
-    def on_event_network_updated(self):
-        self.nlayout.update()
-
-    def clean_up(self):
-        if self._cleaned_up:
-            return
-        self._cleaned_up = True
-        self.nlayout.clean_up()
-        self.unregister_callbacks()
+        self.tabs.setCurrentWidget(self._proxy_tab if proxy_tab else self._blockchain_tab)
 
 
 class NodesListWidget(QTreeWidget):
@@ -227,375 +217,172 @@ def update(self):
         super().update()
 
 
-class NetworkChoiceLayout(object):
-    # TODO consolidate to ProxyWidget+ServerWidget
-    # TODO TorDetector is unnecessary, Network tests socks5 peer and detects Tor
-    # TODO apply on editingFinished is not ideal, separate Apply button and on Close?
-    def __init__(self, network: Network, config: 'SimpleConfig', wizard=False):
-        self.network = network
-        self.config = config
-        self.tor_proxy = None
+class ProxyWidget(QWidget):
+    PROXY_MODES = {
+        'socks4': 'SOCKS4',
+        'socks5': 'SOCKS5/TOR'
+    }
 
-        self.tabs = tabs = QTabWidget()
-        self._proxy_tab = proxy_tab = QWidget()
-        blockchain_tab = QWidget()
-        tabs.addTab(blockchain_tab, _('Overview'))
-        tabs.addTab(proxy_tab, _('Proxy'))
-        tabs.currentChanged.connect(self._on_tab_changed)
+    torProbeFinished = pyqtSignal([str, int], arguments=['host', 'port'])
 
-        fixed_width_hostname = 24 * char_width_in_lineedit()
-        fixed_width_port = 6 * char_width_in_lineedit()
+    def __init__(self, network: Network, parent=None):
+        super().__init__(parent)
+        self.network = network
+        self.config = network.config
 
-        # Proxy tab
-        grid = QGridLayout(proxy_tab)
-        grid.setSpacing(8)
+        fixed_width_port = 6 * char_width_in_lineedit()
 
-        # proxy setting
+        # proxy setting.
         self.proxy_cb = QCheckBox(_('Use proxy'))
-        self.proxy_cb.clicked.connect(self.check_disable_proxy)
-        self.proxy_cb.clicked.connect(self.set_proxy)
-
         self.proxy_mode = QComboBox()
-        self.proxy_mode.addItems(['SOCKS4', 'SOCKS5'])
+        for k, v in self.PROXY_MODES.items():
+            self.proxy_mode.addItem(v, k)
+        self.proxy_mode.setCurrentIndex(1)
         self.proxy_host = QLineEdit()
-        self.proxy_host.setFixedWidth(fixed_width_hostname)
         self.proxy_port = QLineEdit()
         self.proxy_port.setFixedWidth(fixed_width_port)
         self.proxy_port_validator = QIntValidator(1, 65535)
         self.proxy_port.setValidator(self.proxy_port_validator)
 
         self.proxy_user = QLineEdit()
-        self.proxy_user.setPlaceholderText(_("Proxy user"))
+        self.proxy_user.setPlaceholderText(_("Proxy username"))
         self.proxy_password = PasswordLineEdit()
-        self.proxy_password.setPlaceholderText(_("Password"))
-        self.proxy_password.setFixedWidth(fixed_width_port)
-
-        self.proxy_mode.currentIndexChanged.connect(self.set_proxy)
-        self.proxy_host.editingFinished.connect(self.set_proxy)
-        self.proxy_port.editingFinished.connect(self.set_proxy)
-        self.proxy_user.editingFinished.connect(self.set_proxy)
-        self.proxy_password.editingFinished.connect(self.set_proxy)
-
-        self.proxy_mode.currentIndexChanged.connect(self.proxy_settings_changed)
-        self.proxy_host.textEdited.connect(self.proxy_settings_changed)
-        self.proxy_port.textEdited.connect(self.proxy_settings_changed)
-        self.proxy_user.textEdited.connect(self.proxy_settings_changed)
-        self.proxy_password.textEdited.connect(self.proxy_settings_changed)
-
-        self.tor_cb = QCheckBox(_("Use Tor Proxy"))
-        self.tor_cb.setIcon(read_QIcon("tor_logo.png"))
-        self.tor_cb.hide()
-        self.tor_cb.clicked.connect(self.use_tor_proxy)
-
-        grid.addWidget(self.tor_cb, 1, 0, 1, 3)
-        grid.addWidget(self.proxy_cb, 2, 0, 1, 3)
-        grid.addWidget(HelpButton(_('Proxy settings apply to all connections: with Electrum servers, but also with third-party services.')), 2, 4)
-        grid.addWidget(self.proxy_mode, 4, 1)
-        grid.addWidget(self.proxy_host, 4, 2)
-        grid.addWidget(self.proxy_port, 4, 3)
-        grid.addWidget(self.proxy_user, 5, 2)
-        grid.addWidget(self.proxy_password, 5, 3)
-        grid.setRowStretch(7, 1)
-
-        # Blockchain Tab
-        grid = QGridLayout(blockchain_tab)
-        msg =  ' '.join([
-            _("Electrum connects to several nodes in order to download block headers and find out the longest blockchain."),
-            _("This blockchain is used to verify the transactions sent by your transaction server.")
-        ])
-        self.status_label = QLabel('')
-        grid.addWidget(QLabel(_('Status') + ':'), 0, 0)
-        grid.addWidget(self.status_label, 0, 1, 1, 3)
-        grid.addWidget(HelpButton(msg), 0, 4)
-
-        self.autoconnect_cb = QCheckBox(_('Select server automatically'))
-        self.autoconnect_cb.setEnabled(self.config.cv.NETWORK_AUTO_CONNECT.is_modifiable())
-        self.autoconnect_cb.clicked.connect(self.set_server)
-        self.autoconnect_cb.clicked.connect(self.update)
-        msg = ' '.join([
-            _("If auto-connect is enabled, Electrum will always use a server that is on the longest blockchain."),
-            _("If it is disabled, you have to choose a server you want to use. Electrum will warn you if your server is lagging.")
-        ])
-        grid.addWidget(self.autoconnect_cb, 1, 0, 1, 3)
-        grid.addWidget(HelpButton(msg), 1, 4)
-
-        self.server_e = QLineEdit()
-        self.server_e.setFixedWidth(fixed_width_hostname + fixed_width_port)
-        self.server_e.editingFinished.connect(self.set_server)
-        msg = _("Electrum sends your wallet addresses to a single server, in order to receive your transaction history.")
-        grid.addWidget(QLabel(_('Server') + ':'), 2, 0)
-        grid.addWidget(self.server_e, 2, 1, 1, 3)
-        grid.addWidget(HelpButton(msg), 2, 4)
-
-        self.height_label = QLabel('')
-        msg = _('This is the height of your local copy of the blockchain.')
-        grid.addWidget(QLabel(_('Blockchain') + ':'), 3, 0)
-        grid.addWidget(self.height_label, 3, 1)
-        grid.addWidget(HelpButton(msg), 3, 4)
+        self.proxy_password.setPlaceholderText(_("Proxy password"))
 
-        self.split_label = QLabel('')
-        grid.addWidget(self.split_label, 4, 0, 1, 3)
-
-        self.nodes_list_widget = NodesListWidget(network=self.network)
-        self.nodes_list_widget.followServer.connect(self.follow_server)
-        self.nodes_list_widget.followChain.connect(self.follow_branch)
-
-        def do_set_server(server):
-            self.server_e.setText(server)
-            self.set_server()
-        self.nodes_list_widget.setServer.connect(do_set_server)
-        grid.addWidget(self.nodes_list_widget, 6, 0, 1, 5)
-
-        vbox = QVBoxLayout()
-        vbox.addWidget(tabs)
-        self.layout_ = vbox
-        # tor detector
-        self.td = td = TorDetector()
-        td.found_proxy.connect(self.suggest_proxy)
-        td.start()
+        grid = QGridLayout(self)
+        grid.setSpacing(8)
 
-        self.fill_in_proxy_settings()
+        grid.addWidget(self.proxy_cb, 0, 0, 1, 4)
+        proxy_helpbutton = HelpButton(
+            _('Proxy settings apply to all connections: with Electrum servers, but also with third-party services.'))
+        grid.addWidget(proxy_helpbutton, 0, 4, alignment=Qt.AlignmentFlag.AlignRight)
+        grid.addWidget(self.proxy_mode, 1, 0, 1, 1)
+        grid.addWidget(self.proxy_host, 1, 1, 1, 3)
+        grid.addWidget(self.proxy_port, 1, 4, 1, 1)
+        grid.addWidget(self.proxy_user, 2, 1, 1, 2)
+        grid.addWidget(self.proxy_password, 2, 3, 1, 2)
+
+        detect_l = QHBoxLayout()
+        self.detect_button = QPushButton(_('Detect Tor proxy'))
+        self.spinner = Spinner()
+        self.spinner.setMargin(5)
+        detect_l.addWidget(self.detect_button)
+        detect_l.addWidget(self.spinner)
+
+        grid.addLayout(detect_l, 3, 0, 1, 5, alignment=Qt.AlignmentFlag.AlignLeft)
+
+        spacer = QVBoxLayout()
+        spacer.addStretch(1)
+        grid.addLayout(spacer, 4, 0, 1, 5)
+
+        self.update_from_config()
         self.update()
 
-    def clean_up(self):
-        if self.td:
-            self.td.found_proxy.disconnect()
-            self.td.stop()
-            self.td = None
+        # connect signal handlers after init from config
+        self.proxy_cb.stateChanged.connect(self.on_proxy_enable_toggle)
+        self.proxy_mode.currentIndexChanged.connect(self.on_proxy_settings_changed)
+        self.proxy_host.editingFinished.connect(self.on_proxy_settings_changed)
+        self.proxy_port.editingFinished.connect(self.on_proxy_settings_changed)
+        self.proxy_user.editingFinished.connect(self.on_proxy_settings_changed)
+        self.proxy_password.editingFinished.connect(self.on_proxy_settings_changed)
+        self.detect_button.clicked.connect(self.detect_tor)
 
-    def check_disable_proxy(self, b):
-        if not self.config.cv.NETWORK_PROXY.is_modifiable():
-            b = False
-        for w in [self.proxy_mode, self.proxy_host, self.proxy_port, self.proxy_user, self.proxy_password]:
-            w.setEnabled(b)
-
-    def enable_set_server(self):
-        if self.config.cv.NETWORK_SERVER.is_modifiable():
-            enabled = not self.autoconnect_cb.isChecked()
-            self.server_e.setEnabled(enabled)
-        else:
-            for w in [self.autoconnect_cb, self.server_e, self.nodes_list_widget]:
-                w.setEnabled(False)
+        self.torProbeFinished.connect(self.on_tor_probe_finished)
 
     def update(self):
-        net_params = self.network.get_parameters()
-        server = net_params.server
-        auto_connect = net_params.auto_connect
-        if not self.server_e.hasFocus():
-            self.server_e.setText(server.to_friendly_name())
-        self.autoconnect_cb.setChecked(auto_connect)
-
-        height_str = "%d "%(self.network.get_local_height()) + _('blocks')
-        self.height_label.setText(height_str)
-        self.status_label.setText(self.network.get_status())
-        chains = self.network.get_blockchains()
-        if len(chains) > 1:
-            chain = self.network.blockchain()
-            forkpoint = chain.get_max_forkpoint()
-            name = chain.get_name()
-            msg = _('Chain split detected at block {0}').format(forkpoint) + '\n'
-            msg += (_('You are following branch') if auto_connect else _('Your server is on branch'))+ ' ' + name
-            msg += ' (%d %s)' % (chain.get_branch_size(), _('blocks'))
-        else:
-            msg = ''
-        self.split_label.setText(msg)
-        self.nodes_list_widget.update()
-        self.enable_set_server()
-
-    def fill_in_proxy_settings(self):
-        proxy_config = self.network.get_parameters().proxy
-        if not proxy_config:
-            proxy_config = {"mode": "none", "host": "localhost", "port": "9050"}
-
-        b = proxy_config.get('mode') != "none"
-        self.check_disable_proxy(b)
-        if b:
-            self.proxy_cb.setChecked(True)
-            self.proxy_mode.setCurrentIndex(
-                self.proxy_mode.findText(str(proxy_config.get("mode").upper())))
-
-        self.proxy_host.setText(proxy_config.get("host"))
-        self.proxy_port.setText(proxy_config.get("port"))
-        self.proxy_user.setText(proxy_config.get("user", ""))
-        self.proxy_password.setText(proxy_config.get("password", ""))
-
-    def layout(self):
-        return self.layout_
-
-    def follow_branch(self, chain_id):
-        self.network.run_from_another_thread(self.network.follow_chain_given_id(chain_id))
-        self.update()
-
-    def follow_server(self, server: ServerAddr):
-        self.network.run_from_another_thread(self.network.follow_chain_given_server(server))
-        self.update()
-
-    def accept(self):
-        pass
+        enabled = self.proxy_cb.isChecked() and self.config.cv.NETWORK_PROXY.is_modifiable()
+        for item in [
+                self.proxy_mode, self.proxy_host, self.proxy_port, self.proxy_user, self.proxy_password,
+                self.detect_button
+        ]:
+            item.setEnabled(enabled)
+
+        if not self.proxy_port.hasAcceptableInput() and not is_valid_port(self.proxy_port.text()):
+            return
 
-    def set_server(self):
-        net_params = self.network.get_parameters()
-        try:
-            server = ServerAddr.from_str_with_inference(str(self.server_e.text()))
-            if not server: raise Exception("failed to parse")
-        except Exception:
+        if not is_valid_host(self.proxy_host.text()):
             return
-        net_params = net_params._replace(server=server,
-                                         auto_connect=self.autoconnect_cb.isChecked())
-        self.network.run_from_another_thread(self.network.set_parameters(net_params))
 
-    def set_proxy(self):
         net_params = self.network.get_parameters()
-        if self.proxy_cb.isChecked():
-            if not self.proxy_port.hasAcceptableInput():
-                return
-            proxy = {'mode':str(self.proxy_mode.currentText()).lower(),
-                     'host':str(self.proxy_host.text()),
-                     'port':str(self.proxy_port.text()),
-                     'user':str(self.proxy_user.text()),
-                     'password':str(self.proxy_password.text())}
-        else:
-            proxy = None
-            self.tor_cb.setChecked(False)
+        proxy = self.get_proxy_settings()
         net_params = net_params._replace(proxy=proxy)
         self.network.run_from_another_thread(self.network.set_parameters(net_params))
 
-    def _on_tab_changed(self):
-        if self.tabs.currentWidget() is self._proxy_tab:
-            self.td.trigger_rescan()
+    def update_from_config(self):
+        proxy = ProxySettings.from_config(self.config)
+        self.proxy_cb.setChecked(proxy.enabled)
+        self.proxy_mode.setCurrentText(self.PROXY_MODES.get(proxy.mode))
+        self.proxy_host.setText(proxy.host)
+        self.proxy_port.setText(proxy.port)
+        self.proxy_user.setText(proxy.user)
+        self.proxy_password.setText(proxy.password)
 
-    def suggest_proxy(self, found_proxy):
-        if found_proxy is None:
-            self.tor_cb.hide()
-            return
-        self.tor_proxy = found_proxy
-        self.tor_cb.setText(_("Use Tor proxy at port {}").format(str(found_proxy[1])))
-        if (self.proxy_cb.isChecked()
-                and self.proxy_mode.currentIndex() == self.proxy_mode.findText('SOCKS5')
-                and self.proxy_host.text() == "127.0.0.1"
-                and self.proxy_port.text() == str(found_proxy[1])):
-            self.tor_cb.setChecked(True)
-        self.tor_cb.show()
-
-    def use_tor_proxy(self, use_it):
-        if not use_it:
-            self.proxy_cb.setChecked(False)
-        else:
-            socks5_mode_index = self.proxy_mode.findText('SOCKS5')
-            if socks5_mode_index == -1:
-                _logger.info("can't find proxy_mode 'SOCKS5'")
-                return
-            self.proxy_mode.setCurrentIndex(socks5_mode_index)
-            self.proxy_host.setText("127.0.0.1")
-            self.proxy_port.setText(str(self.tor_proxy[1]))
-            self.proxy_user.setText("")
-            self.proxy_password.setText("")
-            self.tor_cb.setChecked(True)
-            self.proxy_cb.setChecked(True)
-        self.check_disable_proxy(use_it)
-        self.set_proxy()
-
-    def proxy_settings_changed(self):
-        self.tor_cb.setChecked(False)
-
-
-class TorDetector(QThread):
-    found_proxy = pyqtSignal(object)
-
-    def __init__(self):
-        QThread.__init__(self)
-        self._work_to_do_evt = threading.Event()
-        self._stopping = False
-
-    def run(self):
-        while True:
-            # do rescan
-            net_addr = detect_tor_socks_proxy()
-            self.found_proxy.emit(net_addr)
-            # wait until triggered
-            self._work_to_do_evt.wait()
-            self._work_to_do_evt.clear()
-            if self._stopping:
-                return
-
-    def trigger_rescan(self) -> None:
-        self._work_to_do_evt.set()
-
-    def stop(self):
-        self._stopping = True
-        self._work_to_do_evt.set()
-        self.exit()
-        self.wait()
-
-
-class ProxyWidget(QWidget):
-    def __init__(self, parent=None):
-        super().__init__(parent)
+        if not self.config.cv.NETWORK_PROXY.is_modifiable():
+            for w in [
+                    self.proxy_cb, self.proxy_mode, self.proxy_host, self.proxy_port,
+                    self.proxy_user, self.proxy_password, self.detect_button
+            ]:
+                w.setEnabled(False)
 
-        fixed_width_hostname = 24 * char_width_in_lineedit()
-        fixed_width_port = 6 * char_width_in_lineedit()
+    def on_proxy_enable_toggle(self):
+        # probe if enabled and no pre-existing settings
+        # if self.proxy_cb.isChecked() and (not self.proxy_host.text() or not self.proxy_port.text()):
+        #     self.detect_tor()
+        self.update()
 
-        grid = QGridLayout(self)
-        grid.setSpacing(8)
+    def on_proxy_settings_changed(self):
+        self.update()
 
-        # proxy setting.
-        self.proxy_cb = QCheckBox(_('Use proxy'))
-        self.proxy_mode = QComboBox()
-        self.proxy_mode.addItems(['SOCKS4', 'SOCKS5'])
-        self.proxy_mode.setCurrentIndex(1)
-        self.proxy_host = QLineEdit()
-        self.proxy_host.setFixedWidth(fixed_width_hostname)
-        self.proxy_port = QLineEdit()
-        self.proxy_port.setFixedWidth(fixed_width_port)
-        self.proxy_user = QLineEdit()
-        self.proxy_user.setPlaceholderText(_("Proxy user"))
-        self.proxy_password = PasswordLineEdit()
-        self.proxy_password.setPlaceholderText(_("Password"))
-        self.proxy_password.setFixedWidth(fixed_width_port)
-
-        grid.addWidget(self.proxy_cb, 0, 0, 1, 3)
-        grid.addWidget(HelpButton(_('Proxy settings apply to all connections: with Electrum servers, but also with third-party services.')), 0, 4)
-        grid.addWidget(self.proxy_mode, 1, 1)
-        grid.addWidget(self.proxy_host, 1, 2)
-        grid.addWidget(self.proxy_port, 1, 3)
-        grid.addWidget(self.proxy_user, 2, 2)
-        grid.addWidget(self.proxy_password, 2, 3)
-
-    def get_proxy_settings(self):
-        return {
-            'enabled': self.proxy_cb.isChecked(),
-            'mode': ['socks4', 'socks5'][self.proxy_mode.currentIndex()],
-            'host': self.proxy_host.text(),
-            'port': self.proxy_port.text(),
-            'user': self.proxy_user.text(),
-            'password': self.proxy_password.text()
-        }
+    def get_proxy_settings(self) -> ProxySettings:
+        proxy = ProxySettings()
+        proxy.enabled = self.proxy_cb.isChecked()
+        proxy.mode = self.proxy_mode.currentData()
+        proxy.host = self.proxy_host.text()
+        proxy.port = self.proxy_port.text()
+        proxy.user = self.proxy_user.text()
+        proxy.password = self.proxy_password.text()
+        return proxy
+
+    def detect_tor(self):
+        self.detect_button.setEnabled(False)
+        self.spinner.setVisible(True)
+        ProxySettings.probe_tor(self.torProbeFinished.emit)  # via signal
+
+    @pyqtSlot(str, int)
+    def on_tor_probe_finished(self, host: str, port: int):
+        self.detect_button.setEnabled(True)
+        self.spinner.setVisible(False)
+        if host:
+            self.proxy_mode.setCurrentIndex(1)
+            self.proxy_host.setText(host)
+            self.proxy_port.setText(str(port))
+            self.update()
 
 
 class ServerWidget(QWidget, QtEventListener):
-    def __init__(self, network, parent=None):
+    def __init__(self, network: Network, parent=None):
         super().__init__(parent)
         self.network = network
         self.config = network.config
 
-        fixed_width_hostname = 24 * char_width_in_lineedit()
-        fixed_width_port = 6 * char_width_in_lineedit()
-
         self.setLayout(QVBoxLayout())
 
-        grid = QGridLayout(self)
+        grid = QGridLayout()
 
         msg = ' '.join([
             _("Electrum connects to several nodes in order to download block headers and find out the longest blockchain."),
             _("This blockchain is used to verify the transactions sent by your transaction server.")
         ])
+        self.status_label_header = QLabel(_('Status') + ':')
         self.status_label = QLabel('')
-        grid.addWidget(QLabel(_('Status') + ':'), 0, 0)
+        self.status_label_helpbutton = HelpButton(msg)
+        grid.addWidget(self.status_label_header, 0, 0)
         grid.addWidget(self.status_label, 0, 1, 1, 3)
-        grid.addWidget(HelpButton(msg), 0, 4)
+        grid.addWidget(self.status_label_helpbutton, 0, 4)
 
         self.autoconnect_cb = QCheckBox(_('Select server automatically'))
-        self.autoconnect_cb.setEnabled(self.config.cv.NETWORK_AUTO_CONNECT.is_modifiable())
+        self.autoconnect_cb.stateChanged.connect(self.on_server_settings_changed)
+
         msg = ' '.join([
             _("If auto-connect is enabled, Electrum will always use a server that is on the longest blockchain."),
             _("If it is disabled, you have to choose a server you want to use. Electrum will warn you if your server is lagging.")
@@ -604,17 +391,19 @@ def __init__(self, network, parent=None):
         grid.addWidget(HelpButton(msg), 1, 4)
 
         self.server_e = QLineEdit()
-        self.server_e.setFixedWidth(fixed_width_hostname + fixed_width_port)
+        self.server_e.editingFinished.connect(self.on_server_settings_changed)
         msg = _("Electrum sends your wallet addresses to a single server, in order to receive your transaction history.")
         grid.addWidget(QLabel(_('Server') + ':'), 2, 0)
         grid.addWidget(self.server_e, 2, 1, 1, 3)
         grid.addWidget(HelpButton(msg), 2, 4)
 
-        self.height_label = QLabel('')
         msg = _('This is the height of your local copy of the blockchain.')
-        grid.addWidget(QLabel(_('Blockchain') + ':'), 3, 0)
+        self.height_label_header = QLabel(_('Blockchain') + ':')
+        self.height_label = QLabel('')
+        self.height_label_helpbutton = HelpButton(msg)
+        grid.addWidget(self.height_label_header, 3, 0)
         grid.addWidget(self.height_label, 3, 1)
-        grid.addWidget(HelpButton(msg), 3, 4)
+        grid.addWidget(self.height_label_helpbutton, 3, 4)
 
         self.split_label = QLabel('')
         grid.addWidget(self.split_label, 4, 0, 1, 3)
@@ -636,9 +425,62 @@ def do_set_server(server):
         self.register_callbacks()
         self.destroyed.connect(lambda: self.unregister_callbacks())
 
+        self.update_from_config()
+        self.update()
+
     @qt_event_listener
     def on_event_network_updated(self):
-        self.nodes_list_widget.update()
+        self.nodes_list_widget.update()  # NOTE: move event handling to widget itself?
+        self.update()
+
+    def on_server_settings_changed(self):
+        if not self.network._was_started:
+            self.update()
+            return
+        auto_connect = self.autoconnect_cb.isChecked()
+        server = self.server_e.text().strip()
+        net_params = self.network.get_parameters()
+        if server != net_params.server or auto_connect != net_params.auto_connect:
+            self.set_server()
+
+    def update(self):
+        auto_connect = self.autoconnect_cb.isChecked()
+        self.server_e.setEnabled(self.config.cv.NETWORK_SERVER.is_modifiable() and not auto_connect)
+
+        for item in [
+                self.status_label_header, self.status_label, self.status_label_helpbutton,
+                self.height_label_header, self.height_label, self.height_label_helpbutton]:
+            item.setVisible(self.network._was_started)
+
+        msg = ''
+        if self.network._was_started:
+            # Network was started, so we don't run in initial setup wizard.
+            # behavior in this case is to apply changes immediately.
+            # Also, we show block height and potential chain tips
+            height_str = _('{} blocks').format(self.network.get_local_height())
+            self.height_label.setText(height_str)
+            self.status_label.setText(self.network.get_status())
+            chains = self.network.get_blockchains()
+            if len(chains) > 1:
+                chain = self.network.blockchain()
+                forkpoint = chain.get_max_forkpoint()
+                name = chain.get_name()
+                msg = _('Chain split detected at block {0}').format(forkpoint) + '\n'
+                if auto_connect:
+                    msg += _('You are following branch {}').format(name)
+                else:
+                    msg += _('Your server is on branch {0} ({1} blocks)').format(name, chain.get_branch_size())
+        self.split_label.setText(msg)
+
+    def update_from_config(self):
+        auto_connect = self.config.NETWORK_AUTO_CONNECT
+        self.autoconnect_cb.setChecked(auto_connect)
+        server = self.config.NETWORK_SERVER
+        self.server_e.setText(server)
+
+        self.autoconnect_cb.setEnabled(self.config.cv.NETWORK_AUTO_CONNECT.is_modifiable())
+        self.server_e.setEnabled(self.config.cv.NETWORK_SERVER.is_modifiable() and not auto_connect)
+        self.nodes_list_widget.setEnabled(self.config.cv.NETWORK_SERVER.is_modifiable())
 
     def follow_branch(self, chain_id):
         self.network.run_from_another_thread(self.network.follow_chain_given_id(chain_id))
diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py
index ab2cd754115c..093b76b1277e 100644
--- a/electrum/gui/qt/send_tab.py
+++ b/electrum/gui/qt/send_tab.py
@@ -25,7 +25,7 @@
 from .amountedit import AmountEdit, BTCAmountEdit, SizedFreezableLineEdit
 from .paytoedit import InvalidPaymentIdentifier
 from .util import (WaitingDialog, HelpLabel, MessageBoxMixin, EnterButton, char_width_in_lineedit,
-                   get_iconname_camera, read_QIcon, ColorScheme, icon_path, IconLabel)
+                   get_iconname_camera, read_QIcon, ColorScheme, icon_path, IconLabel, Spinner)
 from .invoice_list import InvoiceList
 
 if TYPE_CHECKING:
@@ -142,14 +142,8 @@ def __init__(self, window: 'ElectrumWindow'):
         self.paste_button.setToolTip(_('Paste invoice from clipboard'))
         self.paste_button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
 
-        self.spinner = QMovie(icon_path('spinner.gif'))
-        self.spinner.setScaledSize(QSize(24, 24))
-        self.spinner.setBackgroundColor(QColor('black'))
-        self.spinner_l = QLabel()
-        self.spinner_l.setMargin(5)
-        self.spinner_l.setVisible(False)
-        self.spinner_l.setMovie(self.spinner)
-        grid.addWidget(self.spinner_l, 0, 1, 1, 4, Qt.AlignmentFlag.AlignRight)
+        self.spinner = Spinner()
+        grid.addWidget(self.spinner, 0, 1, 1, 4, Qt.AlignmentFlag.AlignRight)
 
         self.save_button = EnterButton(_("Save"), self.do_save_invoice)
         self.save_button.setEnabled(False)
@@ -216,13 +210,6 @@ def reset_max(text):
 
         self.setTabOrder(self.send_button, self.invoice_list)
 
-    def showSpinner(self, b):
-        self.spinner_l.setVisible(b)
-        if b:
-            self.spinner.start()
-        else:
-            self.spinner.stop()
-
     def on_amount_changed(self, text):
         # FIXME: implement full valid amount check to enable/disable Pay button
         pi = self.payto_e.payment_identifier
@@ -388,7 +375,7 @@ def do_clear(self):
     def prepare_for_send_tab_network_lookup(self):
         for btn in [self.save_button, self.send_button, self.clear_button]:
             btn.setEnabled(False)
-        self.showSpinner(True)
+        self.spinner.setVisible(True)
 
     def payment_request_error(self, error):
         self.show_message(error)
@@ -499,7 +486,7 @@ def on_resolve_done(self, pi: 'PaymentIdentifier'):
         # TODO: resolve can happen while typing, we don't want message dialogs to pop up
         # currently we don't set error for emaillike recipients to avoid just that
         self.logger.debug('payment identifier resolve done')
-        self.showSpinner(False)
+        self.spinner.setVisible(False)
         if pi.error:
             self.show_error(pi.error)
             self.do_clear()
@@ -555,7 +542,7 @@ def get_amount(self) -> int:
         return self.amount_e.get_amount() or 0
 
     def on_finalize_done(self, pi: PaymentIdentifier):
-        self.showSpinner(False)
+        self.spinner.setVisible(False)
         self.update_fields()
         if pi.error:
             self.show_error(pi.error)
diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py
index 1e9fc6fb50d8..273f73805ec3 100644
--- a/electrum/gui/qt/util.py
+++ b/electrum/gui/qt/util.py
@@ -11,7 +11,7 @@
 
 from PyQt6 import QtCore
 from PyQt6.QtGui import (QFont, QColor, QCursor, QPixmap, QImage,
-                         QPalette, QIcon, QFontMetrics, QPainter, QContextMenuEvent)
+                         QPalette, QIcon, QFontMetrics, QPainter, QContextMenuEvent, QMovie)
 from PyQt6.QtCore import (Qt, pyqtSignal, QCoreApplication, QThread, QSize, QRect, QPoint, QObject)
 from PyQt6.QtWidgets import (QPushButton, QLabel, QMessageBox, QHBoxLayout, QVBoxLayout, QLineEdit,
                              QStyle, QDialog, QGroupBox, QButtonGroup, QRadioButton,
@@ -117,6 +117,22 @@ def __init__(self, *args, **kwargs):
         self.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
 
 
+class Spinner(QLabel):
+    def __init__(self, *args, **kwargs):
+        QLabel.__init__(self, *args, **kwargs)
+        self.spinner = QMovie(icon_path('spinner.gif'))
+        self.spinner.setScaledSize(QSize(20, 20))
+        self.spinner.frameChanged.connect(lambda: self.setPixmap(self.spinner.currentPixmap()))
+        self.setVisible(False)
+
+    def setVisible(self, visible):
+        if visible:
+            self.spinner.start()
+        else:
+            self.spinner.stop()
+        super().setVisible(visible)
+
+
 class HelpMixin:
     def __init__(self, help_text: str, *, help_title: str = None):
         assert isinstance(self, QWidget), "HelpMixin must be a QWidget instance!"
diff --git a/electrum/gui/qt/wizard/server_connect.py b/electrum/gui/qt/wizard/server_connect.py
index b4489d7e0b44..ff8412e6cb61 100644
--- a/electrum/gui/qt/wizard/server_connect.py
+++ b/electrum/gui/qt/wizard/server_connect.py
@@ -88,16 +88,15 @@ def apply(self):
 class WCProxyConfig(WizardComponent):
     def __init__(self, parent, wizard):
         WizardComponent.__init__(self, parent, wizard, title=_('Proxy'))
-        self.pw = ProxyWidget(self)
+        self.pw = ProxyWidget(wizard._daemon.network, self)
         self.pw.proxy_cb.setChecked(True)
         self.pw.proxy_host.setText('localhost')
         self.pw.proxy_port.setText('9050')
         self.layout().addWidget(self.pw)
-        self.layout().addStretch(1)
         self._valid = True
 
     def apply(self):
-        self.wizard_data['proxy'] = self.pw.get_proxy_settings()
+        self.wizard_data['proxy'] = self.pw.get_proxy_settings().to_dict()
 
 
 class WCServerConfig(WizardComponent):
diff --git a/electrum/gui/text.py b/electrum/gui/text.py
index 5eeef882872f..c1c52745d808 100644
--- a/electrum/gui/text.py
+++ b/electrum/gui/text.py
@@ -23,7 +23,7 @@
 from electrum.wallet import Wallet, Abstract_Wallet
 from electrum.wallet_db import WalletDB
 from electrum.storage import WalletStorage
-from electrum.network import NetworkParameters, TxBroadcastError, BestEffortRequestFailed
+from electrum.network import NetworkParameters, TxBroadcastError, BestEffortRequestFailed, ProxySettings
 from electrum.interface import ServerAddr
 from electrum.invoices import Invoice
 
@@ -753,10 +753,14 @@ def network_dialog(self):
                         self.show_message("Error:" + server_str + "\nIn doubt, type \"auto-connect\"")
                         return False
             if out.get('server') or out.get('proxy') or out.get('proxy user') or out.get('proxy pass'):
-                new_proxy_config = electrum.network.deserialize_proxy(out.get('proxy')) if out.get('proxy') else proxy_config
-                if new_proxy_config:
-                    new_proxy_config['user'] = out.get('proxy user') if 'proxy user' in out else proxy_config['user']
-                    new_proxy_config['pass'] = out.get('proxy pass') if 'proxy pass' in out else proxy_config['pass']
+                if out.get('proxy'):
+                    new_proxy_config = ProxySettings()
+                    new_proxy_config.deserialize_proxy_cfgstr(out.get('proxy'))
+                    new_proxy_config.user = out.get('proxy user', proxy_config.user)
+                    new_proxy_config.password = out.get('proxy pass', proxy_config.password)
+                    new_proxy_config.enabled = True
+                else:
+                    new_proxy_config = proxy_config
                 net_params = NetworkParameters(
                     server=server_addr,
                     proxy=new_proxy_config,
diff --git a/electrum/network.py b/electrum/network.py
index 48b824e82653..22880bac384c 100644
--- a/electrum/network.py
+++ b/electrum/network.py
@@ -31,7 +31,10 @@
 import socket
 import json
 import sys
-from typing import NamedTuple, Optional, Sequence, List, Dict, Tuple, TYPE_CHECKING, Iterable, Set, Any, TypeVar
+from typing import (
+    NamedTuple, Optional, Sequence, List, Dict, Tuple, TYPE_CHECKING, Iterable, Set, Any, TypeVar,
+    Callable
+)
 import traceback
 import concurrent
 from concurrent import futures
@@ -48,7 +51,7 @@
 from .util import (log_exceptions, ignore_exceptions, OldTaskGroup,
                    bfh, make_aiohttp_session, send_exception_to_crash_reporter,
                    is_hash256_str, is_non_negative_integer, MyEncoder, NetworkRetryManager,
-                   error_text_str_to_safe_str)
+                   error_text_str_to_safe_str, detect_tor_socks_proxy)
 from .bitcoin import COIN, DummyAddress, DummyAddressUsedInTxException
 from . import constants
 from . import blockchain
@@ -156,65 +159,133 @@ def pick_random_server(hostmap=None, *, allowed_protocols: Iterable[str],
     return random.choice(eligible) if eligible else None
 
 
-class NetworkParameters(NamedTuple):
-    server: ServerAddr
-    proxy: Optional[dict]
-    auto_connect: bool
-    oneserver: bool = False
+def is_valid_port(ps: str):
+    try:
+        return 0 < int(ps) < 65535
+    except ValueError:
+        return False
 
 
-proxy_modes = ['socks4', 'socks5']
+def is_valid_host(ph: str):
+    try:
+        NetAddress(ph, '1')
+    except ValueError:
+        return False
+    return True
 
 
-def serialize_proxy(p):
-    if not isinstance(p, dict):
-        return None
-    return ':'.join([p.get('mode'), p.get('host'), p.get('port')])
+class ProxySettings:
+    MODES = ['socks4', 'socks5']
 
+    probe_fut = None
 
-def deserialize_proxy(s: Optional[str], user: str = None, password: str = None) -> Optional[dict]:
-    if not isinstance(s, str):
-        return None
-    if s.lower() == 'none':
-        return None
-    proxy = {"mode": "socks5", "host": "localhost"}
+    def __init__(self):
+        self.enabled = False
+        self.mode = 'socks5'
+        self.host = ''
+        self.port = ''
+        self.user = None
+        self.password = None
 
-    args = s.split(':')
-    if args[0] in proxy_modes:
-        proxy['mode'] = args[0]
-        args = args[1:]
+    def set_defaults(self):
+        self.__init__()  # call __init__ for default values
 
-    def is_valid_port(ps: str):
-        try:
-            return 0 < int(ps) < 65535
-        except ValueError:
-            return False
+    def serialize_proxy_cfgstr(self):
+        return ':'.join([self.mode, self.host, self.port])
 
-    def is_valid_host(ph: str):
-        try:
-            NetAddress(ph, '1')
-        except ValueError:
-            return False
-        return True
+    def deserialize_proxy_cfgstr(self, s: Optional[str], user: str = None, password: str = None) -> None:
+        if s is None or (isinstance(s, str) and s.lower() == 'none'):
+            self.set_defaults()
+            self.user = user
+            self.password = password
+            return
+
+        if not isinstance(s, str):
+            raise ValueError('proxy config not a string')
+
+        args = s.split(':')
+        if args[0] in ProxySettings.MODES:
+            self.mode = args[0]
+            args = args[1:]
+
+        # detect migrate from old settings
+        if len(args) == 4 and is_valid_host(args[0]) and is_valid_port(args[1]):  # host:port:user:pass,
+            self.host = args[0]
+            self.port = args[1]
+            self.user = args[2]
+            self.password = args[3]
+        else:
+            self.host = ':'.join(args[:-1])
+            self.port = args[-1]
+            self.user = user
+            self.password = password
+
+        if not is_valid_host(self.host) or not is_valid_port(self.port):
+            self.enabled = False
+
+    def to_dict(self):
+        return {
+            'enabled': self.enabled,
+            'mode': self.mode,
+            'host': self.host,
+            'port': self.port,
+            'user': self.user,
+            'password': self.password
+        }
 
-    # detect migrate from old settings
-    if len(args) == 4 and is_valid_host(args[0]) and is_valid_port(args[1]):  # host:port:user:pass,
-        proxy['host'] = args[0]
-        proxy['port'] = args[1]
-        proxy['user'] = args[2]
-        proxy['password'] = args[3]
+    @classmethod
+    def from_config(cls, config: 'SimpleConfig') -> 'ProxySettings':
+        proxy = ProxySettings()
+        proxy.deserialize_proxy_cfgstr(
+            config.NETWORK_PROXY, config.NETWORK_PROXY_USER, config.NETWORK_PROXY_PASSWORD
+        )
+        proxy.enabled = config.NETWORK_PROXY_ENABLED
         return proxy
 
-    proxy['host'] = ':'.join(args[:-1])
-    proxy['port'] = args[-1]
+    @classmethod
+    def from_dict(cls, d: dict) -> 'ProxySettings':
+        proxy = ProxySettings()
+        proxy.enabled = d.get('enabled', proxy.enabled)
+        proxy.mode = d.get('mode', proxy.mode)
+        proxy.host = d.get('host', proxy.host)
+        proxy.port = d.get('port', proxy.port)
+        proxy.user = d.get('user', proxy.user)
+        proxy.password = d.get('password', proxy.password)
+        return proxy
 
-    if not is_valid_host(proxy['host']) or not is_valid_port(proxy['port']):
-        return None
+    @classmethod
+    def probe_tor(cls, on_finished: Callable[[str | None, int | None], None]):
+        async def detect_task(finished: Callable[[str | None, int | None], None]):
+            net_addr = await detect_tor_socks_proxy()
+            if net_addr is None:
+                finished('', -1)
+            else:
+                host = net_addr[0]
+                port = net_addr[1]
+                finished(host, port)
+            cls.probe_fut = None
+
+        if cls.probe_fut:  # one probe at a time
+            return
+        cls.probe_fut = asyncio.run_coroutine_threadsafe(detect_task(on_finished), util.get_asyncio_loop())
 
-    proxy['user'] = user
-    proxy['password'] = password
+    def __eq__(self, other):
+        return self.enabled == other.enabled \
+            and self.mode == other.mode \
+            and self.host == other.host \
+            and self.port == other.port \
+            and self.user == other.user \
+            and self.password == other.password
 
-    return proxy
+    def __str__(self):
+        return f'{self.enabled=} {self.mode=} {self.host=} {self.port=} {self.user=}'
+
+
+class NetworkParameters(NamedTuple):
+    server: ServerAddr
+    proxy: ProxySettings
+    auto_connect: bool
+    oneserver: bool = False
 
 
 class BestEffortRequestFailed(NetworkException): pass
@@ -323,7 +394,7 @@ def __init__(self, config: 'SimpleConfig', *, daemon: 'Daemon' = None):
 
         self._allowed_protocols = {PREFERRED_NETWORK_PROTOCOL}
 
-        self.proxy = None  # type: Optional[dict]
+        self.proxy = ProxySettings()
         self.is_proxy_tor = None
         self._init_parameters_from_config()
 
@@ -364,7 +435,6 @@ def __init__(self, config: 'SimpleConfig', *, daemon: 'Daemon' = None):
         self._has_ever_managed_to_connect_to_server = False
         self._was_started = False
 
-
     def has_internet_connection(self) -> bool:
         """Our guess whether the device has Internet-connectivity."""
         return self._has_ever_managed_to_connect_to_server
@@ -513,8 +583,7 @@ def _init_parameters_from_config(self) -> None:
         dns_hacks.configure_dns_resolver()
         self.auto_connect = self.config.NETWORK_AUTO_CONNECT
         self._set_default_server()
-        self._set_proxy(deserialize_proxy(self.config.NETWORK_PROXY, self.config.NETWORK_PROXY_USER,
-                                          self.config.NETWORK_PROXY_PASSWORD))
+        self._set_proxy(ProxySettings.from_config(self.config))
         self._maybe_set_oneserver()
 
     def get_donation_address(self):
@@ -637,7 +706,7 @@ def _set_default_server(self) -> None:
             self.default_server = pick_random_server(allowed_protocols=self._allowed_protocols)
         assert isinstance(self.default_server, ServerAddr), f"invalid type for default_server: {self.default_server!r}"
 
-    def _set_proxy(self, proxy: Optional[dict]):
+    def _set_proxy(self, proxy: ProxySettings):
         if self.proxy == proxy:
             return
 
@@ -651,9 +720,9 @@ def _set_proxy(self, proxy: Optional[dict]):
         util.trigger_callback('proxy_set', self.proxy)
 
     def _detect_if_proxy_is_tor(self) -> None:
-        def tor_probe_task(p):
+        async def tor_probe_task(p):
             assert p is not None
-            is_tor = util.is_tor_socks_port(p['host'], int(p['port']))
+            is_tor = await util.is_tor_socks_port(p.host, int(p.port))
             if self.proxy == p:  # is this the proxy we probed?
                 if self.is_proxy_tor != is_tor:
                     self.logger.info(f'Proxy is {"" if is_tor else "not "}TOR')
@@ -661,32 +730,38 @@ def tor_probe_task(p):
                 util.trigger_callback('tor_probed', is_tor)
 
         proxy = self.proxy
-        if proxy and proxy['mode'] == 'socks5':
-            t = threading.Thread(target=tor_probe_task, args=(proxy,), daemon=True)
-            t.start()
+        if proxy and proxy.enabled and proxy.mode == 'socks5':
+            # FIXME GC issues? do we need to store the Future?
+            asyncio.run_coroutine_threadsafe(tor_probe_task(proxy), self.asyncio_loop)
 
     @log_exceptions
     async def set_parameters(self, net_params: NetworkParameters):
         proxy = net_params.proxy
-        proxy_str = serialize_proxy(proxy)
-        proxy_user = proxy['user'] if proxy else None
-        proxy_pass = proxy['password'] if proxy else None
+        proxy_str = proxy.serialize_proxy_cfgstr()
+        proxy_enabled = proxy.enabled
+        proxy_user = proxy.user
+        proxy_pass = proxy.password
         server = net_params.server
         # sanitize parameters
         try:
             if proxy:
-                proxy_modes.index(proxy['mode']) + 1
-                int(proxy['port'])
+                # proxy_modes.index(proxy['mode']) + 1
+                ProxySettings.MODES.index(proxy.mode) + 1
+                # int(proxy['port'])
+                int(proxy.port)
         except Exception:
-            return
+            proxy.enabled = False
+            # return
         self.config.NETWORK_AUTO_CONNECT = net_params.auto_connect
         self.config.NETWORK_ONESERVER = net_params.oneserver
+        self.config.NETWORK_PROXY_ENABLED = proxy_enabled
         self.config.NETWORK_PROXY = proxy_str
         self.config.NETWORK_PROXY_USER = proxy_user
         self.config.NETWORK_PROXY_PASSWORD = proxy_pass
         self.config.NETWORK_SERVER = str(server)
         # abort if changes were not allowed by config
         if self.config.NETWORK_SERVER != str(server) \
+                or self.config.NETWORK_PROXY_ENABLED != proxy_enabled \
                 or self.config.NETWORK_PROXY != proxy_str \
                 or self.config.NETWORK_PROXY_USER != proxy_user \
                 or self.config.NETWORK_PROXY_PASSWORD != proxy_pass \
@@ -863,7 +938,7 @@ def get_network_timeout_seconds(self, request_type=NetworkTimeout.Generic) -> in
             return self.config.NETWORK_TIMEOUT
         if self.oneserver and not self.auto_connect:
             return request_type.MOST_RELAXED
-        if self.proxy:
+        if self.proxy and self.proxy.enabled:
             return request_type.RELAXED
         return request_type.NORMAL
 
diff --git a/electrum/simple_config.py b/electrum/simple_config.py
index 26042b30b3a0..e5528b94152b 100644
--- a/electrum/simple_config.py
+++ b/electrum/simple_config.py
@@ -950,6 +950,7 @@ def __setattr__(self, name, value):
     NETWORK_PROXY = ConfigVar('proxy', default=None, type_=str, convert_getter=lambda v: "none" if v is None else v)
     NETWORK_PROXY_USER = ConfigVar('proxy_user', default=None, type_=str)
     NETWORK_PROXY_PASSWORD = ConfigVar('proxy_password', default=None, type_=str)
+    NETWORK_PROXY_ENABLED = ConfigVar('enable_proxy', default=lambda config: config.NETWORK_PROXY not in [None, "none"], type_=bool)
     NETWORK_SERVER = ConfigVar('server', default=None, type_=str)
     NETWORK_NOONION = ConfigVar('noonion', default=False, type_=bool)
     NETWORK_OFFLINE = ConfigVar('offline', default=False, type_=bool)
diff --git a/electrum/util.py b/electrum/util.py
index a5237ee688b9..66188fae65dc 100644
--- a/electrum/util.py
+++ b/electrum/util.py
@@ -67,7 +67,7 @@
 from .logging import get_logger, Logger
 
 if TYPE_CHECKING:
-    from .network import Network
+    from .network import Network, ProxySettings
     from .interface import Interface
     from .simple_config import SimpleConfig
     from .paymentrequest import PaymentRequest
@@ -1313,7 +1313,7 @@ def format_short_id(short_channel_id: Optional[bytes]):
         + 'x' + str(int.from_bytes(short_channel_id[6:], 'big'))
 
 
-def make_aiohttp_session(proxy: Optional[dict], headers=None, timeout=None):
+def make_aiohttp_session(proxy: Optional['ProxySettings'], headers=None, timeout=None):
     if headers is None:
         headers = {'User-Agent': 'Electrum'}
     if timeout is None:
@@ -1324,13 +1324,13 @@ def make_aiohttp_session(proxy: Optional[dict], headers=None, timeout=None):
         timeout = aiohttp.ClientTimeout(total=timeout)
     ssl_context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=ca_path)
 
-    if proxy:
+    if proxy and proxy.enabled:
         connector = ProxyConnector(
-            proxy_type=ProxyType.SOCKS5 if proxy['mode'] == 'socks5' else ProxyType.SOCKS4,
-            host=proxy['host'],
-            port=int(proxy['port']),
-            username=proxy.get('user', None),
-            password=proxy.get('password', None),
+            proxy_type=ProxyType.SOCKS5 if proxy.mode == 'socks5' else ProxyType.SOCKS4,
+            host=proxy.host,
+            port=int(proxy.port),
+            username=proxy.user,
+            password=proxy.password,
             rdns=True,  # needed to prevent DNS leaks over proxy
             ssl=ssl_context,
         )
@@ -1547,32 +1547,51 @@ def session(self):
         return s
 
 
-def detect_tor_socks_proxy() -> Optional[Tuple[str, int]]:
+async def detect_tor_socks_proxy() -> Optional[Tuple[str, int]]:
     # Probable ports for Tor to listen at
     candidates = [
         ("127.0.0.1", 9050),
+        ("127.0.0.1", 9051),
         ("127.0.0.1", 9150),
     ]
-    for net_addr in candidates:
-        if is_tor_socks_port(*net_addr):
-            return net_addr
-    return None
 
+    proxy_addr = None
+    async def test_net_addr(net_addr):
+        is_tor = await is_tor_socks_port(*net_addr)
+        # set result, and cancel remaining probes
+        if is_tor:
+            nonlocal proxy_addr
+            proxy_addr = net_addr
+            await group.cancel_remaining()
 
-def is_tor_socks_port(host: str, port: int) -> bool:
+    async with OldTaskGroup() as group:
+        for net_addr in candidates:
+            await group.spawn(test_net_addr(net_addr))
+    return proxy_addr
+
+
+@log_exceptions
+async def is_tor_socks_port(host: str, port: int) -> bool:
+    # mimic "tor-resolve 0.0.0.0".
+    # see https://github.com/spesmilo/electrum/issues/7317#issuecomment-1369281075
+    # > this is a socks5 handshake, followed by a socks RESOLVE request as defined in
+    # > [tor's socks extension spec](https://github.com/torproject/torspec/blob/7116c9cdaba248aae07a3f1d0e15d9dd102f62c5/socks-extensions.txt#L63),
+    # > resolving 0.0.0.0, which being an IP, tor resolves itself without needing to ask a relay.
+    writer = None
     try:
-        with socket.create_connection((host, port), timeout=10) as s:
-            # mimic "tor-resolve 0.0.0.0".
-            # see https://github.com/spesmilo/electrum/issues/7317#issuecomment-1369281075
-            # > this is a socks5 handshake, followed by a socks RESOLVE request as defined in
-            # > [tor's socks extension spec](https://github.com/torproject/torspec/blob/7116c9cdaba248aae07a3f1d0e15d9dd102f62c5/socks-extensions.txt#L63),
-            # > resolving 0.0.0.0, which being an IP, tor resolves itself without needing to ask a relay.
-            s.send(b'\x05\x01\x00\x05\xf0\x00\x03\x070.0.0.0\x00\x00')
-            if s.recv(1024) == b'\x05\x00\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00':
+        async with async_timeout(10):
+            reader, writer = await asyncio.open_connection(host, port)
+            writer.write(b'\x05\x01\x00\x05\xf0\x00\x03\x070.0.0.0\x00\x00')
+            await writer.drain()
+            data = await reader.read(1024)
+            if data == b'\x05\x00\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00':
                 return True
-    except socket.error:
-        pass
-    return False
+            return False
+    except (OSError, asyncio.TimeoutError):
+        return False
+    finally:
+        if writer:
+            writer.close()
 
 
 AS_LIB_USER_I_WANT_TO_MANAGE_MY_OWN_ASYNCIO_LOOP = False  # used by unit tests
@@ -1969,10 +1988,10 @@ async def open_connection(self, host=None, port=None, **kwargs):
 
     @classmethod
     def from_network_settings(cls, network: Optional['Network']) -> Optional['ESocksProxy']:
-        if not network or not network.proxy:
+        if not network or not network.proxy or not network.proxy.enabled:
             return None
         proxy = network.proxy
-        username, pw = proxy.get('user'), proxy.get('password')
+        username, pw = proxy.user, proxy.password
         if not username or not pw:
             # is_proxy_tor is tri-state; None indicates it is still probing the proxy to test for TOR
             if network.is_proxy_tor:
@@ -1981,10 +2000,10 @@ def from_network_settings(cls, network: Optional['Network']) -> Optional['ESocks
                 auth = None
         else:
             auth = aiorpcx.socks.SOCKSUserAuth(username, pw)
-        addr = aiorpcx.NetAddress(proxy['host'], proxy['port'])
-        if proxy['mode'] == "socks4":
+        addr = aiorpcx.NetAddress(proxy.host, proxy.port)
+        if proxy.mode == "socks4":
             ret = cls(addr, aiorpcx.socks.SOCKS4a, auth)
-        elif proxy['mode'] == "socks5":
+        elif proxy.mode == "socks5":
             ret = cls(addr, aiorpcx.socks.SOCKS5, auth)
         else:
             raise NotImplementedError  # http proxy not available with aiorpcx
diff --git a/electrum/wizard.py b/electrum/wizard.py
index 0b34463e0e54..9df66ce6117e 100644
--- a/electrum/wizard.py
+++ b/electrum/wizard.py
@@ -7,6 +7,7 @@
 from electrum.interface import ServerAddr
 from electrum.keystore import hardware_keystore
 from electrum.logging import get_logger
+from electrum.network import ProxySettings
 from electrum.plugin import run_hook
 from electrum.slip39 import EncryptedSeed
 from electrum.storage import WalletStorage, StorageEncryptionVersion
@@ -731,9 +732,8 @@ def do_configure_proxy(self, wizard_data: dict):
             return
         self._logger.debug(f'configuring proxy: {proxy_settings!r}')
         net_params = self._daemon.network.get_parameters()
-        if not proxy_settings['enabled']:
-            proxy_settings = None
-        net_params = net_params._replace(proxy=proxy_settings, auto_connect=bool(wizard_data['autoconnect']))
+        proxy = ProxySettings.from_dict(proxy_settings)
+        net_params = net_params._replace(proxy=proxy, auto_connect=bool(wizard_data['autoconnect']))
         self._daemon.network.run_from_another_thread(self._daemon.network.set_parameters(net_params))
 
     def do_configure_server(self, wizard_data: dict):
diff --git a/tests/test_wizard.py b/tests/test_wizard.py
index 8e98b9c760c1..822c220989fb 100644
--- a/tests/test_wizard.py
+++ b/tests/test_wizard.py
@@ -2,7 +2,7 @@
 
 from electrum import SimpleConfig
 from electrum.interface import ServerAddr
-from electrum.network import NetworkParameters
+from electrum.network import NetworkParameters, ProxySettings
 from electrum.plugin import Plugins
 from electrum.wizard import ServerConnectWizard, NewWalletWizard
 from tests import ElectrumTestCase
@@ -88,7 +88,7 @@ async def test_proxy(self):
         self.assertTrue(w.is_last_view(v.view, d))
 
         self.assertTrue(w._daemon.network.run_called)
-        self.assertEqual(NetworkParameters(server=None, proxy=d_proxy, auto_connect=True, oneserver=None), w._daemon.network.parameters)
+        self.assertEqual(NetworkParameters(server=None, proxy=ProxySettings.from_dict(d_proxy), auto_connect=True, oneserver=None), w._daemon.network.parameters)
 
     async def test_proxy_and_server(self):
         w = ServerConnectWizard(DaemonMock(self.config))


Reviews:
user: login: SomberNight
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-03T13:59:33Z
user: login: accumulator
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-04T08:49:37Z
user: login: SomberNight
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T18:50:12Z
user: login: accumulator
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T08:22:31Z
user: login: SomberNight
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T14:39:54Z

Review comments:
diff_hunk: @@ -227,344 +214,109 @@ def update(self):
         super().update()
 
 
-class NetworkChoiceLayout(object):
-    # TODO consolidate to ProxyWidget+ServerWidget
-    # TODO TorDetector is unnecessary, Network tests socks5 peer and detects Tor
-    # TODO apply on editingFinished is not ideal, separate Apply button and on Close?
-    def __init__(self, network: Network, config: 'SimpleConfig', wizard=False):
-        self.network = network
-        self.config = config
-        self.tor_proxy = None
+class ProxyWidget(QWidget):
+    PROXY_MODES = {
+        'socks4': 'SOCKS4',
+        'socks5': 'SOCKS5/TOR'
+    }
 
-        self.tabs = tabs = QTabWidget()
-        self._proxy_tab = proxy_tab = QWidget()
-        blockchain_tab = QWidget()
-        tabs.addTab(blockchain_tab, _('Overview'))
-        tabs.addTab(proxy_tab, _('Proxy'))
-        tabs.currentChanged.connect(self._on_tab_changed)
+    def __init__(self, network: Network, parent=None):
+        super().__init__(parent)
+        self.network = network
+        self.config = network.config
 
-        fixed_width_hostname = 24 * char_width_in_lineedit()
         fixed_width_port = 6 * char_width_in_lineedit()
 
-        # Proxy tab
-        grid = QGridLayout(proxy_tab)
-        grid.setSpacing(8)
-
-        # proxy setting
+        # proxy setting.
         self.proxy_cb = QCheckBox(_('Use proxy'))
-        self.proxy_cb.clicked.connect(self.check_disable_proxy)
-        self.proxy_cb.clicked.connect(self.set_proxy)
-
+        self.proxy_cb.stateChanged.connect(self.on_proxy_settings_changed)
         self.proxy_mode = QComboBox()
-        self.proxy_mode.addItems(['SOCKS4', 'SOCKS5'])
+        for k, v in self.PROXY_MODES.items():
+            self.proxy_mode.addItem(v, k)
+        self.proxy_mode.setCurrentIndex(1)
+        self.proxy_mode.currentIndexChanged.connect(self.on_proxy_settings_changed)
         self.proxy_host = QLineEdit()
-        self.proxy_host.setFixedWidth(fixed_width_hostname)
+        self.proxy_host.editingFinished.connect(self.on_proxy_settings_changed)
         self.proxy_port = QLineEdit()
+        self.proxy_port.editingFinished.connect(self.on_proxy_settings_changed)
         self.proxy_port.setFixedWidth(fixed_width_port)
         self.proxy_port_validator = QIntValidator(1, 65535)
         self.proxy_port.setValidator(self.proxy_port_validator)
 
         self.proxy_user = QLineEdit()
-        self.proxy_user.setPlaceholderText(_("Proxy user"))
+        self.proxy_user.editingFinished.connect(self.on_proxy_settings_changed)
+        self.proxy_user.setPlaceholderText(_("Proxy username"))
         self.proxy_password = PasswordLineEdit()
-        self.proxy_password.setPlaceholderText(_("Password"))
-        self.proxy_password.setFixedWidth(fixed_width_port)
-
-        self.proxy_mode.currentIndexChanged.connect(self.set_proxy)
-        self.proxy_host.editingFinished.connect(self.set_proxy)
-        self.proxy_port.editingFinished.connect(self.set_proxy)
-        self.proxy_user.editingFinished.connect(self.set_proxy)
-        self.proxy_password.editingFinished.connect(self.set_proxy)
-
-        self.proxy_mode.currentIndexChanged.connect(self.proxy_settings_changed)
-        self.proxy_host.textEdited.connect(self.proxy_settings_changed)
-        self.proxy_port.textEdited.connect(self.proxy_settings_changed)
-        self.proxy_user.textEdited.connect(self.proxy_settings_changed)
-        self.proxy_password.textEdited.connect(self.proxy_settings_changed)
-
-        self.tor_cb = QCheckBox(_("Use Tor Proxy"))
-        self.tor_cb.setIcon(read_QIcon("tor_logo.png"))
-        self.tor_cb.hide()
-        self.tor_cb.clicked.connect(self.use_tor_proxy)
-
-        grid.addWidget(self.tor_cb, 1, 0, 1, 3)
-        grid.addWidget(self.proxy_cb, 2, 0, 1, 3)
-        grid.addWidget(HelpButton(_('Proxy settings apply to all connections: with Electrum servers, but also with third-party services.')), 2, 4)
-        grid.addWidget(self.proxy_mode, 4, 1)
-        grid.addWidget(self.proxy_host, 4, 2)
-        grid.addWidget(self.proxy_port, 4, 3)
-        grid.addWidget(self.proxy_user, 5, 2)
-        grid.addWidget(self.proxy_password, 5, 3)
-        grid.setRowStretch(7, 1)
-
-        # Blockchain Tab
-        grid = QGridLayout(blockchain_tab)
-        msg =  ' '.join([
-            _("Electrum connects to several nodes in order to download block headers and find out the longest blockchain."),
-            _("This blockchain is used to verify the transactions sent by your transaction server.")
-        ])
-        self.status_label = QLabel('')
-        grid.addWidget(QLabel(_('Status') + ':'), 0, 0)
-        grid.addWidget(self.status_label, 0, 1, 1, 3)
-        grid.addWidget(HelpButton(msg), 0, 4)
-
-        self.autoconnect_cb = QCheckBox(_('Select server automatically'))
-        self.autoconnect_cb.setEnabled(self.config.cv.NETWORK_AUTO_CONNECT.is_modifiable())
-        self.autoconnect_cb.clicked.connect(self.set_server)
-        self.autoconnect_cb.clicked.connect(self.update)
-        msg = ' '.join([
-            _("If auto-connect is enabled, Electrum will always use a server that is on the longest blockchain."),
-            _("If it is disabled, you have to choose a server you want to use. Electrum will warn you if your server is lagging.")
-        ])
-        grid.addWidget(self.autoconnect_cb, 1, 0, 1, 3)
-        grid.addWidget(HelpButton(msg), 1, 4)
-
-        self.server_e = QLineEdit()
-        self.server_e.setFixedWidth(fixed_width_hostname + fixed_width_port)
-        self.server_e.editingFinished.connect(self.set_server)
-        msg = _("Electrum sends your wallet addresses to a single server, in order to receive your transaction history.")
-        grid.addWidget(QLabel(_('Server') + ':'), 2, 0)
-        grid.addWidget(self.server_e, 2, 1, 1, 3)
-        grid.addWidget(HelpButton(msg), 2, 4)
+        self.proxy_password.editingFinished.connect(self.on_proxy_settings_changed)
+        self.proxy_password.setPlaceholderText(_("Proxy password"))
 
-        self.height_label = QLabel('')
-        msg = _('This is the height of your local copy of the blockchain.')
-        grid.addWidget(QLabel(_('Blockchain') + ':'), 3, 0)
-        grid.addWidget(self.height_label, 3, 1)
-        grid.addWidget(HelpButton(msg), 3, 4)
-
-        self.split_label = QLabel('')
-        grid.addWidget(self.split_label, 4, 0, 1, 3)
-
-        self.nodes_list_widget = NodesListWidget(network=self.network)
-        self.nodes_list_widget.followServer.connect(self.follow_server)
-        self.nodes_list_widget.followChain.connect(self.follow_branch)
-
-        def do_set_server(server):
-            self.server_e.setText(server)
-            self.set_server()
-        self.nodes_list_widget.setServer.connect(do_set_server)
-        grid.addWidget(self.nodes_list_widget, 6, 0, 1, 5)
-
-        vbox = QVBoxLayout()
-        vbox.addWidget(tabs)
-        self.layout_ = vbox
-        # tor detector
-        self.td = td = TorDetector()
-        td.found_proxy.connect(self.suggest_proxy)
-        td.start()
+        grid = QGridLayout(self)
+        grid.setSpacing(8)
 
-        self.fill_in_proxy_settings()
+        grid.addWidget(self.proxy_cb, 0, 0, 1, 4)
+        proxy_helpbutton = HelpButton(
+            _('Proxy settings apply to all connections: with Electrum servers, but also with third-party services.'))
+        grid.addWidget(proxy_helpbutton, 0, 4, alignment=Qt.AlignmentFlag.AlignRight)
+        grid.addWidget(self.proxy_mode, 1, 0, 1, 1)
+        grid.addWidget(self.proxy_host, 1, 1, 1, 3)
+        grid.addWidget(self.proxy_port, 1, 4, 1, 1)
+        grid.addWidget(self.proxy_user, 2, 1, 1, 2)
+        grid.addWidget(self.proxy_password, 2, 3, 1, 2)
+
+        spacer = QVBoxLayout()
+        spacer.addStretch()
+        grid.addLayout(spacer, 3, 0, 1, 4)
+
+        self.update_from_config()
         self.update()
 
-    def clean_up(self):
-        if self.td:
-            self.td.found_proxy.disconnect()
-            self.td.stop()
-            self.td = None
-
-    def check_disable_proxy(self, b):
-        if not self.config.cv.NETWORK_PROXY.is_modifiable():
-            b = False
-        for w in [self.proxy_mode, self.proxy_host, self.proxy_port, self.proxy_user, self.proxy_password]:
-            w.setEnabled(b)
-
-    def enable_set_server(self):
-        if self.config.cv.NETWORK_SERVER.is_modifiable():
-            enabled = not self.autoconnect_cb.isChecked()
-            self.server_e.setEnabled(enabled)
-        else:
-            for w in [self.autoconnect_cb, self.server_e, self.nodes_list_widget]:
-                w.setEnabled(False)
-
     def update(self):
-        net_params = self.network.get_parameters()
-        server = net_params.server
-        auto_connect = net_params.auto_connect
-        if not self.server_e.hasFocus():
-            self.server_e.setText(server.to_friendly_name())
-        self.autoconnect_cb.setChecked(auto_connect)
-
-        height_str = "%d "%(self.network.get_local_height()) + _('blocks')
-        self.height_label.setText(height_str)
-        self.status_label.setText(self.network.get_status())
-        chains = self.network.get_blockchains()
-        if len(chains) > 1:
-            chain = self.network.blockchain()
-            forkpoint = chain.get_max_forkpoint()
-            name = chain.get_name()
-            msg = _('Chain split detected at block {0}').format(forkpoint) + '\n'
-            msg += (_('You are following branch') if auto_connect else _('Your server is on branch'))+ ' ' + name
-            msg += ' (%d %s)' % (chain.get_branch_size(), _('blocks'))
-        else:
-            msg = ''
-        self.split_label.setText(msg)
-        self.nodes_list_widget.update()
-        self.enable_set_server()
-
-    def fill_in_proxy_settings(self):
-        proxy_config = self.network.get_parameters().proxy
-        if not proxy_config:
-            proxy_config = {"mode": "none", "host": "localhost", "port": "9050"}
-
-        b = proxy_config.get('mode') != "none"
-        self.check_disable_proxy(b)
-        if b:
-            self.proxy_cb.setChecked(True)
-            self.proxy_mode.setCurrentIndex(
-                self.proxy_mode.findText(str(proxy_config.get("mode").upper())))
-
-        self.proxy_host.setText(proxy_config.get("host"))
-        self.proxy_port.setText(proxy_config.get("port"))
-        self.proxy_user.setText(proxy_config.get("user", ""))
-        self.proxy_password.setText(proxy_config.get("password", ""))
-
-    def layout(self):
-        return self.layout_
+        enabled = self.proxy_cb.isChecked() and self.config.cv.NETWORK_PROXY.is_modifiable()
+        for item in [self.proxy_mode, self.proxy_host, self.proxy_port, self.proxy_user, self.proxy_password]:
+            item.setEnabled(enabled)
 
-    def follow_branch(self, chain_id):
-        self.network.run_from_another_thread(self.network.follow_chain_given_id(chain_id))
-        self.update()
-
-    def follow_server(self, server: ServerAddr):
-        self.network.run_from_another_thread(self.network.follow_chain_given_server(server))
-        self.update()
-
-    def accept(self):
-        pass
-
-    def set_server(self):
-        net_params = self.network.get_parameters()
-        try:
-            server = ServerAddr.from_str_with_inference(str(self.server_e.text()))
-            if not server: raise Exception("failed to parse")
-        except Exception:
-            return
-        net_params = net_params._replace(server=server,
-                                         auto_connect=self.autoconnect_cb.isChecked())
-        self.network.run_from_another_thread(self.network.set_parameters(net_params))
-
-    def set_proxy(self):
         net_params = self.network.get_parameters()
         if self.proxy_cb.isChecked():
             if not self.proxy_port.hasAcceptableInput():
                 return
-            proxy = {'mode':str(self.proxy_mode.currentText()).lower(),
-                     'host':str(self.proxy_host.text()),
-                     'port':str(self.proxy_port.text()),
-                     'user':str(self.proxy_user.text()),
-                     'password':str(self.proxy_password.text())}
+            proxy = {'mode': str(self.proxy_mode.currentData()),
+                     'host': str(self.proxy_host.text()),
+                     'port': str(self.proxy_port.text()),
+                     'user': str(self.proxy_user.text()),
+                     'password': str(self.proxy_password.text())}
         else:
             proxy = None
-            self.tor_cb.setChecked(False)
         net_params = net_params._replace(proxy=proxy)
         self.network.run_from_another_thread(self.network.set_parameters(net_params))
 
-    def _on_tab_changed(self):
-        if self.tabs.currentWidget() is self._proxy_tab:
-            self.td.trigger_rescan()
-
-    def suggest_proxy(self, found_proxy):
-        if found_proxy is None:
-            self.tor_cb.hide()
-            return
-        self.tor_proxy = found_proxy
-        self.tor_cb.setText(_("Use Tor proxy at port {}").format(str(found_proxy[1])))
-        if (self.proxy_cb.isChecked()
-                and self.proxy_mode.currentIndex() == self.proxy_mode.findText('SOCKS5')
-                and self.proxy_host.text() == "127.0.0.1"
-                and self.proxy_port.text() == str(found_proxy[1])):
-            self.tor_cb.setChecked(True)
-        self.tor_cb.show()
-
-    def use_tor_proxy(self, use_it):
-        if not use_it:
-            self.proxy_cb.setChecked(False)
-        else:
-            socks5_mode_index = self.proxy_mode.findText('SOCKS5')
-            if socks5_mode_index == -1:
-                _logger.info("can't find proxy_mode 'SOCKS5'")
-                return
-            self.proxy_mode.setCurrentIndex(socks5_mode_index)
-            self.proxy_host.setText("127.0.0.1")
-            self.proxy_port.setText(str(self.tor_proxy[1]))
-            self.proxy_user.setText("")
-            self.proxy_password.setText("")
-            self.tor_cb.setChecked(True)
-            self.proxy_cb.setChecked(True)
-        self.check_disable_proxy(use_it)
-        self.set_proxy()
-
-    def proxy_settings_changed(self):
-        self.tor_cb.setChecked(False)
-
-
-class TorDetector(QThread):
path: electrum/gui/qt/network_dialog.py
user: login: SomberNight
body: > The separate Tor checkbox was removed, as it was somewhat confusing and not kept in UI-sync with actual Tor use.

The utility here was that if the user had a Tor proxy available they could use, e.g. because their Linux distro runs one in the background, or because they have Tor Browser running, they could set that up using a single click. I think this was useful, but this PR removes it completely AFAICT :/

It does not have to be a checkbox, it could be a button too: "Tor proxy found: [activate]", or maybe in a completely different way.
created_at: 2025-02-03T13:59:32Z
author_association: MEMBER
diff_hunk: @@ -227,344 +214,109 @@ def update(self):
         super().update()
 
 
-class NetworkChoiceLayout(object):
-    # TODO consolidate to ProxyWidget+ServerWidget
-    # TODO TorDetector is unnecessary, Network tests socks5 peer and detects Tor
-    # TODO apply on editingFinished is not ideal, separate Apply button and on Close?
-    def __init__(self, network: Network, config: 'SimpleConfig', wizard=False):
-        self.network = network
-        self.config = config
-        self.tor_proxy = None
+class ProxyWidget(QWidget):
+    PROXY_MODES = {
+        'socks4': 'SOCKS4',
+        'socks5': 'SOCKS5/TOR'
+    }
 
-        self.tabs = tabs = QTabWidget()
-        self._proxy_tab = proxy_tab = QWidget()
-        blockchain_tab = QWidget()
-        tabs.addTab(blockchain_tab, _('Overview'))
-        tabs.addTab(proxy_tab, _('Proxy'))
-        tabs.currentChanged.connect(self._on_tab_changed)
+    def __init__(self, network: Network, parent=None):
+        super().__init__(parent)
+        self.network = network
+        self.config = network.config
 
-        fixed_width_hostname = 24 * char_width_in_lineedit()
         fixed_width_port = 6 * char_width_in_lineedit()
 
-        # Proxy tab
-        grid = QGridLayout(proxy_tab)
-        grid.setSpacing(8)
-
-        # proxy setting
+        # proxy setting.
         self.proxy_cb = QCheckBox(_('Use proxy'))
-        self.proxy_cb.clicked.connect(self.check_disable_proxy)
-        self.proxy_cb.clicked.connect(self.set_proxy)
-
+        self.proxy_cb.stateChanged.connect(self.on_proxy_settings_changed)
         self.proxy_mode = QComboBox()
-        self.proxy_mode.addItems(['SOCKS4', 'SOCKS5'])
+        for k, v in self.PROXY_MODES.items():
+            self.proxy_mode.addItem(v, k)
+        self.proxy_mode.setCurrentIndex(1)
+        self.proxy_mode.currentIndexChanged.connect(self.on_proxy_settings_changed)
         self.proxy_host = QLineEdit()
-        self.proxy_host.setFixedWidth(fixed_width_hostname)
+        self.proxy_host.editingFinished.connect(self.on_proxy_settings_changed)
         self.proxy_port = QLineEdit()
+        self.proxy_port.editingFinished.connect(self.on_proxy_settings_changed)
         self.proxy_port.setFixedWidth(fixed_width_port)
         self.proxy_port_validator = QIntValidator(1, 65535)
         self.proxy_port.setValidator(self.proxy_port_validator)
 
         self.proxy_user = QLineEdit()
-        self.proxy_user.setPlaceholderText(_("Proxy user"))
+        self.proxy_user.editingFinished.connect(self.on_proxy_settings_changed)
+        self.proxy_user.setPlaceholderText(_("Proxy username"))
         self.proxy_password = PasswordLineEdit()
-        self.proxy_password.setPlaceholderText(_("Password"))
-        self.proxy_password.setFixedWidth(fixed_width_port)
-
-        self.proxy_mode.currentIndexChanged.connect(self.set_proxy)
-        self.proxy_host.editingFinished.connect(self.set_proxy)
-        self.proxy_port.editingFinished.connect(self.set_proxy)
-        self.proxy_user.editingFinished.connect(self.set_proxy)
-        self.proxy_password.editingFinished.connect(self.set_proxy)
-
-        self.proxy_mode.currentIndexChanged.connect(self.proxy_settings_changed)
-        self.proxy_host.textEdited.connect(self.proxy_settings_changed)
-        self.proxy_port.textEdited.connect(self.proxy_settings_changed)
-        self.proxy_user.textEdited.connect(self.proxy_settings_changed)
-        self.proxy_password.textEdited.connect(self.proxy_settings_changed)
-
-        self.tor_cb = QCheckBox(_("Use Tor Proxy"))
-        self.tor_cb.setIcon(read_QIcon("tor_logo.png"))
-        self.tor_cb.hide()
-        self.tor_cb.clicked.connect(self.use_tor_proxy)
-
-        grid.addWidget(self.tor_cb, 1, 0, 1, 3)
-        grid.addWidget(self.proxy_cb, 2, 0, 1, 3)
-        grid.addWidget(HelpButton(_('Proxy settings apply to all connections: with Electrum servers, but also with third-party services.')), 2, 4)
-        grid.addWidget(self.proxy_mode, 4, 1)
-        grid.addWidget(self.proxy_host, 4, 2)
-        grid.addWidget(self.proxy_port, 4, 3)
-        grid.addWidget(self.proxy_user, 5, 2)
-        grid.addWidget(self.proxy_password, 5, 3)
-        grid.setRowStretch(7, 1)
-
-        # Blockchain Tab
-        grid = QGridLayout(blockchain_tab)
-        msg =  ' '.join([
-            _("Electrum connects to several nodes in order to download block headers and find out the longest blockchain."),
-            _("This blockchain is used to verify the transactions sent by your transaction server.")
-        ])
-        self.status_label = QLabel('')
-        grid.addWidget(QLabel(_('Status') + ':'), 0, 0)
-        grid.addWidget(self.status_label, 0, 1, 1, 3)
-        grid.addWidget(HelpButton(msg), 0, 4)
-
-        self.autoconnect_cb = QCheckBox(_('Select server automatically'))
-        self.autoconnect_cb.setEnabled(self.config.cv.NETWORK_AUTO_CONNECT.is_modifiable())
-        self.autoconnect_cb.clicked.connect(self.set_server)
-        self.autoconnect_cb.clicked.connect(self.update)
-        msg = ' '.join([
-            _("If auto-connect is enabled, Electrum will always use a server that is on the longest blockchain."),
-            _("If it is disabled, you have to choose a server you want to use. Electrum will warn you if your server is lagging.")
-        ])
-        grid.addWidget(self.autoconnect_cb, 1, 0, 1, 3)
-        grid.addWidget(HelpButton(msg), 1, 4)
-
-        self.server_e = QLineEdit()
-        self.server_e.setFixedWidth(fixed_width_hostname + fixed_width_port)
-        self.server_e.editingFinished.connect(self.set_server)
-        msg = _("Electrum sends your wallet addresses to a single server, in order to receive your transaction history.")
-        grid.addWidget(QLabel(_('Server') + ':'), 2, 0)
-        grid.addWidget(self.server_e, 2, 1, 1, 3)
-        grid.addWidget(HelpButton(msg), 2, 4)
+        self.proxy_password.editingFinished.connect(self.on_proxy_settings_changed)
+        self.proxy_password.setPlaceholderText(_("Proxy password"))
 
-        self.height_label = QLabel('')
-        msg = _('This is the height of your local copy of the blockchain.')
-        grid.addWidget(QLabel(_('Blockchain') + ':'), 3, 0)
-        grid.addWidget(self.height_label, 3, 1)
-        grid.addWidget(HelpButton(msg), 3, 4)
-
-        self.split_label = QLabel('')
-        grid.addWidget(self.split_label, 4, 0, 1, 3)
-
-        self.nodes_list_widget = NodesListWidget(network=self.network)
-        self.nodes_list_widget.followServer.connect(self.follow_server)
-        self.nodes_list_widget.followChain.connect(self.follow_branch)
-
-        def do_set_server(server):
-            self.server_e.setText(server)
-            self.set_server()
-        self.nodes_list_widget.setServer.connect(do_set_server)
-        grid.addWidget(self.nodes_list_widget, 6, 0, 1, 5)
-
-        vbox = QVBoxLayout()
-        vbox.addWidget(tabs)
-        self.layout_ = vbox
-        # tor detector
-        self.td = td = TorDetector()
-        td.found_proxy.connect(self.suggest_proxy)
-        td.start()
+        grid = QGridLayout(self)
+        grid.setSpacing(8)
 
-        self.fill_in_proxy_settings()
+        grid.addWidget(self.proxy_cb, 0, 0, 1, 4)
+        proxy_helpbutton = HelpButton(
+            _('Proxy settings apply to all connections: with Electrum servers, but also with third-party services.'))
+        grid.addWidget(proxy_helpbutton, 0, 4, alignment=Qt.AlignmentFlag.AlignRight)
+        grid.addWidget(self.proxy_mode, 1, 0, 1, 1)
+        grid.addWidget(self.proxy_host, 1, 1, 1, 3)
+        grid.addWidget(self.proxy_port, 1, 4, 1, 1)
+        grid.addWidget(self.proxy_user, 2, 1, 1, 2)
+        grid.addWidget(self.proxy_password, 2, 3, 1, 2)
+
+        spacer = QVBoxLayout()
+        spacer.addStretch()
+        grid.addLayout(spacer, 3, 0, 1, 4)
+
+        self.update_from_config()
         self.update()
 
-    def clean_up(self):
-        if self.td:
-            self.td.found_proxy.disconnect()
-            self.td.stop()
-            self.td = None
-
-    def check_disable_proxy(self, b):
-        if not self.config.cv.NETWORK_PROXY.is_modifiable():
-            b = False
-        for w in [self.proxy_mode, self.proxy_host, self.proxy_port, self.proxy_user, self.proxy_password]:
-            w.setEnabled(b)
-
-    def enable_set_server(self):
-        if self.config.cv.NETWORK_SERVER.is_modifiable():
-            enabled = not self.autoconnect_cb.isChecked()
-            self.server_e.setEnabled(enabled)
-        else:
-            for w in [self.autoconnect_cb, self.server_e, self.nodes_list_widget]:
-                w.setEnabled(False)
-
     def update(self):
-        net_params = self.network.get_parameters()
-        server = net_params.server
-        auto_connect = net_params.auto_connect
-        if not self.server_e.hasFocus():
-            self.server_e.setText(server.to_friendly_name())
-        self.autoconnect_cb.setChecked(auto_connect)
-
-        height_str = "%d "%(self.network.get_local_height()) + _('blocks')
-        self.height_label.setText(height_str)
-        self.status_label.setText(self.network.get_status())
-        chains = self.network.get_blockchains()
-        if len(chains) > 1:
-            chain = self.network.blockchain()
-            forkpoint = chain.get_max_forkpoint()
-            name = chain.get_name()
-            msg = _('Chain split detected at block {0}').format(forkpoint) + '\n'
-            msg += (_('You are following branch') if auto_connect else _('Your server is on branch'))+ ' ' + name
-            msg += ' (%d %s)' % (chain.get_branch_size(), _('blocks'))
-        else:
-            msg = ''
-        self.split_label.setText(msg)
-        self.nodes_list_widget.update()
-        self.enable_set_server()
-
-    def fill_in_proxy_settings(self):
-        proxy_config = self.network.get_parameters().proxy
-        if not proxy_config:
-            proxy_config = {"mode": "none", "host": "localhost", "port": "9050"}
-
-        b = proxy_config.get('mode') != "none"
-        self.check_disable_proxy(b)
-        if b:
-            self.proxy_cb.setChecked(True)
-            self.proxy_mode.setCurrentIndex(
-                self.proxy_mode.findText(str(proxy_config.get("mode").upper())))
-
-        self.proxy_host.setText(proxy_config.get("host"))
-        self.proxy_port.setText(proxy_config.get("port"))
-        self.proxy_user.setText(proxy_config.get("user", ""))
-        self.proxy_password.setText(proxy_config.get("password", ""))
-
-    def layout(self):
-        return self.layout_
+        enabled = self.proxy_cb.isChecked() and self.config.cv.NETWORK_PROXY.is_modifiable()
+        for item in [self.proxy_mode, self.proxy_host, self.proxy_port, self.proxy_user, self.proxy_password]:
+            item.setEnabled(enabled)
 
-    def follow_branch(self, chain_id):
-        self.network.run_from_another_thread(self.network.follow_chain_given_id(chain_id))
-        self.update()
-
-    def follow_server(self, server: ServerAddr):
-        self.network.run_from_another_thread(self.network.follow_chain_given_server(server))
-        self.update()
-
-    def accept(self):
-        pass
-
-    def set_server(self):
-        net_params = self.network.get_parameters()
-        try:
-            server = ServerAddr.from_str_with_inference(str(self.server_e.text()))
-            if not server: raise Exception("failed to parse")
-        except Exception:
-            return
-        net_params = net_params._replace(server=server,
-                                         auto_connect=self.autoconnect_cb.isChecked())
-        self.network.run_from_another_thread(self.network.set_parameters(net_params))
-
-    def set_proxy(self):
         net_params = self.network.get_parameters()
         if self.proxy_cb.isChecked():
             if not self.proxy_port.hasAcceptableInput():
                 return
-            proxy = {'mode':str(self.proxy_mode.currentText()).lower(),
-                     'host':str(self.proxy_host.text()),
-                     'port':str(self.proxy_port.text()),
-                     'user':str(self.proxy_user.text()),
-                     'password':str(self.proxy_password.text())}
+            proxy = {'mode': str(self.proxy_mode.currentData()),
+                     'host': str(self.proxy_host.text()),
+                     'port': str(self.proxy_port.text()),
+                     'user': str(self.proxy_user.text()),
+                     'password': str(self.proxy_password.text())}
         else:
             proxy = None
-            self.tor_cb.setChecked(False)
         net_params = net_params._replace(proxy=proxy)
         self.network.run_from_another_thread(self.network.set_parameters(net_params))
 
-    def _on_tab_changed(self):
-        if self.tabs.currentWidget() is self._proxy_tab:
-            self.td.trigger_rescan()
-
-    def suggest_proxy(self, found_proxy):
-        if found_proxy is None:
-            self.tor_cb.hide()
-            return
-        self.tor_proxy = found_proxy
-        self.tor_cb.setText(_("Use Tor proxy at port {}").format(str(found_proxy[1])))
-        if (self.proxy_cb.isChecked()
-                and self.proxy_mode.currentIndex() == self.proxy_mode.findText('SOCKS5')
-                and self.proxy_host.text() == "127.0.0.1"
-                and self.proxy_port.text() == str(found_proxy[1])):
-            self.tor_cb.setChecked(True)
-        self.tor_cb.show()
-
-    def use_tor_proxy(self, use_it):
-        if not use_it:
-            self.proxy_cb.setChecked(False)
-        else:
-            socks5_mode_index = self.proxy_mode.findText('SOCKS5')
-            if socks5_mode_index == -1:
-                _logger.info("can't find proxy_mode 'SOCKS5'")
-                return
-            self.proxy_mode.setCurrentIndex(socks5_mode_index)
-            self.proxy_host.setText("127.0.0.1")
-            self.proxy_port.setText(str(self.tor_proxy[1]))
-            self.proxy_user.setText("")
-            self.proxy_password.setText("")
-            self.tor_cb.setChecked(True)
-            self.proxy_cb.setChecked(True)
-        self.check_disable_proxy(use_it)
-        self.set_proxy()
-
-    def proxy_settings_changed(self):
-        self.tor_cb.setChecked(False)
-
-
-class TorDetector(QThread):
path: electrum/gui/qt/network_dialog.py
user: login: accumulator
body: Maybe something like this: when user enables the 'use proxy' checkbox, it does a probe. If it finds a proxy on one of the ports (also non-tor proxies, but e.g. prefer tor), show a yes/no popup asking the user to apply the detected settings. The probe should maybe only happen if the user did not already enter a hostname and/or port number, so this is not spammy. 

Note: I think we should keep proxy settings stored in config, even if proxy gets disabled. Currently all proxy related settings are deleted from config when proxy gets disabled.



created_at: 2025-02-04T08:49:37Z
author_association: MEMBER
diff_hunk: @@ -156,65 +159,129 @@ def pick_random_server(hostmap=None, *, allowed_protocols: Iterable[str],
     return random.choice(eligible) if eligible else None
 
 
-class NetworkParameters(NamedTuple):
-    server: ServerAddr
-    proxy: Optional[dict]
-    auto_connect: bool
-    oneserver: bool = False
+class ProxySettings:
+    MODES = ['socks4', 'socks5']
 
+    probe_thread = None
 
-proxy_modes = ['socks4', 'socks5']
+    def __init__(self):
+        self.enabled = False
+        self.mode = 'socks5'
+        self.host = ''
+        self.port = ''
+        self.user = None
+        self.password = None
 
+    def set_defaults(self):
+        self.__init__()  # call __init__ for default values
 
-def serialize_proxy(p):
-    if not isinstance(p, dict):
-        return None
-    return ':'.join([p.get('mode'), p.get('host'), p.get('port')])
+    def serialize_proxy_cfgstr(self):
+        return ':'.join([self.mode, self.host, self.port])
 
+    def deserialize_proxy_cfgstr(self, s: Optional[str], user: str = None, password: str = None) -> Optional[dict]:
+        if s is None or not isinstance(s, str) or s.lower() == 'none':
path: electrum/network.py
user: login: SomberNight
body: if `s` is neither a str, nor None, we should raise. E.g. if `s` is a dict, the caller is doing something seriously wrong and we should propagate and exception out.
created_at: 2025-03-04T17:23:02Z
author_association: MEMBER
diff_hunk: @@ -950,6 +950,7 @@ def __setattr__(self, name, value):
     NETWORK_PROXY = ConfigVar('proxy', default=None, type_=str, convert_getter=lambda v: "none" if v is None else v)
     NETWORK_PROXY_USER = ConfigVar('proxy_user', default=None, type_=str)
     NETWORK_PROXY_PASSWORD = ConfigVar('proxy_password', default=None, type_=str)
+    NETWORK_ENABLE_PROXY = ConfigVar('enable_proxy', default=lambda config: config.NETWORK_PROXY not in [None, "none"], type_=bool)
path: electrum/simple_config.py
user: login: SomberNight
body: I would prefer the ConfigVar to be named `NETWORK_PROXY_ENABLED` (or without trailing "D") - so that all the proxy related ones have the "NETWORK_PROXY_" prefix.

As for the user-visible config key str itself, idk, maybe it's fine as-is.
created_at: 2025-03-04T18:19:23Z
author_association: MEMBER
diff_hunk: @@ -156,65 +159,129 @@ def pick_random_server(hostmap=None, *, allowed_protocols: Iterable[str],
     return random.choice(eligible) if eligible else None
 
 
-class NetworkParameters(NamedTuple):
-    server: ServerAddr
-    proxy: Optional[dict]
-    auto_connect: bool
-    oneserver: bool = False
+class ProxySettings:
+    MODES = ['socks4', 'socks5']
 
+    probe_thread = None
 
-proxy_modes = ['socks4', 'socks5']
+    def __init__(self):
+        self.enabled = False
+        self.mode = 'socks5'
+        self.host = ''
+        self.port = ''
+        self.user = None
+        self.password = None
 
+    def set_defaults(self):
+        self.__init__()  # call __init__ for default values
 
-def serialize_proxy(p):
-    if not isinstance(p, dict):
-        return None
-    return ':'.join([p.get('mode'), p.get('host'), p.get('port')])
+    def serialize_proxy_cfgstr(self):
+        return ':'.join([self.mode, self.host, self.port])
 
+    def deserialize_proxy_cfgstr(self, s: Optional[str], user: str = None, password: str = None) -> Optional[dict]:
+        if s is None or not isinstance(s, str) or s.lower() == 'none':
+            self.set_defaults()
+            self.user = user
+            self.password = password
+            return
 
-def deserialize_proxy(s: Optional[str], user: str = None, password: str = None) -> Optional[dict]:
-    if not isinstance(s, str):
-        return None
-    if s.lower() == 'none':
-        return None
-    proxy = {"mode": "socks5", "host": "localhost"}
+        args = s.split(':')
+        if args[0] in ProxySettings.MODES:
+            self.mode = args[0]
+            args = args[1:]
 
-    args = s.split(':')
-    if args[0] in proxy_modes:
-        proxy['mode'] = args[0]
-        args = args[1:]
+        def is_valid_port(ps: str):
+            try:
+                return 0 < int(ps) < 65535
+            except ValueError:
+                return False
 
-    def is_valid_port(ps: str):
-        try:
-            return 0 < int(ps) < 65535
-        except ValueError:
-            return False
+        def is_valid_host(ph: str):
+            try:
+                NetAddress(ph, '1')
+            except ValueError:
+                return False
+            return True
 
-    def is_valid_host(ph: str):
-        try:
-            NetAddress(ph, '1')
-        except ValueError:
-            return False
-        return True
+        # detect migrate from old settings
+        if len(args) == 4 and is_valid_host(args[0]) and is_valid_port(args[1]):  # host:port:user:pass,
+            self.host = args[0]
+            self.port = args[1]
+            self.user = args[2]
+            self.password = args[3]
+        else:
+            self.host = ':'.join(args[:-1])
+            self.port = args[-1]
+            self.user = user
+            self.password = password
+
+        if not is_valid_host(self.host) or not is_valid_port(self.port):
+            self.enabled = False
path: electrum/network.py
user: login: SomberNight
body: In the qt gui, try setting a proxy with `host=""`.
```
 50.27 | E | n/network | Exception in _run_new_interface: ValueError("'' does not appear to be an IPv4 or IPv6 address")
Traceback (most recent call last):
  File "...\electrum_ws\electrum\electrum\util.py", line 1200, in wrapper
    return await func(*args, **kwargs)
  File "...\electrum_ws\electrum\electrum\network.py", line 954, in _run_new_interface
    interface = Interface(network=self, server=server)
  File "...\electrum_ws\electrum\electrum\interface.py", line 399, in __init__
    self.proxy = ESocksProxy.from_network_settings(network)
  File "...\electrum_ws\electrum\electrum\util.py", line 1985, in from_network_settings
    addr = aiorpcx.NetAddress(proxy.host, proxy.port)
  File "...\aiorpcX\aiorpcx\util.py", line 126, in __init__
    self._host = classify_host(host)
  File "...\aiorpcX\aiorpcx\util.py", line 77, in classify_host
    return ip_address(host)
  File ...\Python310\lib\ipaddress.py", line 54, in ip_address
    raise ValueError(f'{address!r} does not appear to be an IPv4 or IPv6 address')
ValueError: '' does not appear to be an IPv4 or IPv6 address
```
perhaps this `enabled` gets overwritten somewhere
created_at: 2025-03-04T18:31:01Z
author_association: MEMBER
diff_hunk: @@ -227,375 +215,162 @@ def update(self):
         super().update()
 
 
-class NetworkChoiceLayout(object):
-    # TODO consolidate to ProxyWidget+ServerWidget
-    # TODO TorDetector is unnecessary, Network tests socks5 peer and detects Tor
-    # TODO apply on editingFinished is not ideal, separate Apply button and on Close?
-    def __init__(self, network: Network, config: 'SimpleConfig', wizard=False):
-        self.network = network
-        self.config = config
-        self.tor_proxy = None
+class ProxyWidget(QWidget):
+    PROXY_MODES = {
+        'socks4': 'SOCKS4',
+        'socks5': 'SOCKS5/TOR'
+    }
 
-        self.tabs = tabs = QTabWidget()
-        self._proxy_tab = proxy_tab = QWidget()
-        blockchain_tab = QWidget()
-        tabs.addTab(blockchain_tab, _('Overview'))
-        tabs.addTab(proxy_tab, _('Proxy'))
-        tabs.currentChanged.connect(self._on_tab_changed)
+    torProbeFinished = pyqtSignal([str, int], arguments=['host', 'port'])
 
-        fixed_width_hostname = 24 * char_width_in_lineedit()
-        fixed_width_port = 6 * char_width_in_lineedit()
+    def __init__(self, network: Network, parent=None):
+        super().__init__(parent)
+        self.network = network
+        self.config = network.config
 
-        # Proxy tab
-        grid = QGridLayout(proxy_tab)
-        grid.setSpacing(8)
+        fixed_width_port = 6 * char_width_in_lineedit()
 
-        # proxy setting
+        # proxy setting.
         self.proxy_cb = QCheckBox(_('Use proxy'))
-        self.proxy_cb.clicked.connect(self.check_disable_proxy)
-        self.proxy_cb.clicked.connect(self.set_proxy)
-
         self.proxy_mode = QComboBox()
-        self.proxy_mode.addItems(['SOCKS4', 'SOCKS5'])
+        for k, v in self.PROXY_MODES.items():
+            self.proxy_mode.addItem(v, k)
+        self.proxy_mode.setCurrentIndex(1)
         self.proxy_host = QLineEdit()
-        self.proxy_host.setFixedWidth(fixed_width_hostname)
         self.proxy_port = QLineEdit()
         self.proxy_port.setFixedWidth(fixed_width_port)
         self.proxy_port_validator = QIntValidator(1, 65535)
         self.proxy_port.setValidator(self.proxy_port_validator)
 
         self.proxy_user = QLineEdit()
-        self.proxy_user.setPlaceholderText(_("Proxy user"))
+        self.proxy_user.setPlaceholderText(_("Proxy username"))
         self.proxy_password = PasswordLineEdit()
-        self.proxy_password.setPlaceholderText(_("Password"))
-        self.proxy_password.setFixedWidth(fixed_width_port)
-
-        self.proxy_mode.currentIndexChanged.connect(self.set_proxy)
-        self.proxy_host.editingFinished.connect(self.set_proxy)
-        self.proxy_port.editingFinished.connect(self.set_proxy)
-        self.proxy_user.editingFinished.connect(self.set_proxy)
-        self.proxy_password.editingFinished.connect(self.set_proxy)
-
-        self.proxy_mode.currentIndexChanged.connect(self.proxy_settings_changed)
-        self.proxy_host.textEdited.connect(self.proxy_settings_changed)
-        self.proxy_port.textEdited.connect(self.proxy_settings_changed)
-        self.proxy_user.textEdited.connect(self.proxy_settings_changed)
-        self.proxy_password.textEdited.connect(self.proxy_settings_changed)
-
-        self.tor_cb = QCheckBox(_("Use Tor Proxy"))
-        self.tor_cb.setIcon(read_QIcon("tor_logo.png"))
-        self.tor_cb.hide()
-        self.tor_cb.clicked.connect(self.use_tor_proxy)
-
-        grid.addWidget(self.tor_cb, 1, 0, 1, 3)
-        grid.addWidget(self.proxy_cb, 2, 0, 1, 3)
-        grid.addWidget(HelpButton(_('Proxy settings apply to all connections: with Electrum servers, but also with third-party services.')), 2, 4)
-        grid.addWidget(self.proxy_mode, 4, 1)
-        grid.addWidget(self.proxy_host, 4, 2)
-        grid.addWidget(self.proxy_port, 4, 3)
-        grid.addWidget(self.proxy_user, 5, 2)
-        grid.addWidget(self.proxy_password, 5, 3)
-        grid.setRowStretch(7, 1)
-
-        # Blockchain Tab
-        grid = QGridLayout(blockchain_tab)
-        msg =  ' '.join([
-            _("Electrum connects to several nodes in order to download block headers and find out the longest blockchain."),
-            _("This blockchain is used to verify the transactions sent by your transaction server.")
-        ])
-        self.status_label = QLabel('')
-        grid.addWidget(QLabel(_('Status') + ':'), 0, 0)
-        grid.addWidget(self.status_label, 0, 1, 1, 3)
-        grid.addWidget(HelpButton(msg), 0, 4)
+        self.proxy_password.setPlaceholderText(_("Proxy password"))
 
-        self.autoconnect_cb = QCheckBox(_('Select server automatically'))
-        self.autoconnect_cb.setEnabled(self.config.cv.NETWORK_AUTO_CONNECT.is_modifiable())
-        self.autoconnect_cb.clicked.connect(self.set_server)
-        self.autoconnect_cb.clicked.connect(self.update)
-        msg = ' '.join([
-            _("If auto-connect is enabled, Electrum will always use a server that is on the longest blockchain."),
-            _("If it is disabled, you have to choose a server you want to use. Electrum will warn you if your server is lagging.")
-        ])
-        grid.addWidget(self.autoconnect_cb, 1, 0, 1, 3)
-        grid.addWidget(HelpButton(msg), 1, 4)
+        grid = QGridLayout(self)
+        grid.setSpacing(8)
 
-        self.server_e = QLineEdit()
-        self.server_e.setFixedWidth(fixed_width_hostname + fixed_width_port)
-        self.server_e.editingFinished.connect(self.set_server)
-        msg = _("Electrum sends your wallet addresses to a single server, in order to receive your transaction history.")
-        grid.addWidget(QLabel(_('Server') + ':'), 2, 0)
-        grid.addWidget(self.server_e, 2, 1, 1, 3)
-        grid.addWidget(HelpButton(msg), 2, 4)
+        grid.addWidget(self.proxy_cb, 0, 0, 1, 4)
+        proxy_helpbutton = HelpButton(
+            _('Proxy settings apply to all connections: with Electrum servers, but also with third-party services.'))
+        grid.addWidget(proxy_helpbutton, 0, 4, alignment=Qt.AlignmentFlag.AlignRight)
+        grid.addWidget(self.proxy_mode, 1, 0, 1, 1)
+        grid.addWidget(self.proxy_host, 1, 1, 1, 3)
+        grid.addWidget(self.proxy_port, 1, 4, 1, 1)
+        grid.addWidget(self.proxy_user, 2, 1, 1, 2)
+        grid.addWidget(self.proxy_password, 2, 3, 1, 2)
 
-        self.height_label = QLabel('')
-        msg = _('This is the height of your local copy of the blockchain.')
-        grid.addWidget(QLabel(_('Blockchain') + ':'), 3, 0)
-        grid.addWidget(self.height_label, 3, 1)
-        grid.addWidget(HelpButton(msg), 3, 4)
+        spacer = QVBoxLayout()
+        spacer.addStretch(1)
+        grid.addLayout(spacer, 3, 0, 1, 5)
 
-        self.split_label = QLabel('')
-        grid.addWidget(self.split_label, 4, 0, 1, 3)
+        self.detect_button = QPushButton(_('Detect TOR proxy'))
+        grid.addWidget(self.detect_button, 4, 0, 1, 5, alignment=Qt.AlignmentFlag.AlignHCenter)
 
-        self.nodes_list_widget = NodesListWidget(network=self.network)
-        self.nodes_list_widget.followServer.connect(self.follow_server)
-        self.nodes_list_widget.followChain.connect(self.follow_branch)
-
-        def do_set_server(server):
-            self.server_e.setText(server)
-            self.set_server()
-        self.nodes_list_widget.setServer.connect(do_set_server)
-        grid.addWidget(self.nodes_list_widget, 6, 0, 1, 5)
-
-        vbox = QVBoxLayout()
-        vbox.addWidget(tabs)
-        self.layout_ = vbox
-        # tor detector
-        self.td = td = TorDetector()
-        td.found_proxy.connect(self.suggest_proxy)
-        td.start()
+        spacer = QVBoxLayout()
+        spacer.addStretch(1)
+        grid.addLayout(spacer, 5, 0, 1, 5)
 
-        self.fill_in_proxy_settings()
+        self.update_from_config()
         self.update()
 
-    def clean_up(self):
-        if self.td:
-            self.td.found_proxy.disconnect()
-            self.td.stop()
-            self.td = None
+        # connect signal handlers after init from config
+        self.proxy_cb.stateChanged.connect(self.on_proxy_enable_toggle)
+        self.proxy_mode.currentIndexChanged.connect(self.on_proxy_settings_changed)
+        self.proxy_host.editingFinished.connect(self.on_proxy_settings_changed)
+        self.proxy_port.editingFinished.connect(self.on_proxy_settings_changed)
+        self.proxy_user.editingFinished.connect(self.on_proxy_settings_changed)
+        self.proxy_password.editingFinished.connect(self.on_proxy_settings_changed)
+        self.detect_button.clicked.connect(self.detect_tor)
 
-    def check_disable_proxy(self, b):
-        if not self.config.cv.NETWORK_PROXY.is_modifiable():
-            b = False
-        for w in [self.proxy_mode, self.proxy_host, self.proxy_port, self.proxy_user, self.proxy_password]:
-            w.setEnabled(b)
-
-    def enable_set_server(self):
-        if self.config.cv.NETWORK_SERVER.is_modifiable():
-            enabled = not self.autoconnect_cb.isChecked()
-            self.server_e.setEnabled(enabled)
-        else:
-            for w in [self.autoconnect_cb, self.server_e, self.nodes_list_widget]:
-                w.setEnabled(False)
+        self.torProbeFinished.connect(self.on_tor_probe_finished)
 
     def update(self):
-        net_params = self.network.get_parameters()
-        server = net_params.server
-        auto_connect = net_params.auto_connect
-        if not self.server_e.hasFocus():
-            self.server_e.setText(server.to_friendly_name())
-        self.autoconnect_cb.setChecked(auto_connect)
-
-        height_str = "%d "%(self.network.get_local_height()) + _('blocks')
-        self.height_label.setText(height_str)
-        self.status_label.setText(self.network.get_status())
-        chains = self.network.get_blockchains()
-        if len(chains) > 1:
-            chain = self.network.blockchain()
-            forkpoint = chain.get_max_forkpoint()
-            name = chain.get_name()
-            msg = _('Chain split detected at block {0}').format(forkpoint) + '\n'
-            msg += (_('You are following branch') if auto_connect else _('Your server is on branch'))+ ' ' + name
-            msg += ' (%d %s)' % (chain.get_branch_size(), _('blocks'))
-        else:
-            msg = ''
-        self.split_label.setText(msg)
-        self.nodes_list_widget.update()
-        self.enable_set_server()
-
-    def fill_in_proxy_settings(self):
-        proxy_config = self.network.get_parameters().proxy
-        if not proxy_config:
-            proxy_config = {"mode": "none", "host": "localhost", "port": "9050"}
-
-        b = proxy_config.get('mode') != "none"
-        self.check_disable_proxy(b)
-        if b:
-            self.proxy_cb.setChecked(True)
-            self.proxy_mode.setCurrentIndex(
-                self.proxy_mode.findText(str(proxy_config.get("mode").upper())))
-
-        self.proxy_host.setText(proxy_config.get("host"))
-        self.proxy_port.setText(proxy_config.get("port"))
-        self.proxy_user.setText(proxy_config.get("user", ""))
-        self.proxy_password.setText(proxy_config.get("password", ""))
-
-    def layout(self):
-        return self.layout_
-
-    def follow_branch(self, chain_id):
-        self.network.run_from_another_thread(self.network.follow_chain_given_id(chain_id))
-        self.update()
-
-    def follow_server(self, server: ServerAddr):
-        self.network.run_from_another_thread(self.network.follow_chain_given_server(server))
-        self.update()
-
-    def accept(self):
-        pass
-
-    def set_server(self):
-        net_params = self.network.get_parameters()
-        try:
-            server = ServerAddr.from_str_with_inference(str(self.server_e.text()))
-            if not server: raise Exception("failed to parse")
-        except Exception:
+        enabled = self.proxy_cb.isChecked() and self.config.cv.NETWORK_PROXY.is_modifiable()
+        for item in [
+                self.proxy_mode, self.proxy_host, self.proxy_port, self.proxy_user, self.proxy_password,
+                self.detect_button
+        ]:
+            item.setEnabled(enabled)
+
+        if not self.proxy_port.hasAcceptableInput():
             return
-        net_params = net_params._replace(server=server,
-                                         auto_connect=self.autoconnect_cb.isChecked())
-        self.network.run_from_another_thread(self.network.set_parameters(net_params))
 
-    def set_proxy(self):
         net_params = self.network.get_parameters()
-        if self.proxy_cb.isChecked():
-            if not self.proxy_port.hasAcceptableInput():
-                return
-            proxy = {'mode':str(self.proxy_mode.currentText()).lower(),
-                     'host':str(self.proxy_host.text()),
-                     'port':str(self.proxy_port.text()),
-                     'user':str(self.proxy_user.text()),
-                     'password':str(self.proxy_password.text())}
-        else:
-            proxy = None
-            self.tor_cb.setChecked(False)
+        proxy = self.get_proxy_settings()
         net_params = net_params._replace(proxy=proxy)
         self.network.run_from_another_thread(self.network.set_parameters(net_params))
 
-    def _on_tab_changed(self):
-        if self.tabs.currentWidget() is self._proxy_tab:
-            self.td.trigger_rescan()
+    def update_from_config(self):
+        proxy = ProxySettings.from_config(self.config)
+        self.proxy_cb.setChecked(proxy.enabled)
+        self.proxy_mode.setCurrentText(self.PROXY_MODES.get(proxy.mode))
+        self.proxy_host.setText(proxy.host)
+        self.proxy_port.setText(proxy.port)
+        self.proxy_user.setText(proxy.user)
+        self.proxy_password.setText(proxy.password)
 
-    def suggest_proxy(self, found_proxy):
-        if found_proxy is None:
-            self.tor_cb.hide()
-            return
-        self.tor_proxy = found_proxy
-        self.tor_cb.setText(_("Use Tor proxy at port {}").format(str(found_proxy[1])))
-        if (self.proxy_cb.isChecked()
-                and self.proxy_mode.currentIndex() == self.proxy_mode.findText('SOCKS5')
-                and self.proxy_host.text() == "127.0.0.1"
-                and self.proxy_port.text() == str(found_proxy[1])):
-            self.tor_cb.setChecked(True)
-        self.tor_cb.show()
-
-    def use_tor_proxy(self, use_it):
-        if not use_it:
-            self.proxy_cb.setChecked(False)
-        else:
-            socks5_mode_index = self.proxy_mode.findText('SOCKS5')
-            if socks5_mode_index == -1:
-                _logger.info("can't find proxy_mode 'SOCKS5'")
-                return
-            self.proxy_mode.setCurrentIndex(socks5_mode_index)
-            self.proxy_host.setText("127.0.0.1")
-            self.proxy_port.setText(str(self.tor_proxy[1]))
-            self.proxy_user.setText("")
-            self.proxy_password.setText("")
-            self.tor_cb.setChecked(True)
-            self.proxy_cb.setChecked(True)
-        self.check_disable_proxy(use_it)
-        self.set_proxy()
-
-    def proxy_settings_changed(self):
-        self.tor_cb.setChecked(False)
-
-
-class TorDetector(QThread):
-    found_proxy = pyqtSignal(object)
-
-    def __init__(self):
-        QThread.__init__(self)
-        self._work_to_do_evt = threading.Event()
-        self._stopping = False
-
-    def run(self):
-        while True:
-            # do rescan
-            net_addr = detect_tor_socks_proxy()
-            self.found_proxy.emit(net_addr)
-            # wait until triggered
-            self._work_to_do_evt.wait()
-            self._work_to_do_evt.clear()
-            if self._stopping:
-                return
-
-    def trigger_rescan(self) -> None:
-        self._work_to_do_evt.set()
-
-    def stop(self):
-        self._stopping = True
-        self._work_to_do_evt.set()
-        self.exit()
-        self.wait()
+        if not self.config.cv.NETWORK_PROXY.is_modifiable():
+            for w in [
+                    self.proxy_cb, self.proxy_mode, self.proxy_host, self.proxy_port,
+                    self.proxy_user, self.proxy_password, self.detect_button
+            ]:
+                w.setEnabled(False)
 
+    def on_proxy_enable_toggle(self):
+        # probe if enabled and no pre-existing settings
+        # if self.proxy_cb.isChecked() and (not self.proxy_host.text() or not self.proxy_port.text()):
+        #     self.detect_tor()
+        self.update()
 
-class ProxyWidget(QWidget):
-    def __init__(self, parent=None):
-        super().__init__(parent)
+    def on_proxy_settings_changed(self):
+        self.update()
 
-        fixed_width_hostname = 24 * char_width_in_lineedit()
-        fixed_width_port = 6 * char_width_in_lineedit()
+    def get_proxy_settings(self) -> ProxySettings:
+        proxy = ProxySettings()
+        proxy.enabled = self.proxy_cb.isChecked()
+        proxy.mode = self.proxy_mode.currentData()
+        proxy.host = self.proxy_host.text()
+        proxy.port = self.proxy_port.text()
+        proxy.user = self.proxy_user.text()
+        proxy.password = self.proxy_password.text()
+        return proxy
 
-        grid = QGridLayout(self)
-        grid.setSpacing(8)
+    def detect_tor(self):
+        ProxySettings.probe_tor(self.torProbeFinished.emit)  # via signal
path: electrum/gui/qt/network_dialog.py
user: login: SomberNight
body: would be nice if there was some feedback to the user that something is going on.
Currently if I click the detect button, there is no feedback at all, and it takes like 5 seconds to set the text fields after that. (btw, why does it take that long? if it was faster, maybe it would be ok not to have feedback)
created_at: 2025-03-04T18:40:58Z
author_association: MEMBER
diff_hunk: @@ -227,375 +215,162 @@ def update(self):
         super().update()
 
 
-class NetworkChoiceLayout(object):
-    # TODO consolidate to ProxyWidget+ServerWidget
-    # TODO TorDetector is unnecessary, Network tests socks5 peer and detects Tor
-    # TODO apply on editingFinished is not ideal, separate Apply button and on Close?
-    def __init__(self, network: Network, config: 'SimpleConfig', wizard=False):
-        self.network = network
-        self.config = config
-        self.tor_proxy = None
+class ProxyWidget(QWidget):
+    PROXY_MODES = {
+        'socks4': 'SOCKS4',
+        'socks5': 'SOCKS5/TOR'
+    }
 
-        self.tabs = tabs = QTabWidget()
-        self._proxy_tab = proxy_tab = QWidget()
-        blockchain_tab = QWidget()
-        tabs.addTab(blockchain_tab, _('Overview'))
-        tabs.addTab(proxy_tab, _('Proxy'))
-        tabs.currentChanged.connect(self._on_tab_changed)
+    torProbeFinished = pyqtSignal([str, int], arguments=['host', 'port'])
 
-        fixed_width_hostname = 24 * char_width_in_lineedit()
-        fixed_width_port = 6 * char_width_in_lineedit()
+    def __init__(self, network: Network, parent=None):
+        super().__init__(parent)
+        self.network = network
+        self.config = network.config
 
-        # Proxy tab
-        grid = QGridLayout(proxy_tab)
-        grid.setSpacing(8)
+        fixed_width_port = 6 * char_width_in_lineedit()
 
-        # proxy setting
+        # proxy setting.
         self.proxy_cb = QCheckBox(_('Use proxy'))
-        self.proxy_cb.clicked.connect(self.check_disable_proxy)
-        self.proxy_cb.clicked.connect(self.set_proxy)
-
         self.proxy_mode = QComboBox()
-        self.proxy_mode.addItems(['SOCKS4', 'SOCKS5'])
+        for k, v in self.PROXY_MODES.items():
+            self.proxy_mode.addItem(v, k)
+        self.proxy_mode.setCurrentIndex(1)
         self.proxy_host = QLineEdit()
-        self.proxy_host.setFixedWidth(fixed_width_hostname)
         self.proxy_port = QLineEdit()
         self.proxy_port.setFixedWidth(fixed_width_port)
         self.proxy_port_validator = QIntValidator(1, 65535)
         self.proxy_port.setValidator(self.proxy_port_validator)
 
         self.proxy_user = QLineEdit()
-        self.proxy_user.setPlaceholderText(_("Proxy user"))
+        self.proxy_user.setPlaceholderText(_("Proxy username"))
         self.proxy_password = PasswordLineEdit()
-        self.proxy_password.setPlaceholderText(_("Password"))
-        self.proxy_password.setFixedWidth(fixed_width_port)
-
-        self.proxy_mode.currentIndexChanged.connect(self.set_proxy)
-        self.proxy_host.editingFinished.connect(self.set_proxy)
-        self.proxy_port.editingFinished.connect(self.set_proxy)
-        self.proxy_user.editingFinished.connect(self.set_proxy)
-        self.proxy_password.editingFinished.connect(self.set_proxy)
-
-        self.proxy_mode.currentIndexChanged.connect(self.proxy_settings_changed)
-        self.proxy_host.textEdited.connect(self.proxy_settings_changed)
-        self.proxy_port.textEdited.connect(self.proxy_settings_changed)
-        self.proxy_user.textEdited.connect(self.proxy_settings_changed)
-        self.proxy_password.textEdited.connect(self.proxy_settings_changed)
-
-        self.tor_cb = QCheckBox(_("Use Tor Proxy"))
-        self.tor_cb.setIcon(read_QIcon("tor_logo.png"))
-        self.tor_cb.hide()
-        self.tor_cb.clicked.connect(self.use_tor_proxy)
-
-        grid.addWidget(self.tor_cb, 1, 0, 1, 3)
-        grid.addWidget(self.proxy_cb, 2, 0, 1, 3)
-        grid.addWidget(HelpButton(_('Proxy settings apply to all connections: with Electrum servers, but also with third-party services.')), 2, 4)
-        grid.addWidget(self.proxy_mode, 4, 1)
-        grid.addWidget(self.proxy_host, 4, 2)
-        grid.addWidget(self.proxy_port, 4, 3)
-        grid.addWidget(self.proxy_user, 5, 2)
-        grid.addWidget(self.proxy_password, 5, 3)
-        grid.setRowStretch(7, 1)
-
-        # Blockchain Tab
-        grid = QGridLayout(blockchain_tab)
-        msg =  ' '.join([
-            _("Electrum connects to several nodes in order to download block headers and find out the longest blockchain."),
-            _("This blockchain is used to verify the transactions sent by your transaction server.")
-        ])
-        self.status_label = QLabel('')
-        grid.addWidget(QLabel(_('Status') + ':'), 0, 0)
-        grid.addWidget(self.status_label, 0, 1, 1, 3)
-        grid.addWidget(HelpButton(msg), 0, 4)
+        self.proxy_password.setPlaceholderText(_("Proxy password"))
 
-        self.autoconnect_cb = QCheckBox(_('Select server automatically'))
-        self.autoconnect_cb.setEnabled(self.config.cv.NETWORK_AUTO_CONNECT.is_modifiable())
-        self.autoconnect_cb.clicked.connect(self.set_server)
-        self.autoconnect_cb.clicked.connect(self.update)
-        msg = ' '.join([
-            _("If auto-connect is enabled, Electrum will always use a server that is on the longest blockchain."),
-            _("If it is disabled, you have to choose a server you want to use. Electrum will warn you if your server is lagging.")
-        ])
-        grid.addWidget(self.autoconnect_cb, 1, 0, 1, 3)
-        grid.addWidget(HelpButton(msg), 1, 4)
+        grid = QGridLayout(self)
+        grid.setSpacing(8)
 
-        self.server_e = QLineEdit()
-        self.server_e.setFixedWidth(fixed_width_hostname + fixed_width_port)
-        self.server_e.editingFinished.connect(self.set_server)
-        msg = _("Electrum sends your wallet addresses to a single server, in order to receive your transaction history.")
-        grid.addWidget(QLabel(_('Server') + ':'), 2, 0)
-        grid.addWidget(self.server_e, 2, 1, 1, 3)
-        grid.addWidget(HelpButton(msg), 2, 4)
+        grid.addWidget(self.proxy_cb, 0, 0, 1, 4)
+        proxy_helpbutton = HelpButton(
+            _('Proxy settings apply to all connections: with Electrum servers, but also with third-party services.'))
+        grid.addWidget(proxy_helpbutton, 0, 4, alignment=Qt.AlignmentFlag.AlignRight)
+        grid.addWidget(self.proxy_mode, 1, 0, 1, 1)
+        grid.addWidget(self.proxy_host, 1, 1, 1, 3)
+        grid.addWidget(self.proxy_port, 1, 4, 1, 1)
+        grid.addWidget(self.proxy_user, 2, 1, 1, 2)
+        grid.addWidget(self.proxy_password, 2, 3, 1, 2)
 
-        self.height_label = QLabel('')
-        msg = _('This is the height of your local copy of the blockchain.')
-        grid.addWidget(QLabel(_('Blockchain') + ':'), 3, 0)
-        grid.addWidget(self.height_label, 3, 1)
-        grid.addWidget(HelpButton(msg), 3, 4)
+        spacer = QVBoxLayout()
+        spacer.addStretch(1)
+        grid.addLayout(spacer, 3, 0, 1, 5)
 
-        self.split_label = QLabel('')
-        grid.addWidget(self.split_label, 4, 0, 1, 3)
+        self.detect_button = QPushButton(_('Detect TOR proxy'))
+        grid.addWidget(self.detect_button, 4, 0, 1, 5, alignment=Qt.AlignmentFlag.AlignHCenter)
path: electrum/gui/qt/network_dialog.py
user: login: SomberNight
body: This looks really weird.
<details>
 <summary>pic1</summary>

![proxy1](https://github.com/user-attachments/assets/29f2c8fe-6a4e-46a0-a1b0-f6e9535be56e)
</details>

I mean... wtf? :D

-----

Just put it here:
<details>
 <summary>pic2</summary>

![proxy2](https://github.com/user-attachments/assets/b7db1d52-970c-4981-aec9-d06ab96b3971)
</details>

created_at: 2025-03-04T18:45:53Z
author_association: MEMBER
diff_hunk: @@ -227,375 +215,162 @@ def update(self):
         super().update()
 
 
-class NetworkChoiceLayout(object):
-    # TODO consolidate to ProxyWidget+ServerWidget
-    # TODO TorDetector is unnecessary, Network tests socks5 peer and detects Tor
-    # TODO apply on editingFinished is not ideal, separate Apply button and on Close?
-    def __init__(self, network: Network, config: 'SimpleConfig', wizard=False):
-        self.network = network
-        self.config = config
-        self.tor_proxy = None
+class ProxyWidget(QWidget):
+    PROXY_MODES = {
+        'socks4': 'SOCKS4',
+        'socks5': 'SOCKS5/TOR'
+    }
 
-        self.tabs = tabs = QTabWidget()
-        self._proxy_tab = proxy_tab = QWidget()
-        blockchain_tab = QWidget()
-        tabs.addTab(blockchain_tab, _('Overview'))
-        tabs.addTab(proxy_tab, _('Proxy'))
-        tabs.currentChanged.connect(self._on_tab_changed)
+    torProbeFinished = pyqtSignal([str, int], arguments=['host', 'port'])
 
-        fixed_width_hostname = 24 * char_width_in_lineedit()
-        fixed_width_port = 6 * char_width_in_lineedit()
+    def __init__(self, network: Network, parent=None):
+        super().__init__(parent)
+        self.network = network
+        self.config = network.config
 
-        # Proxy tab
-        grid = QGridLayout(proxy_tab)
-        grid.setSpacing(8)
+        fixed_width_port = 6 * char_width_in_lineedit()
 
-        # proxy setting
+        # proxy setting.
         self.proxy_cb = QCheckBox(_('Use proxy'))
-        self.proxy_cb.clicked.connect(self.check_disable_proxy)
-        self.proxy_cb.clicked.connect(self.set_proxy)
-
         self.proxy_mode = QComboBox()
-        self.proxy_mode.addItems(['SOCKS4', 'SOCKS5'])
+        for k, v in self.PROXY_MODES.items():
+            self.proxy_mode.addItem(v, k)
+        self.proxy_mode.setCurrentIndex(1)
         self.proxy_host = QLineEdit()
-        self.proxy_host.setFixedWidth(fixed_width_hostname)
         self.proxy_port = QLineEdit()
         self.proxy_port.setFixedWidth(fixed_width_port)
         self.proxy_port_validator = QIntValidator(1, 65535)
         self.proxy_port.setValidator(self.proxy_port_validator)
 
         self.proxy_user = QLineEdit()
-        self.proxy_user.setPlaceholderText(_("Proxy user"))
+        self.proxy_user.setPlaceholderText(_("Proxy username"))
         self.proxy_password = PasswordLineEdit()
-        self.proxy_password.setPlaceholderText(_("Password"))
-        self.proxy_password.setFixedWidth(fixed_width_port)
-
-        self.proxy_mode.currentIndexChanged.connect(self.set_proxy)
-        self.proxy_host.editingFinished.connect(self.set_proxy)
-        self.proxy_port.editingFinished.connect(self.set_proxy)
-        self.proxy_user.editingFinished.connect(self.set_proxy)
-        self.proxy_password.editingFinished.connect(self.set_proxy)
-
-        self.proxy_mode.currentIndexChanged.connect(self.proxy_settings_changed)
-        self.proxy_host.textEdited.connect(self.proxy_settings_changed)
-        self.proxy_port.textEdited.connect(self.proxy_settings_changed)
-        self.proxy_user.textEdited.connect(self.proxy_settings_changed)
-        self.proxy_password.textEdited.connect(self.proxy_settings_changed)
-
-        self.tor_cb = QCheckBox(_("Use Tor Proxy"))
-        self.tor_cb.setIcon(read_QIcon("tor_logo.png"))
-        self.tor_cb.hide()
-        self.tor_cb.clicked.connect(self.use_tor_proxy)
-
-        grid.addWidget(self.tor_cb, 1, 0, 1, 3)
-        grid.addWidget(self.proxy_cb, 2, 0, 1, 3)
-        grid.addWidget(HelpButton(_('Proxy settings apply to all connections: with Electrum servers, but also with third-party services.')), 2, 4)
-        grid.addWidget(self.proxy_mode, 4, 1)
-        grid.addWidget(self.proxy_host, 4, 2)
-        grid.addWidget(self.proxy_port, 4, 3)
-        grid.addWidget(self.proxy_user, 5, 2)
-        grid.addWidget(self.proxy_password, 5, 3)
-        grid.setRowStretch(7, 1)
-
-        # Blockchain Tab
-        grid = QGridLayout(blockchain_tab)
-        msg =  ' '.join([
-            _("Electrum connects to several nodes in order to download block headers and find out the longest blockchain."),
-            _("This blockchain is used to verify the transactions sent by your transaction server.")
-        ])
-        self.status_label = QLabel('')
-        grid.addWidget(QLabel(_('Status') + ':'), 0, 0)
-        grid.addWidget(self.status_label, 0, 1, 1, 3)
-        grid.addWidget(HelpButton(msg), 0, 4)
+        self.proxy_password.setPlaceholderText(_("Proxy password"))
 
-        self.autoconnect_cb = QCheckBox(_('Select server automatically'))
-        self.autoconnect_cb.setEnabled(self.config.cv.NETWORK_AUTO_CONNECT.is_modifiable())
-        self.autoconnect_cb.clicked.connect(self.set_server)
-        self.autoconnect_cb.clicked.connect(self.update)
-        msg = ' '.join([
-            _("If auto-connect is enabled, Electrum will always use a server that is on the longest blockchain."),
-            _("If it is disabled, you have to choose a server you want to use. Electrum will warn you if your server is lagging.")
-        ])
-        grid.addWidget(self.autoconnect_cb, 1, 0, 1, 3)
-        grid.addWidget(HelpButton(msg), 1, 4)
+        grid = QGridLayout(self)
+        grid.setSpacing(8)
 
-        self.server_e = QLineEdit()
-        self.server_e.setFixedWidth(fixed_width_hostname + fixed_width_port)
-        self.server_e.editingFinished.connect(self.set_server)
-        msg = _("Electrum sends your wallet addresses to a single server, in order to receive your transaction history.")
-        grid.addWidget(QLabel(_('Server') + ':'), 2, 0)
-        grid.addWidget(self.server_e, 2, 1, 1, 3)
-        grid.addWidget(HelpButton(msg), 2, 4)
+        grid.addWidget(self.proxy_cb, 0, 0, 1, 4)
+        proxy_helpbutton = HelpButton(
+            _('Proxy settings apply to all connections: with Electrum servers, but also with third-party services.'))
+        grid.addWidget(proxy_helpbutton, 0, 4, alignment=Qt.AlignmentFlag.AlignRight)
+        grid.addWidget(self.proxy_mode, 1, 0, 1, 1)
+        grid.addWidget(self.proxy_host, 1, 1, 1, 3)
+        grid.addWidget(self.proxy_port, 1, 4, 1, 1)
+        grid.addWidget(self.proxy_user, 2, 1, 1, 2)
+        grid.addWidget(self.proxy_password, 2, 3, 1, 2)
 
-        self.height_label = QLabel('')
-        msg = _('This is the height of your local copy of the blockchain.')
-        grid.addWidget(QLabel(_('Blockchain') + ':'), 3, 0)
-        grid.addWidget(self.height_label, 3, 1)
-        grid.addWidget(HelpButton(msg), 3, 4)
+        spacer = QVBoxLayout()
+        spacer.addStretch(1)
+        grid.addLayout(spacer, 3, 0, 1, 5)
 
-        self.split_label = QLabel('')
-        grid.addWidget(self.split_label, 4, 0, 1, 3)
+        self.detect_button = QPushButton(_('Detect TOR proxy'))
path: electrum/gui/qt/network_dialog.py
user: login: SomberNight
body: I think the preferred official styling is `Tor` (not all caps).
(this is not the only place this appears)
created_at: 2025-03-04T18:47:14Z
author_association: MEMBER
diff_hunk: @@ -109,5 +98,30 @@ Item {
                 enabled: proxy_enabled_cb.checked
             }
         }
+
+        Pane {
+            Layout.alignment: Qt.AlignHCenter
+            Layout.topMargin: constants.paddingLarge
+            padding: 0
+            background: Rectangle {
+                color: Material.dialogColor
+            }
+            FlatButton {
+                enabled: proxy_enabled_cb.checked
+                text: qsTr('Detect TOR proxy')
+                onClicked: Network.probeTor()
+            }
path: electrum/gui/qml/components/controls/ProxyConfig.qml
user: login: SomberNight
body: cool to have this functionality in qml!

(but same comment as in qt: there is no feedback after clicking and it takes a loooong time)
created_at: 2025-03-04T18:50:09Z
author_association: MEMBER
diff_hunk: @@ -227,375 +215,162 @@ def update(self):
         super().update()
 
 
-class NetworkChoiceLayout(object):
-    # TODO consolidate to ProxyWidget+ServerWidget
-    # TODO TorDetector is unnecessary, Network tests socks5 peer and detects Tor
-    # TODO apply on editingFinished is not ideal, separate Apply button and on Close?
-    def __init__(self, network: Network, config: 'SimpleConfig', wizard=False):
-        self.network = network
-        self.config = config
-        self.tor_proxy = None
+class ProxyWidget(QWidget):
+    PROXY_MODES = {
+        'socks4': 'SOCKS4',
+        'socks5': 'SOCKS5/TOR'
+    }
 
-        self.tabs = tabs = QTabWidget()
-        self._proxy_tab = proxy_tab = QWidget()
-        blockchain_tab = QWidget()
-        tabs.addTab(blockchain_tab, _('Overview'))
-        tabs.addTab(proxy_tab, _('Proxy'))
-        tabs.currentChanged.connect(self._on_tab_changed)
+    torProbeFinished = pyqtSignal([str, int], arguments=['host', 'port'])
 
-        fixed_width_hostname = 24 * char_width_in_lineedit()
-        fixed_width_port = 6 * char_width_in_lineedit()
+    def __init__(self, network: Network, parent=None):
+        super().__init__(parent)
+        self.network = network
+        self.config = network.config
 
-        # Proxy tab
-        grid = QGridLayout(proxy_tab)
-        grid.setSpacing(8)
+        fixed_width_port = 6 * char_width_in_lineedit()
 
-        # proxy setting
+        # proxy setting.
         self.proxy_cb = QCheckBox(_('Use proxy'))
-        self.proxy_cb.clicked.connect(self.check_disable_proxy)
-        self.proxy_cb.clicked.connect(self.set_proxy)
-
         self.proxy_mode = QComboBox()
-        self.proxy_mode.addItems(['SOCKS4', 'SOCKS5'])
+        for k, v in self.PROXY_MODES.items():
+            self.proxy_mode.addItem(v, k)
+        self.proxy_mode.setCurrentIndex(1)
         self.proxy_host = QLineEdit()
-        self.proxy_host.setFixedWidth(fixed_width_hostname)
         self.proxy_port = QLineEdit()
         self.proxy_port.setFixedWidth(fixed_width_port)
         self.proxy_port_validator = QIntValidator(1, 65535)
         self.proxy_port.setValidator(self.proxy_port_validator)
 
         self.proxy_user = QLineEdit()
-        self.proxy_user.setPlaceholderText(_("Proxy user"))
+        self.proxy_user.setPlaceholderText(_("Proxy username"))
         self.proxy_password = PasswordLineEdit()
-        self.proxy_password.setPlaceholderText(_("Password"))
-        self.proxy_password.setFixedWidth(fixed_width_port)
-
-        self.proxy_mode.currentIndexChanged.connect(self.set_proxy)
-        self.proxy_host.editingFinished.connect(self.set_proxy)
-        self.proxy_port.editingFinished.connect(self.set_proxy)
-        self.proxy_user.editingFinished.connect(self.set_proxy)
-        self.proxy_password.editingFinished.connect(self.set_proxy)
-
-        self.proxy_mode.currentIndexChanged.connect(self.proxy_settings_changed)
-        self.proxy_host.textEdited.connect(self.proxy_settings_changed)
-        self.proxy_port.textEdited.connect(self.proxy_settings_changed)
-        self.proxy_user.textEdited.connect(self.proxy_settings_changed)
-        self.proxy_password.textEdited.connect(self.proxy_settings_changed)
-
-        self.tor_cb = QCheckBox(_("Use Tor Proxy"))
-        self.tor_cb.setIcon(read_QIcon("tor_logo.png"))
-        self.tor_cb.hide()
-        self.tor_cb.clicked.connect(self.use_tor_proxy)
-
-        grid.addWidget(self.tor_cb, 1, 0, 1, 3)
-        grid.addWidget(self.proxy_cb, 2, 0, 1, 3)
-        grid.addWidget(HelpButton(_('Proxy settings apply to all connections: with Electrum servers, but also with third-party services.')), 2, 4)
-        grid.addWidget(self.proxy_mode, 4, 1)
-        grid.addWidget(self.proxy_host, 4, 2)
-        grid.addWidget(self.proxy_port, 4, 3)
-        grid.addWidget(self.proxy_user, 5, 2)
-        grid.addWidget(self.proxy_password, 5, 3)
-        grid.setRowStretch(7, 1)
-
-        # Blockchain Tab
-        grid = QGridLayout(blockchain_tab)
-        msg =  ' '.join([
-            _("Electrum connects to several nodes in order to download block headers and find out the longest blockchain."),
-            _("This blockchain is used to verify the transactions sent by your transaction server.")
-        ])
-        self.status_label = QLabel('')
-        grid.addWidget(QLabel(_('Status') + ':'), 0, 0)
-        grid.addWidget(self.status_label, 0, 1, 1, 3)
-        grid.addWidget(HelpButton(msg), 0, 4)
+        self.proxy_password.setPlaceholderText(_("Proxy password"))
 
-        self.autoconnect_cb = QCheckBox(_('Select server automatically'))
-        self.autoconnect_cb.setEnabled(self.config.cv.NETWORK_AUTO_CONNECT.is_modifiable())
-        self.autoconnect_cb.clicked.connect(self.set_server)
-        self.autoconnect_cb.clicked.connect(self.update)
-        msg = ' '.join([
-            _("If auto-connect is enabled, Electrum will always use a server that is on the longest blockchain."),
-            _("If it is disabled, you have to choose a server you want to use. Electrum will warn you if your server is lagging.")
-        ])
-        grid.addWidget(self.autoconnect_cb, 1, 0, 1, 3)
-        grid.addWidget(HelpButton(msg), 1, 4)
+        grid = QGridLayout(self)
+        grid.setSpacing(8)
 
-        self.server_e = QLineEdit()
-        self.server_e.setFixedWidth(fixed_width_hostname + fixed_width_port)
-        self.server_e.editingFinished.connect(self.set_server)
-        msg = _("Electrum sends your wallet addresses to a single server, in order to receive your transaction history.")
-        grid.addWidget(QLabel(_('Server') + ':'), 2, 0)
-        grid.addWidget(self.server_e, 2, 1, 1, 3)
-        grid.addWidget(HelpButton(msg), 2, 4)
+        grid.addWidget(self.proxy_cb, 0, 0, 1, 4)
+        proxy_helpbutton = HelpButton(
+            _('Proxy settings apply to all connections: with Electrum servers, but also with third-party services.'))
+        grid.addWidget(proxy_helpbutton, 0, 4, alignment=Qt.AlignmentFlag.AlignRight)
+        grid.addWidget(self.proxy_mode, 1, 0, 1, 1)
+        grid.addWidget(self.proxy_host, 1, 1, 1, 3)
+        grid.addWidget(self.proxy_port, 1, 4, 1, 1)
+        grid.addWidget(self.proxy_user, 2, 1, 1, 2)
+        grid.addWidget(self.proxy_password, 2, 3, 1, 2)
 
-        self.height_label = QLabel('')
-        msg = _('This is the height of your local copy of the blockchain.')
-        grid.addWidget(QLabel(_('Blockchain') + ':'), 3, 0)
-        grid.addWidget(self.height_label, 3, 1)
-        grid.addWidget(HelpButton(msg), 3, 4)
+        spacer = QVBoxLayout()
+        spacer.addStretch(1)
+        grid.addLayout(spacer, 3, 0, 1, 5)
 
-        self.split_label = QLabel('')
-        grid.addWidget(self.split_label, 4, 0, 1, 3)
+        self.detect_button = QPushButton(_('Detect TOR proxy'))
+        grid.addWidget(self.detect_button, 4, 0, 1, 5, alignment=Qt.AlignmentFlag.AlignHCenter)
 
-        self.nodes_list_widget = NodesListWidget(network=self.network)
-        self.nodes_list_widget.followServer.connect(self.follow_server)
-        self.nodes_list_widget.followChain.connect(self.follow_branch)
-
-        def do_set_server(server):
-            self.server_e.setText(server)
-            self.set_server()
-        self.nodes_list_widget.setServer.connect(do_set_server)
-        grid.addWidget(self.nodes_list_widget, 6, 0, 1, 5)
-
-        vbox = QVBoxLayout()
-        vbox.addWidget(tabs)
-        self.layout_ = vbox
-        # tor detector
-        self.td = td = TorDetector()
-        td.found_proxy.connect(self.suggest_proxy)
-        td.start()
+        spacer = QVBoxLayout()
+        spacer.addStretch(1)
+        grid.addLayout(spacer, 5, 0, 1, 5)
 
-        self.fill_in_proxy_settings()
+        self.update_from_config()
         self.update()
 
-    def clean_up(self):
-        if self.td:
-            self.td.found_proxy.disconnect()
-            self.td.stop()
-            self.td = None
+        # connect signal handlers after init from config
+        self.proxy_cb.stateChanged.connect(self.on_proxy_enable_toggle)
+        self.proxy_mode.currentIndexChanged.connect(self.on_proxy_settings_changed)
+        self.proxy_host.editingFinished.connect(self.on_proxy_settings_changed)
+        self.proxy_port.editingFinished.connect(self.on_proxy_settings_changed)
+        self.proxy_user.editingFinished.connect(self.on_proxy_settings_changed)
+        self.proxy_password.editingFinished.connect(self.on_proxy_settings_changed)
+        self.detect_button.clicked.connect(self.detect_tor)
 
-    def check_disable_proxy(self, b):
-        if not self.config.cv.NETWORK_PROXY.is_modifiable():
-            b = False
-        for w in [self.proxy_mode, self.proxy_host, self.proxy_port, self.proxy_user, self.proxy_password]:
-            w.setEnabled(b)
-
-    def enable_set_server(self):
-        if self.config.cv.NETWORK_SERVER.is_modifiable():
-            enabled = not self.autoconnect_cb.isChecked()
-            self.server_e.setEnabled(enabled)
-        else:
-            for w in [self.autoconnect_cb, self.server_e, self.nodes_list_widget]:
-                w.setEnabled(False)
+        self.torProbeFinished.connect(self.on_tor_probe_finished)
 
     def update(self):
-        net_params = self.network.get_parameters()
-        server = net_params.server
-        auto_connect = net_params.auto_connect
-        if not self.server_e.hasFocus():
-            self.server_e.setText(server.to_friendly_name())
-        self.autoconnect_cb.setChecked(auto_connect)
-
-        height_str = "%d "%(self.network.get_local_height()) + _('blocks')
-        self.height_label.setText(height_str)
-        self.status_label.setText(self.network.get_status())
-        chains = self.network.get_blockchains()
-        if len(chains) > 1:
-            chain = self.network.blockchain()
-            forkpoint = chain.get_max_forkpoint()
-            name = chain.get_name()
-            msg = _('Chain split detected at block {0}').format(forkpoint) + '\n'
-            msg += (_('You are following branch') if auto_connect else _('Your server is on branch'))+ ' ' + name
-            msg += ' (%d %s)' % (chain.get_branch_size(), _('blocks'))
-        else:
-            msg = ''
-        self.split_label.setText(msg)
-        self.nodes_list_widget.update()
-        self.enable_set_server()
-
-    def fill_in_proxy_settings(self):
-        proxy_config = self.network.get_parameters().proxy
-        if not proxy_config:
-            proxy_config = {"mode": "none", "host": "localhost", "port": "9050"}
-
-        b = proxy_config.get('mode') != "none"
-        self.check_disable_proxy(b)
-        if b:
-            self.proxy_cb.setChecked(True)
-            self.proxy_mode.setCurrentIndex(
-                self.proxy_mode.findText(str(proxy_config.get("mode").upper())))
-
-        self.proxy_host.setText(proxy_config.get("host"))
-        self.proxy_port.setText(proxy_config.get("port"))
-        self.proxy_user.setText(proxy_config.get("user", ""))
-        self.proxy_password.setText(proxy_config.get("password", ""))
-
-    def layout(self):
-        return self.layout_
-
-    def follow_branch(self, chain_id):
-        self.network.run_from_another_thread(self.network.follow_chain_given_id(chain_id))
-        self.update()
-
-    def follow_server(self, server: ServerAddr):
-        self.network.run_from_another_thread(self.network.follow_chain_given_server(server))
-        self.update()
-
-    def accept(self):
-        pass
-
-    def set_server(self):
-        net_params = self.network.get_parameters()
-        try:
-            server = ServerAddr.from_str_with_inference(str(self.server_e.text()))
-            if not server: raise Exception("failed to parse")
-        except Exception:
+        enabled = self.proxy_cb.isChecked() and self.config.cv.NETWORK_PROXY.is_modifiable()
+        for item in [
+                self.proxy_mode, self.proxy_host, self.proxy_port, self.proxy_user, self.proxy_password,
+                self.detect_button
+        ]:
+            item.setEnabled(enabled)
+
+        if not self.proxy_port.hasAcceptableInput():
             return
-        net_params = net_params._replace(server=server,
-                                         auto_connect=self.autoconnect_cb.isChecked())
-        self.network.run_from_another_thread(self.network.set_parameters(net_params))
 
-    def set_proxy(self):
         net_params = self.network.get_parameters()
-        if self.proxy_cb.isChecked():
-            if not self.proxy_port.hasAcceptableInput():
-                return
-            proxy = {'mode':str(self.proxy_mode.currentText()).lower(),
-                     'host':str(self.proxy_host.text()),
-                     'port':str(self.proxy_port.text()),
-                     'user':str(self.proxy_user.text()),
-                     'password':str(self.proxy_password.text())}
-        else:
-            proxy = None
-            self.tor_cb.setChecked(False)
+        proxy = self.get_proxy_settings()
         net_params = net_params._replace(proxy=proxy)
         self.network.run_from_another_thread(self.network.set_parameters(net_params))
 
-    def _on_tab_changed(self):
-        if self.tabs.currentWidget() is self._proxy_tab:
-            self.td.trigger_rescan()
+    def update_from_config(self):
+        proxy = ProxySettings.from_config(self.config)
+        self.proxy_cb.setChecked(proxy.enabled)
+        self.proxy_mode.setCurrentText(self.PROXY_MODES.get(proxy.mode))
+        self.proxy_host.setText(proxy.host)
+        self.proxy_port.setText(proxy.port)
+        self.proxy_user.setText(proxy.user)
+        self.proxy_password.setText(proxy.password)
 
-    def suggest_proxy(self, found_proxy):
-        if found_proxy is None:
-            self.tor_cb.hide()
-            return
-        self.tor_proxy = found_proxy
-        self.tor_cb.setText(_("Use Tor proxy at port {}").format(str(found_proxy[1])))
-        if (self.proxy_cb.isChecked()
-                and self.proxy_mode.currentIndex() == self.proxy_mode.findText('SOCKS5')
-                and self.proxy_host.text() == "127.0.0.1"
-                and self.proxy_port.text() == str(found_proxy[1])):
-            self.tor_cb.setChecked(True)
-        self.tor_cb.show()
-
-    def use_tor_proxy(self, use_it):
-        if not use_it:
-            self.proxy_cb.setChecked(False)
-        else:
-            socks5_mode_index = self.proxy_mode.findText('SOCKS5')
-            if socks5_mode_index == -1:
-                _logger.info("can't find proxy_mode 'SOCKS5'")
-                return
-            self.proxy_mode.setCurrentIndex(socks5_mode_index)
-            self.proxy_host.setText("127.0.0.1")
-            self.proxy_port.setText(str(self.tor_proxy[1]))
-            self.proxy_user.setText("")
-            self.proxy_password.setText("")
-            self.tor_cb.setChecked(True)
-            self.proxy_cb.setChecked(True)
-        self.check_disable_proxy(use_it)
-        self.set_proxy()
-
-    def proxy_settings_changed(self):
-        self.tor_cb.setChecked(False)
-
-
-class TorDetector(QThread):
-    found_proxy = pyqtSignal(object)
-
-    def __init__(self):
-        QThread.__init__(self)
-        self._work_to_do_evt = threading.Event()
-        self._stopping = False
-
-    def run(self):
-        while True:
-            # do rescan
-            net_addr = detect_tor_socks_proxy()
-            self.found_proxy.emit(net_addr)
-            # wait until triggered
-            self._work_to_do_evt.wait()
-            self._work_to_do_evt.clear()
-            if self._stopping:
-                return
-
-    def trigger_rescan(self) -> None:
-        self._work_to_do_evt.set()
-
-    def stop(self):
-        self._stopping = True
-        self._work_to_do_evt.set()
-        self.exit()
-        self.wait()
+        if not self.config.cv.NETWORK_PROXY.is_modifiable():
+            for w in [
+                    self.proxy_cb, self.proxy_mode, self.proxy_host, self.proxy_port,
+                    self.proxy_user, self.proxy_password, self.detect_button
+            ]:
+                w.setEnabled(False)
 
+    def on_proxy_enable_toggle(self):
+        # probe if enabled and no pre-existing settings
+        # if self.proxy_cb.isChecked() and (not self.proxy_host.text() or not self.proxy_port.text()):
+        #     self.detect_tor()
+        self.update()
 
-class ProxyWidget(QWidget):
-    def __init__(self, parent=None):
-        super().__init__(parent)
+    def on_proxy_settings_changed(self):
+        self.update()
 
-        fixed_width_hostname = 24 * char_width_in_lineedit()
-        fixed_width_port = 6 * char_width_in_lineedit()
+    def get_proxy_settings(self) -> ProxySettings:
+        proxy = ProxySettings()
+        proxy.enabled = self.proxy_cb.isChecked()
+        proxy.mode = self.proxy_mode.currentData()
+        proxy.host = self.proxy_host.text()
+        proxy.port = self.proxy_port.text()
+        proxy.user = self.proxy_user.text()
+        proxy.password = self.proxy_password.text()
+        return proxy
 
-        grid = QGridLayout(self)
-        grid.setSpacing(8)
+    def detect_tor(self):
+        ProxySettings.probe_tor(self.torProbeFinished.emit)  # via signal
path: electrum/gui/qt/network_dialog.py
user: login: accumulator
body: On my system the probe is near-instant, and as the probe is only addressing localhost I'd assume there was no need for an indicator. Maybe on your system Tor is socket activated? I'll add a simple spinner.
created_at: 2025-03-05T08:22:31Z
author_association: MEMBER
diff_hunk: @@ -227,375 +215,162 @@ def update(self):
         super().update()
 
 
-class NetworkChoiceLayout(object):
-    # TODO consolidate to ProxyWidget+ServerWidget
-    # TODO TorDetector is unnecessary, Network tests socks5 peer and detects Tor
-    # TODO apply on editingFinished is not ideal, separate Apply button and on Close?
-    def __init__(self, network: Network, config: 'SimpleConfig', wizard=False):
-        self.network = network
-        self.config = config
-        self.tor_proxy = None
+class ProxyWidget(QWidget):
+    PROXY_MODES = {
+        'socks4': 'SOCKS4',
+        'socks5': 'SOCKS5/TOR'
+    }
 
-        self.tabs = tabs = QTabWidget()
-        self._proxy_tab = proxy_tab = QWidget()
-        blockchain_tab = QWidget()
-        tabs.addTab(blockchain_tab, _('Overview'))
-        tabs.addTab(proxy_tab, _('Proxy'))
-        tabs.currentChanged.connect(self._on_tab_changed)
+    torProbeFinished = pyqtSignal([str, int], arguments=['host', 'port'])
 
-        fixed_width_hostname = 24 * char_width_in_lineedit()
-        fixed_width_port = 6 * char_width_in_lineedit()
+    def __init__(self, network: Network, parent=None):
+        super().__init__(parent)
+        self.network = network
+        self.config = network.config
 
-        # Proxy tab
-        grid = QGridLayout(proxy_tab)
-        grid.setSpacing(8)
+        fixed_width_port = 6 * char_width_in_lineedit()
 
-        # proxy setting
+        # proxy setting.
         self.proxy_cb = QCheckBox(_('Use proxy'))
-        self.proxy_cb.clicked.connect(self.check_disable_proxy)
-        self.proxy_cb.clicked.connect(self.set_proxy)
-
         self.proxy_mode = QComboBox()
-        self.proxy_mode.addItems(['SOCKS4', 'SOCKS5'])
+        for k, v in self.PROXY_MODES.items():
+            self.proxy_mode.addItem(v, k)
+        self.proxy_mode.setCurrentIndex(1)
         self.proxy_host = QLineEdit()
-        self.proxy_host.setFixedWidth(fixed_width_hostname)
         self.proxy_port = QLineEdit()
         self.proxy_port.setFixedWidth(fixed_width_port)
         self.proxy_port_validator = QIntValidator(1, 65535)
         self.proxy_port.setValidator(self.proxy_port_validator)
 
         self.proxy_user = QLineEdit()
-        self.proxy_user.setPlaceholderText(_("Proxy user"))
+        self.proxy_user.setPlaceholderText(_("Proxy username"))
         self.proxy_password = PasswordLineEdit()
-        self.proxy_password.setPlaceholderText(_("Password"))
-        self.proxy_password.setFixedWidth(fixed_width_port)
-
-        self.proxy_mode.currentIndexChanged.connect(self.set_proxy)
-        self.proxy_host.editingFinished.connect(self.set_proxy)
-        self.proxy_port.editingFinished.connect(self.set_proxy)
-        self.proxy_user.editingFinished.connect(self.set_proxy)
-        self.proxy_password.editingFinished.connect(self.set_proxy)
-
-        self.proxy_mode.currentIndexChanged.connect(self.proxy_settings_changed)
-        self.proxy_host.textEdited.connect(self.proxy_settings_changed)
-        self.proxy_port.textEdited.connect(self.proxy_settings_changed)
-        self.proxy_user.textEdited.connect(self.proxy_settings_changed)
-        self.proxy_password.textEdited.connect(self.proxy_settings_changed)
-
-        self.tor_cb = QCheckBox(_("Use Tor Proxy"))
-        self.tor_cb.setIcon(read_QIcon("tor_logo.png"))
-        self.tor_cb.hide()
-        self.tor_cb.clicked.connect(self.use_tor_proxy)
-
-        grid.addWidget(self.tor_cb, 1, 0, 1, 3)
-        grid.addWidget(self.proxy_cb, 2, 0, 1, 3)
-        grid.addWidget(HelpButton(_('Proxy settings apply to all connections: with Electrum servers, but also with third-party services.')), 2, 4)
-        grid.addWidget(self.proxy_mode, 4, 1)
-        grid.addWidget(self.proxy_host, 4, 2)
-        grid.addWidget(self.proxy_port, 4, 3)
-        grid.addWidget(self.proxy_user, 5, 2)
-        grid.addWidget(self.proxy_password, 5, 3)
-        grid.setRowStretch(7, 1)
-
-        # Blockchain Tab
-        grid = QGridLayout(blockchain_tab)
-        msg =  ' '.join([
-            _("Electrum connects to several nodes in order to download block headers and find out the longest blockchain."),
-            _("This blockchain is used to verify the transactions sent by your transaction server.")
-        ])
-        self.status_label = QLabel('')
-        grid.addWidget(QLabel(_('Status') + ':'), 0, 0)
-        grid.addWidget(self.status_label, 0, 1, 1, 3)
-        grid.addWidget(HelpButton(msg), 0, 4)
+        self.proxy_password.setPlaceholderText(_("Proxy password"))
 
-        self.autoconnect_cb = QCheckBox(_('Select server automatically'))
-        self.autoconnect_cb.setEnabled(self.config.cv.NETWORK_AUTO_CONNECT.is_modifiable())
-        self.autoconnect_cb.clicked.connect(self.set_server)
-        self.autoconnect_cb.clicked.connect(self.update)
-        msg = ' '.join([
-            _("If auto-connect is enabled, Electrum will always use a server that is on the longest blockchain."),
-            _("If it is disabled, you have to choose a server you want to use. Electrum will warn you if your server is lagging.")
-        ])
-        grid.addWidget(self.autoconnect_cb, 1, 0, 1, 3)
-        grid.addWidget(HelpButton(msg), 1, 4)
+        grid = QGridLayout(self)
+        grid.setSpacing(8)
 
-        self.server_e = QLineEdit()
-        self.server_e.setFixedWidth(fixed_width_hostname + fixed_width_port)
-        self.server_e.editingFinished.connect(self.set_server)
-        msg = _("Electrum sends your wallet addresses to a single server, in order to receive your transaction history.")
-        grid.addWidget(QLabel(_('Server') + ':'), 2, 0)
-        grid.addWidget(self.server_e, 2, 1, 1, 3)
-        grid.addWidget(HelpButton(msg), 2, 4)
+        grid.addWidget(self.proxy_cb, 0, 0, 1, 4)
+        proxy_helpbutton = HelpButton(
+            _('Proxy settings apply to all connections: with Electrum servers, but also with third-party services.'))
+        grid.addWidget(proxy_helpbutton, 0, 4, alignment=Qt.AlignmentFlag.AlignRight)
+        grid.addWidget(self.proxy_mode, 1, 0, 1, 1)
+        grid.addWidget(self.proxy_host, 1, 1, 1, 3)
+        grid.addWidget(self.proxy_port, 1, 4, 1, 1)
+        grid.addWidget(self.proxy_user, 2, 1, 1, 2)
+        grid.addWidget(self.proxy_password, 2, 3, 1, 2)
 
-        self.height_label = QLabel('')
-        msg = _('This is the height of your local copy of the blockchain.')
-        grid.addWidget(QLabel(_('Blockchain') + ':'), 3, 0)
-        grid.addWidget(self.height_label, 3, 1)
-        grid.addWidget(HelpButton(msg), 3, 4)
+        spacer = QVBoxLayout()
+        spacer.addStretch(1)
+        grid.addLayout(spacer, 3, 0, 1, 5)
 
-        self.split_label = QLabel('')
-        grid.addWidget(self.split_label, 4, 0, 1, 3)
+        self.detect_button = QPushButton(_('Detect TOR proxy'))
+        grid.addWidget(self.detect_button, 4, 0, 1, 5, alignment=Qt.AlignmentFlag.AlignHCenter)
 
-        self.nodes_list_widget = NodesListWidget(network=self.network)
-        self.nodes_list_widget.followServer.connect(self.follow_server)
-        self.nodes_list_widget.followChain.connect(self.follow_branch)
-
-        def do_set_server(server):
-            self.server_e.setText(server)
-            self.set_server()
-        self.nodes_list_widget.setServer.connect(do_set_server)
-        grid.addWidget(self.nodes_list_widget, 6, 0, 1, 5)
-
-        vbox = QVBoxLayout()
-        vbox.addWidget(tabs)
-        self.layout_ = vbox
-        # tor detector
-        self.td = td = TorDetector()
-        td.found_proxy.connect(self.suggest_proxy)
-        td.start()
+        spacer = QVBoxLayout()
+        spacer.addStretch(1)
+        grid.addLayout(spacer, 5, 0, 1, 5)
 
-        self.fill_in_proxy_settings()
+        self.update_from_config()
         self.update()
 
-    def clean_up(self):
-        if self.td:
-            self.td.found_proxy.disconnect()
-            self.td.stop()
-            self.td = None
+        # connect signal handlers after init from config
+        self.proxy_cb.stateChanged.connect(self.on_proxy_enable_toggle)
+        self.proxy_mode.currentIndexChanged.connect(self.on_proxy_settings_changed)
+        self.proxy_host.editingFinished.connect(self.on_proxy_settings_changed)
+        self.proxy_port.editingFinished.connect(self.on_proxy_settings_changed)
+        self.proxy_user.editingFinished.connect(self.on_proxy_settings_changed)
+        self.proxy_password.editingFinished.connect(self.on_proxy_settings_changed)
+        self.detect_button.clicked.connect(self.detect_tor)
 
-    def check_disable_proxy(self, b):
-        if not self.config.cv.NETWORK_PROXY.is_modifiable():
-            b = False
-        for w in [self.proxy_mode, self.proxy_host, self.proxy_port, self.proxy_user, self.proxy_password]:
-            w.setEnabled(b)
-
-    def enable_set_server(self):
-        if self.config.cv.NETWORK_SERVER.is_modifiable():
-            enabled = not self.autoconnect_cb.isChecked()
-            self.server_e.setEnabled(enabled)
-        else:
-            for w in [self.autoconnect_cb, self.server_e, self.nodes_list_widget]:
-                w.setEnabled(False)
+        self.torProbeFinished.connect(self.on_tor_probe_finished)
 
     def update(self):
-        net_params = self.network.get_parameters()
-        server = net_params.server
-        auto_connect = net_params.auto_connect
-        if not self.server_e.hasFocus():
-            self.server_e.setText(server.to_friendly_name())
-        self.autoconnect_cb.setChecked(auto_connect)
-
-        height_str = "%d "%(self.network.get_local_height()) + _('blocks')
-        self.height_label.setText(height_str)
-        self.status_label.setText(self.network.get_status())
-        chains = self.network.get_blockchains()
-        if len(chains) > 1:
-            chain = self.network.blockchain()
-            forkpoint = chain.get_max_forkpoint()
-            name = chain.get_name()
-            msg = _('Chain split detected at block {0}').format(forkpoint) + '\n'
-            msg += (_('You are following branch') if auto_connect else _('Your server is on branch'))+ ' ' + name
-            msg += ' (%d %s)' % (chain.get_branch_size(), _('blocks'))
-        else:
-            msg = ''
-        self.split_label.setText(msg)
-        self.nodes_list_widget.update()
-        self.enable_set_server()
-
-    def fill_in_proxy_settings(self):
-        proxy_config = self.network.get_parameters().proxy
-        if not proxy_config:
-            proxy_config = {"mode": "none", "host": "localhost", "port": "9050"}
-
-        b = proxy_config.get('mode') != "none"
-        self.check_disable_proxy(b)
-        if b:
-            self.proxy_cb.setChecked(True)
-            self.proxy_mode.setCurrentIndex(
-                self.proxy_mode.findText(str(proxy_config.get("mode").upper())))
-
-        self.proxy_host.setText(proxy_config.get("host"))
-        self.proxy_port.setText(proxy_config.get("port"))
-        self.proxy_user.setText(proxy_config.get("user", ""))
-        self.proxy_password.setText(proxy_config.get("password", ""))
-
-    def layout(self):
-        return self.layout_
-
-    def follow_branch(self, chain_id):
-        self.network.run_from_another_thread(self.network.follow_chain_given_id(chain_id))
-        self.update()
-
-    def follow_server(self, server: ServerAddr):
-        self.network.run_from_another_thread(self.network.follow_chain_given_server(server))
-        self.update()
-
-    def accept(self):
-        pass
-
-    def set_server(self):
-        net_params = self.network.get_parameters()
-        try:
-            server = ServerAddr.from_str_with_inference(str(self.server_e.text()))
-            if not server: raise Exception("failed to parse")
-        except Exception:
+        enabled = self.proxy_cb.isChecked() and self.config.cv.NETWORK_PROXY.is_modifiable()
+        for item in [
+                self.proxy_mode, self.proxy_host, self.proxy_port, self.proxy_user, self.proxy_password,
+                self.detect_button
+        ]:
+            item.setEnabled(enabled)
+
+        if not self.proxy_port.hasAcceptableInput():
             return
-        net_params = net_params._replace(server=server,
-                                         auto_connect=self.autoconnect_cb.isChecked())
-        self.network.run_from_another_thread(self.network.set_parameters(net_params))
 
-    def set_proxy(self):
         net_params = self.network.get_parameters()
-        if self.proxy_cb.isChecked():
-            if not self.proxy_port.hasAcceptableInput():
-                return
-            proxy = {'mode':str(self.proxy_mode.currentText()).lower(),
-                     'host':str(self.proxy_host.text()),
-                     'port':str(self.proxy_port.text()),
-                     'user':str(self.proxy_user.text()),
-                     'password':str(self.proxy_password.text())}
-        else:
-            proxy = None
-            self.tor_cb.setChecked(False)
+        proxy = self.get_proxy_settings()
         net_params = net_params._replace(proxy=proxy)
         self.network.run_from_another_thread(self.network.set_parameters(net_params))
 
-    def _on_tab_changed(self):
-        if self.tabs.currentWidget() is self._proxy_tab:
-            self.td.trigger_rescan()
+    def update_from_config(self):
+        proxy = ProxySettings.from_config(self.config)
+        self.proxy_cb.setChecked(proxy.enabled)
+        self.proxy_mode.setCurrentText(self.PROXY_MODES.get(proxy.mode))
+        self.proxy_host.setText(proxy.host)
+        self.proxy_port.setText(proxy.port)
+        self.proxy_user.setText(proxy.user)
+        self.proxy_password.setText(proxy.password)
 
-    def suggest_proxy(self, found_proxy):
-        if found_proxy is None:
-            self.tor_cb.hide()
-            return
-        self.tor_proxy = found_proxy
-        self.tor_cb.setText(_("Use Tor proxy at port {}").format(str(found_proxy[1])))
-        if (self.proxy_cb.isChecked()
-                and self.proxy_mode.currentIndex() == self.proxy_mode.findText('SOCKS5')
-                and self.proxy_host.text() == "127.0.0.1"
-                and self.proxy_port.text() == str(found_proxy[1])):
-            self.tor_cb.setChecked(True)
-        self.tor_cb.show()
-
-    def use_tor_proxy(self, use_it):
-        if not use_it:
-            self.proxy_cb.setChecked(False)
-        else:
-            socks5_mode_index = self.proxy_mode.findText('SOCKS5')
-            if socks5_mode_index == -1:
-                _logger.info("can't find proxy_mode 'SOCKS5'")
-                return
-            self.proxy_mode.setCurrentIndex(socks5_mode_index)
-            self.proxy_host.setText("127.0.0.1")
-            self.proxy_port.setText(str(self.tor_proxy[1]))
-            self.proxy_user.setText("")
-            self.proxy_password.setText("")
-            self.tor_cb.setChecked(True)
-            self.proxy_cb.setChecked(True)
-        self.check_disable_proxy(use_it)
-        self.set_proxy()
-
-    def proxy_settings_changed(self):
-        self.tor_cb.setChecked(False)
-
-
-class TorDetector(QThread):
-    found_proxy = pyqtSignal(object)
-
-    def __init__(self):
-        QThread.__init__(self)
-        self._work_to_do_evt = threading.Event()
-        self._stopping = False
-
-    def run(self):
-        while True:
-            # do rescan
-            net_addr = detect_tor_socks_proxy()
-            self.found_proxy.emit(net_addr)
-            # wait until triggered
-            self._work_to_do_evt.wait()
-            self._work_to_do_evt.clear()
-            if self._stopping:
-                return
-
-    def trigger_rescan(self) -> None:
-        self._work_to_do_evt.set()
-
-    def stop(self):
-        self._stopping = True
-        self._work_to_do_evt.set()
-        self.exit()
-        self.wait()
+        if not self.config.cv.NETWORK_PROXY.is_modifiable():
+            for w in [
+                    self.proxy_cb, self.proxy_mode, self.proxy_host, self.proxy_port,
+                    self.proxy_user, self.proxy_password, self.detect_button
+            ]:
+                w.setEnabled(False)
 
+    def on_proxy_enable_toggle(self):
+        # probe if enabled and no pre-existing settings
+        # if self.proxy_cb.isChecked() and (not self.proxy_host.text() or not self.proxy_port.text()):
+        #     self.detect_tor()
+        self.update()
 
-class ProxyWidget(QWidget):
-    def __init__(self, parent=None):
-        super().__init__(parent)
+    def on_proxy_settings_changed(self):
+        self.update()
 
-        fixed_width_hostname = 24 * char_width_in_lineedit()
-        fixed_width_port = 6 * char_width_in_lineedit()
+    def get_proxy_settings(self) -> ProxySettings:
+        proxy = ProxySettings()
+        proxy.enabled = self.proxy_cb.isChecked()
+        proxy.mode = self.proxy_mode.currentData()
+        proxy.host = self.proxy_host.text()
+        proxy.port = self.proxy_port.text()
+        proxy.user = self.proxy_user.text()
+        proxy.password = self.proxy_password.text()
+        return proxy
 
-        grid = QGridLayout(self)
-        grid.setSpacing(8)
+    def detect_tor(self):
+        ProxySettings.probe_tor(self.torProbeFinished.emit)  # via signal
path: electrum/gui/qt/network_dialog.py
user: login: SomberNight
body: Thanks, the spinner looks good!

I was intrigued how it is so fast for you but not for me, so I debugged it now.
I pushed a commit on top of your branch:
https://github.com/SomberNight/electrum/commit/5dc2ae243e21021f981c343eafb9ff51d9f39ce4

(edit: force-pushed)
created_at: 2025-03-05T14:39:54Z
author_association: MEMBER

PR comments:
user: login: SomberNight
created_at: 2025-02-03T14:03:23Z
body: btw nice to see how much code this is trying to remove :)
author_association: MEMBER
user: login: SomberNight
created_at: 2025-03-05T15:09:30Z
body: Thanks. I've merged this manually as there was a trivial conflict with empty lines.
author_association: MEMBER
user: login: accumulator
created_at: 2025-03-05T15:13:38Z
body: Ah you had tor on port 9150 :)

async+parallel is better, thx
author_association: MEMBER