diff --git a/electrum/gui/qt/network_dialog.py b/electrum/gui/qt/network_dialog.py index 8aed26ea5..3793687d2 100644 --- a/electrum/gui/qt/network_dialog.py +++ b/electrum/gui/qt/network_dialog.py @@ -56,7 +56,7 @@ class NetworkDialog(QDialog): def __init__(self, network, config, network_updated_signal_obj): QDialog.__init__(self) self.setWindowTitle(_('Network')) - self.setMinimumSize(500, 300) + self.setMinimumSize(500, 500) self.nlayout = NetworkChoiceLayout(network, config) self.network_updated_signal_obj = network_updated_signal_obj vbox = QVBoxLayout(self) @@ -79,12 +79,18 @@ class NodesListWidget(QTreeWidget): SERVER_ADDR_ROLE = Qt.UserRole + 100 CHAIN_ID_ROLE = Qt.UserRole + 101 - IS_SERVER_ROLE = Qt.UserRole + 102 + ITEMTYPE_ROLE = Qt.UserRole + 102 + + class ItemType(IntEnum): + CHAIN = 0 + CONNECTED_SERVER = 1 + DISCONNECTED_SERVER = 2 + TOPLEVEL = 3 def __init__(self, parent): QTreeWidget.__init__(self) self.parent = parent # type: NetworkChoiceLayout - self.setHeaderLabels([_('Connected node'), _('Height')]) + self.setHeaderLabels([_('Server'), _('Height')]) self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.create_menu) @@ -92,14 +98,22 @@ class NodesListWidget(QTreeWidget): item = self.currentItem() if not item: return - is_server = bool(item.data(0, self.IS_SERVER_ROLE)) + item_type = item.data(0, self.ITEMTYPE_ROLE) menu = QMenu() - if is_server: + if item_type == self.ItemType.CONNECTED_SERVER: server = item.data(0, self.SERVER_ADDR_ROLE) # type: ServerAddr menu.addAction(_("Use as server"), lambda: self.parent.follow_server(server)) - else: + elif item_type == self.ItemType.DISCONNECTED_SERVER: + server = item.data(0, self.SERVER_ADDR_ROLE) # type: ServerAddr + def func(): + self.parent.server_e.setText(server.net_addr_str()) + self.parent.set_server() + menu.addAction(_("Use as server"), func) + elif item_type == self.ItemType.CHAIN: chain_id = item.data(0, self.CHAIN_ID_ROLE) menu.addAction(_("Follow this branch"), lambda: self.parent.follow_branch(chain_id)) + else: + return menu.exec_(self.viewport().mapToGlobal(position)) def keyPressEvent(self, event): @@ -114,9 +128,12 @@ class NodesListWidget(QTreeWidget): pt.setX(50) self.customContextMenuRequested.emit(pt) - def update(self, network: Network): + def update(self, *, network: Network, servers: dict, use_tor: bool): self.clear() - self.addChild = self.addTopLevelItem + + # connected servers + connected_servers_item = QTreeWidgetItem([_("Connected nodes"), '']) + connected_servers_item.setData(0, self.ITEMTYPE_ROLE, self.ItemType.TOPLEVEL) chains = network.get_blockchains() n_chains = len(chains) for chain_id, interfaces in chains.items(): @@ -125,87 +142,51 @@ class NodesListWidget(QTreeWidget): name = b.get_name() if n_chains > 1: x = QTreeWidgetItem([name + '@%d'%b.get_max_forkpoint(), '%d'%b.height()]) - x.setData(0, self.IS_SERVER_ROLE, 0) + x.setData(0, self.ITEMTYPE_ROLE, self.ItemType.CHAIN) x.setData(0, self.CHAIN_ID_ROLE, b.get_id()) else: - x = self + x = connected_servers_item for i in interfaces: star = ' *' if i == network.interface else '' - item = QTreeWidgetItem([i.host + star, '%d'%i.tip]) - item.setData(0, self.IS_SERVER_ROLE, 1) + item = QTreeWidgetItem([f"{i.server.net_addr_str()}" + star, '%d'%i.tip]) + item.setData(0, self.ITEMTYPE_ROLE, self.ItemType.CONNECTED_SERVER) item.setData(0, self.SERVER_ADDR_ROLE, i.server) item.setToolTip(0, str(i.server)) x.addChild(item) if n_chains > 1: - self.addTopLevelItem(x) - x.setExpanded(True) + connected_servers_item.addChild(x) - h = self.header() - h.setStretchLastSection(False) - h.setSectionResizeMode(0, QHeaderView.Stretch) - h.setSectionResizeMode(1, QHeaderView.ResizeToContents) - - super().update() - - -class ServerListWidget(QTreeWidget): - """List of all known servers.""" - - class Columns(IntEnum): - HOST = 0 - PORT = 1 - - SERVER_ADDR_ROLE = Qt.UserRole + 100 - - def __init__(self, parent): - QTreeWidget.__init__(self) - self.parent = parent # type: NetworkChoiceLayout - self.setHeaderLabels([_('Host'), _('Port')]) - self.setContextMenuPolicy(Qt.CustomContextMenu) - self.customContextMenuRequested.connect(self.create_menu) - - def create_menu(self, position): - item = self.currentItem() - if not item: - return - menu = QMenu() - server = item.data(self.Columns.HOST, self.SERVER_ADDR_ROLE) - menu.addAction(_("Use as server"), lambda: self.set_server(server)) - menu.exec_(self.viewport().mapToGlobal(position)) - - def set_server(self, server: ServerAddr): - self.parent.server_e.setText(server.net_addr_str()) - self.parent.set_server() - - def keyPressEvent(self, event): - if event.key() in [ Qt.Key_F2, Qt.Key_Return ]: - self.on_activated(self.currentItem(), self.currentColumn()) - else: - QTreeWidget.keyPressEvent(self, event) - - def on_activated(self, item, column): - # on 'enter' we show the menu - pt = self.visualItemRect(item).bottomLeft() - pt.setX(50) - self.customContextMenuRequested.emit(pt) - - def update(self, servers, use_tor): - self.clear() + # disconnected servers + disconnected_servers_item = QTreeWidgetItem([_("Other known servers"), ""]) + disconnected_servers_item.setData(0, self.ITEMTYPE_ROLE, self.ItemType.TOPLEVEL) + connected_hosts = set([iface.host for ifaces in chains.values() for iface in ifaces]) protocol = PREFERRED_NETWORK_PROTOCOL for _host, d in sorted(servers.items()): + if _host in connected_hosts: + continue if _host.endswith('.onion') and not use_tor: continue port = d.get(protocol) if port: - x = QTreeWidgetItem([_host, port]) server = ServerAddr(_host, port, protocol=protocol) - x.setData(self.Columns.HOST, self.SERVER_ADDR_ROLE, server) - self.addTopLevelItem(x) + item = QTreeWidgetItem([server.net_addr_str(), ""]) + item.setData(0, self.ITEMTYPE_ROLE, self.ItemType.DISCONNECTED_SERVER) + item.setData(0, self.SERVER_ADDR_ROLE, server) + disconnected_servers_item.addChild(item) + + self.addTopLevelItem(connected_servers_item) + self.addTopLevelItem(disconnected_servers_item) + + connected_servers_item.setExpanded(True) + for i in range(connected_servers_item.childCount()): + connected_servers_item.child(i).setExpanded(True) + disconnected_servers_item.setExpanded(True) + # headers h = self.header() h.setStretchLastSection(False) - h.setSectionResizeMode(self.Columns.HOST, QHeaderView.Stretch) - h.setSectionResizeMode(self.Columns.PORT, QHeaderView.ResizeToContents) + h.setSectionResizeMode(0, QHeaderView.Stretch) + h.setSectionResizeMode(1, QHeaderView.ResizeToContents) super().update() @@ -218,44 +199,14 @@ class NetworkChoiceLayout(object): self.tor_proxy = None self.tabs = tabs = QTabWidget() - server_tab = QWidget() proxy_tab = QWidget() blockchain_tab = QWidget() tabs.addTab(blockchain_tab, _('Overview')) - tabs.addTab(server_tab, _('Server')) tabs.addTab(proxy_tab, _('Proxy')) fixed_width_hostname = 24 * char_width_in_lineedit() fixed_width_port = 6 * char_width_in_lineedit() - # server tab - grid = QGridLayout(server_tab) - grid.setSpacing(8) - - self.server_e = QLineEdit() - self.server_e.setFixedWidth(fixed_width_hostname + fixed_width_port) - self.autoconnect_cb = QCheckBox(_('Select server automatically')) - self.autoconnect_cb.setEnabled(self.config.is_modifiable('auto_connect')) - - self.server_e.editingFinished.connect(self.set_server) - 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, 0, 0, 1, 3) - grid.addWidget(HelpButton(msg), 0, 4) - - grid.addWidget(QLabel(_('Server') + ':'), 1, 0) - grid.addWidget(self.server_e, 1, 1, 1, 3) - - label = _('Server peers') if network.is_connected() else _('Default Servers') - grid.addWidget(QLabel(label), 2, 0, 1, 5) - self.servers_list = ServerListWidget(self) - grid.addWidget(self.servers_list, 3, 0, 1, 5) - # Proxy tab grid = QGridLayout(proxy_tab) grid.setSpacing(8) @@ -315,23 +266,36 @@ class NetworkChoiceLayout(object): grid.addWidget(self.status_label, 0, 1, 1, 3) grid.addWidget(HelpButton(msg), 0, 4) - self.server_label = QLabel('') - msg = _("Electrum sends your wallet addresses to a single server, in order to receive your transaction history.") - grid.addWidget(QLabel(_('Server') + ':'), 1, 0) - grid.addWidget(self.server_label, 1, 1, 1, 3) + self.autoconnect_cb = QCheckBox(_('Select server automatically')) + self.autoconnect_cb.setEnabled(self.config.is_modifiable('auto_connect')) + 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') + ':'), 2, 0) - grid.addWidget(self.height_label, 2, 1) - grid.addWidget(HelpButton(msg), 2, 4) + 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, 3, 0, 1, 3) + grid.addWidget(self.split_label, 4, 0, 1, 3) self.nodes_list_widget = NodesListWidget(self) - grid.addWidget(self.nodes_list_widget, 5, 0, 1, 5) + grid.addWidget(self.nodes_list_widget, 6, 0, 1, 5) vbox = QVBoxLayout() vbox.addWidget(tabs) @@ -354,27 +318,18 @@ class NetworkChoiceLayout(object): if self.config.is_modifiable('server'): enabled = not self.autoconnect_cb.isChecked() self.server_e.setEnabled(enabled) - self.servers_list.setEnabled(enabled) else: - for w in [self.autoconnect_cb, self.server_e, self.servers_list]: + 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 - proxy_config, auto_connect = net_params.proxy, net_params.auto_connect + auto_connect = net_params.auto_connect if not self.server_e.hasFocus(): self.server_e.setText(server.net_addr_str()) self.autoconnect_cb.setChecked(auto_connect) - interface = self.network.interface - host = interface.host if interface else _('None') - self.server_label.setText(host) - - self.servers = self.network.get_servers() - self.servers_list.update(self.servers, self.tor_cb.isChecked()) - self.enable_set_server() - height_str = "%d "%(self.network.get_local_height()) + _('blocks') self.height_label.setText(height_str) n = len(self.network.get_interfaces()) @@ -391,7 +346,10 @@ class NetworkChoiceLayout(object): else: msg = '' self.split_label.setText(msg) - self.nodes_list_widget.update(self.network) + self.nodes_list_widget.update(network=self.network, + servers=self.network.get_servers(), + use_tor=self.tor_cb.isChecked()) + self.enable_set_server() def fill_in_proxy_settings(self): proxy_config = self.network.get_parameters().proxy