qt,qml: review rework, refactor spinner, add tor probe active indicator
This commit is contained in:
@@ -19,6 +19,8 @@ Item {
|
||||
{ text: qsTr('SOCKS4'), value: 'socks4' }
|
||||
]
|
||||
|
||||
property bool _probing: false
|
||||
|
||||
function toProxyDict() {
|
||||
var p = {}
|
||||
p['enabled'] = pc.proxy_enabled
|
||||
@@ -104,19 +106,33 @@ Item {
|
||||
Layout.topMargin: constants.paddingLarge
|
||||
padding: 0
|
||||
background: Rectangle {
|
||||
color: Material.dialogColor
|
||||
color: constants.darkerDialogBackground
|
||||
}
|
||||
FlatButton {
|
||||
enabled: proxy_enabled_cb.checked
|
||||
text: qsTr('Detect TOR proxy')
|
||||
onClicked: Network.probeTor()
|
||||
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
|
||||
|
||||
@@ -25,21 +25,23 @@
|
||||
|
||||
from enum import IntEnum
|
||||
|
||||
from PyQt6.QtCore import Qt, pyqtSignal
|
||||
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
|
||||
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, ProxySettings
|
||||
from electrum.network import Network, ProxySettings, is_valid_host, is_valid_port
|
||||
from electrum.logging import get_logger
|
||||
|
||||
from .util import (Buttons, CloseButton, HelpButton, read_QIcon, char_width_in_lineedit,
|
||||
PasswordLineEdit, 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__)
|
||||
@@ -260,16 +262,18 @@ class ProxyWidget(QWidget):
|
||||
grid.addWidget(self.proxy_user, 2, 1, 1, 2)
|
||||
grid.addWidget(self.proxy_password, 2, 3, 1, 2)
|
||||
|
||||
spacer = QVBoxLayout()
|
||||
spacer.addStretch(1)
|
||||
grid.addLayout(spacer, 3, 0, 1, 5)
|
||||
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)
|
||||
|
||||
self.detect_button = QPushButton(_('Detect TOR proxy'))
|
||||
grid.addWidget(self.detect_button, 4, 0, 1, 5, alignment=Qt.AlignmentFlag.AlignHCenter)
|
||||
grid.addLayout(detect_l, 3, 0, 1, 5, alignment=Qt.AlignmentFlag.AlignLeft)
|
||||
|
||||
spacer = QVBoxLayout()
|
||||
spacer.addStretch(1)
|
||||
grid.addLayout(spacer, 5, 0, 1, 5)
|
||||
grid.addLayout(spacer, 4, 0, 1, 5)
|
||||
|
||||
self.update_from_config()
|
||||
self.update()
|
||||
@@ -293,7 +297,10 @@ class ProxyWidget(QWidget):
|
||||
]:
|
||||
item.setEnabled(enabled)
|
||||
|
||||
if not self.proxy_port.hasAcceptableInput():
|
||||
if not self.proxy_port.hasAcceptableInput() and not is_valid_port(self.proxy_port.text()):
|
||||
return
|
||||
|
||||
if not is_valid_host(self.proxy_host.text()):
|
||||
return
|
||||
|
||||
net_params = self.network.get_parameters()
|
||||
@@ -337,10 +344,15 @@ class ProxyWidget(QWidget):
|
||||
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):
|
||||
if host is not None:
|
||||
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))
|
||||
|
||||
@@ -25,7 +25,7 @@ from electrum.submarine_swaps import SwapServerError
|
||||
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 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
|
||||
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 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
|
||||
|
||||
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 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
|
||||
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 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
|
||||
# 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 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
|
||||
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)
|
||||
|
||||
@@ -11,7 +11,7 @@ from typing import (NamedTuple, Callable, Optional, TYPE_CHECKING, List, Any, Se
|
||||
|
||||
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 @@ class AmountLabel(QLabel):
|
||||
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!"
|
||||
|
||||
@@ -159,6 +159,21 @@ def pick_random_server(hostmap=None, *, allowed_protocols: Iterable[str],
|
||||
return random.choice(eligible) if eligible else None
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
class ProxySettings:
|
||||
MODES = ['socks4', 'socks5']
|
||||
|
||||
@@ -178,31 +193,21 @@ class ProxySettings:
|
||||
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':
|
||||
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:]
|
||||
|
||||
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
|
||||
|
||||
# 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]
|
||||
@@ -234,7 +239,7 @@ class ProxySettings:
|
||||
proxy.deserialize_proxy_cfgstr(
|
||||
config.NETWORK_PROXY, config.NETWORK_PROXY_USER, config.NETWORK_PROXY_PASSWORD
|
||||
)
|
||||
proxy.enabled = config.NETWORK_ENABLE_PROXY
|
||||
proxy.enabled = config.NETWORK_PROXY_ENABLED
|
||||
return proxy
|
||||
|
||||
@classmethod
|
||||
@@ -253,7 +258,7 @@ class ProxySettings:
|
||||
def detect_task(finished: Callable[[str | None, int | None], None]):
|
||||
net_addr = detect_tor_socks_proxy()
|
||||
if net_addr is None:
|
||||
finished(None, None)
|
||||
finished('', -1)
|
||||
else:
|
||||
host = net_addr[0]
|
||||
port = net_addr[1]
|
||||
@@ -750,14 +755,14 @@ class Network(Logger, NetworkRetryManager[ServerAddr]):
|
||||
# return
|
||||
self.config.NETWORK_AUTO_CONNECT = net_params.auto_connect
|
||||
self.config.NETWORK_ONESERVER = net_params.oneserver
|
||||
self.config.NETWORK_ENABLE_PROXY = proxy_enabled
|
||||
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_ENABLE_PROXY != proxy_enabled \
|
||||
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 \
|
||||
|
||||
@@ -950,7 +950,7 @@ class SimpleConfig(Logger):
|
||||
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)
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user