qt: add initial wizard classes for desktop client
This commit is contained in:
0
electrum/gui/qt/wizard/__init__.py
Normal file
0
electrum/gui/qt/wizard/__init__.py
Normal file
82
electrum/gui/qt/wizard/server_connect.py
Normal file
82
electrum/gui/qt/wizard/server_connect.py
Normal file
@@ -0,0 +1,82 @@
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
|
||||
from PyQt5.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||
|
||||
from electrum.i18n import _
|
||||
from .wizard import QEAbstractWizard, WizardComponent
|
||||
from electrum.logging import get_logger
|
||||
from electrum import mnemonic
|
||||
from electrum.wizard import ServerConnectWizard
|
||||
from ..util import ChoicesLayout
|
||||
|
||||
|
||||
class QEServerConnectWizard(ServerConnectWizard, QEAbstractWizard):
|
||||
|
||||
def __init__(self, config: 'SimpleConfig', app: QApplication, daemon, parent=None):
|
||||
ServerConnectWizard.__init__(self, daemon)
|
||||
QEAbstractWizard.__init__(self, config, app, parent)
|
||||
self._daemon = daemon
|
||||
|
||||
# attach view names
|
||||
self.navmap_merge({
|
||||
'autoconnect': { 'gui': WCAutoConnect },
|
||||
'proxy_ask': { 'gui': WCProxyAsk },
|
||||
'proxy_config': { 'gui': WCProxyConfig },
|
||||
'server_config': { 'gui': WCServerConfig },
|
||||
})
|
||||
|
||||
|
||||
class WCAutoConnect(WizardComponent):
|
||||
def __init__(self, parent=None):
|
||||
WizardComponent.__init__(self, parent, title=_("How do you want to connect to a server? "))
|
||||
message = _("Electrum communicates with remote servers to get "
|
||||
"information about your transactions and addresses. The "
|
||||
"servers all fulfill the same purpose only differing in "
|
||||
"hardware. In most cases you simply want to let Electrum "
|
||||
"pick one at random. However if you prefer feel free to "
|
||||
"select a server manually.")
|
||||
choices = [_("Auto connect"), _("Select server manually")]
|
||||
self.clayout = ChoicesLayout(message, choices)
|
||||
self.clayout.group.buttonClicked.connect(self.on_updated)
|
||||
self.layout().addLayout(self.clayout.layout())
|
||||
self._valid = True
|
||||
|
||||
def apply(self):
|
||||
r = self.clayout.selected_index()
|
||||
self.wizard_data['autoconnect'] = (r == 0)
|
||||
# if r == 1:
|
||||
# nlayout = NetworkChoiceLayout(network, self.config, wizard=True)
|
||||
# if self.exec_layout(nlayout.layout()):
|
||||
# nlayout.accept()
|
||||
# self.config.NETWORK_AUTO_CONNECT = network.auto_connect
|
||||
# else:
|
||||
# network.auto_connect = True
|
||||
# self.config.NETWORK_AUTO_CONNECT = True
|
||||
|
||||
|
||||
class WCProxyAsk(WizardComponent):
|
||||
def __init__(self, parent=None):
|
||||
WizardComponent.__init__(self, parent, title=_("Proxy"))
|
||||
message = _("Do you use a local proxy service such as TOR to reach the internet?")
|
||||
choices = [_("Yes"), _("No")]
|
||||
self.clayout = ChoicesLayout(message, choices)
|
||||
self.layout().addLayout(self.clayout.layout())
|
||||
|
||||
def apply(self):
|
||||
r = self.clayout.selected_index()
|
||||
self.wizard_data['want_proxy'] = (r == 0)
|
||||
|
||||
|
||||
class WCProxyConfig(WizardComponent):
|
||||
def __init__(self, parent=None):
|
||||
WizardComponent.__init__(self, parent, title=_("Proxy"))
|
||||
|
||||
def apply(self):
|
||||
pass
|
||||
|
||||
|
||||
class WCServerConfig(WizardComponent):
|
||||
def __init__(self, parent=None):
|
||||
WizardComponent.__init__(self, parent, title=_("Server"))
|
||||
|
||||
def apply(self):
|
||||
pass
|
||||
90
electrum/gui/qt/wizard/wallet.py
Normal file
90
electrum/gui/qt/wizard/wallet.py
Normal file
@@ -0,0 +1,90 @@
|
||||
import os
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
|
||||
from PyQt5.QtQml import QQmlApplicationEngine
|
||||
|
||||
from electrum.logging import get_logger
|
||||
from electrum import mnemonic
|
||||
from electrum.wizard import NewWalletWizard, ServerConnectWizard
|
||||
|
||||
|
||||
class QENewWalletWizard(NewWalletWizard, QEAbstractWizard):
|
||||
|
||||
createError = pyqtSignal([str], arguments=["error"])
|
||||
createSuccess = pyqtSignal()
|
||||
|
||||
def __init__(self, daemon, parent = None):
|
||||
NewWalletWizard.__init__(self, daemon)
|
||||
QEAbstractWizard.__init__(self, parent)
|
||||
self._daemon = daemon
|
||||
|
||||
# attach view names and accept handlers
|
||||
self.navmap_merge({
|
||||
'wallet_name': { 'gui': 'WCWalletName' },
|
||||
'wallet_type': { 'gui': 'WCWalletType' },
|
||||
'keystore_type': { 'gui': 'WCKeystoreType' },
|
||||
'create_seed': { 'gui': 'WCCreateSeed' },
|
||||
'confirm_seed': { 'gui': 'WCConfirmSeed' },
|
||||
'have_seed': { 'gui': 'WCHaveSeed' },
|
||||
'bip39_refine': { 'gui': 'WCBIP39Refine' },
|
||||
'have_master_key': { 'gui': 'WCHaveMasterKey' },
|
||||
'multisig': { 'gui': 'WCMultisig' },
|
||||
'multisig_cosigner_keystore': { 'gui': 'WCCosignerKeystore' },
|
||||
'multisig_cosigner_key': { 'gui': 'WCHaveMasterKey' },
|
||||
'multisig_cosigner_seed': { 'gui': 'WCHaveSeed' },
|
||||
'multisig_cosigner_bip39_refine': { 'gui': 'WCBIP39Refine' },
|
||||
'imported': { 'gui': 'WCImport' },
|
||||
'wallet_password': { 'gui': 'WCWalletPassword' }
|
||||
})
|
||||
|
||||
pathChanged = pyqtSignal()
|
||||
@pyqtProperty(str, notify=pathChanged)
|
||||
def path(self):
|
||||
return self._path
|
||||
|
||||
@path.setter
|
||||
def path(self, path):
|
||||
self._path = path
|
||||
self.pathChanged.emit()
|
||||
|
||||
def is_single_password(self):
|
||||
return self._daemon.singlePasswordEnabled
|
||||
|
||||
@pyqtSlot('QJSValue', result=bool)
|
||||
def hasDuplicateMasterKeys(self, js_data):
|
||||
self._logger.info('Checking for duplicate masterkeys')
|
||||
data = js_data.toVariant()
|
||||
return self.has_duplicate_masterkeys(data)
|
||||
|
||||
@pyqtSlot('QJSValue', result=bool)
|
||||
def hasHeterogeneousMasterKeys(self, js_data):
|
||||
self._logger.info('Checking for heterogeneous masterkeys')
|
||||
data = js_data.toVariant()
|
||||
return self.has_heterogeneous_masterkeys(data)
|
||||
|
||||
@pyqtSlot(str, str, result=bool)
|
||||
def isMatchingSeed(self, seed, seed_again):
|
||||
return mnemonic.is_matching_seed(seed=seed, seed_again=seed_again)
|
||||
|
||||
@pyqtSlot('QJSValue', bool, str)
|
||||
def createStorage(self, js_data, single_password_enabled, single_password):
|
||||
self._logger.info('Creating wallet from wizard data')
|
||||
data = js_data.toVariant()
|
||||
|
||||
if single_password_enabled and single_password:
|
||||
data['encrypt'] = True
|
||||
data['password'] = single_password
|
||||
|
||||
path = os.path.join(os.path.dirname(self._daemon.daemon.config.get_wallet_path()), data['wallet_name'])
|
||||
|
||||
try:
|
||||
self.create_storage(path, data)
|
||||
|
||||
# minimally populate self after create
|
||||
self._password = data['password']
|
||||
self.path = path
|
||||
|
||||
self.createSuccess.emit()
|
||||
except Exception as e:
|
||||
self._logger.error(f"createStorage errored: {e!r}")
|
||||
self.createError.emit(str(e))
|
||||
174
electrum/gui/qt/wizard/wizard.py
Normal file
174
electrum/gui/qt/wizard/wizard.py
Normal file
@@ -0,0 +1,174 @@
|
||||
from abc import abstractmethod
|
||||
|
||||
from PyQt5.QtCore import Qt, QVariant, QTimer, pyqtSignal, pyqtSlot
|
||||
from PyQt5.QtGui import QPixmap
|
||||
from PyQt5.QtWidgets import (QDialog, QApplication, QPushButton, QWidget, QLabel, QVBoxLayout, QScrollArea,
|
||||
QHBoxLayout, QLayout, QStackedWidget)
|
||||
|
||||
from electrum.i18n import _
|
||||
from ..util import Buttons, icon_path
|
||||
from electrum.logging import get_logger
|
||||
|
||||
|
||||
class QEAbstractWizard(QDialog):
|
||||
_logger = get_logger(__name__)
|
||||
|
||||
# def __init__(self, config: 'SimpleConfig', app: QApplication, plugins: 'Plugins', *, gui_object: 'ElectrumGui'):
|
||||
def __init__(self, config: 'SimpleConfig', app: QApplication, daemon):
|
||||
QDialog.__init__(self, None)
|
||||
self.app = app
|
||||
self.config = config
|
||||
# self.gui_thread = gui_object.gui_thread
|
||||
self.setMinimumSize(600, 400)
|
||||
self.title = QLabel()
|
||||
self.main_widget = QStackedWidget(self)
|
||||
self.back_button = QPushButton(_("Back"), self)
|
||||
self.back_button.clicked.connect(self.on_back_button_clicked)
|
||||
self.next_button = QPushButton(_("Next"), self)
|
||||
self.next_button.clicked.connect(self.on_next_button_clicked)
|
||||
self.next_button.setDefault(True)
|
||||
self.logo = QLabel()
|
||||
self.please_wait = QLabel(_("Please wait..."))
|
||||
self.please_wait.setAlignment(Qt.AlignCenter)
|
||||
self.please_wait.setVisible(False)
|
||||
self.icon_filename = None
|
||||
|
||||
outer_vbox = QVBoxLayout(self)
|
||||
inner_vbox = QVBoxLayout()
|
||||
inner_vbox.addWidget(self.title)
|
||||
inner_vbox.addWidget(self.main_widget)
|
||||
inner_vbox.addStretch(1)
|
||||
inner_vbox.addWidget(self.please_wait)
|
||||
inner_vbox.addStretch(1)
|
||||
scroll_widget = QWidget()
|
||||
scroll_widget.setLayout(inner_vbox)
|
||||
scroll = QScrollArea()
|
||||
scroll.setFocusPolicy(Qt.NoFocus)
|
||||
scroll.setWidget(scroll_widget)
|
||||
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
scroll.setWidgetResizable(True)
|
||||
icon_vbox = QVBoxLayout()
|
||||
icon_vbox.addWidget(self.logo)
|
||||
icon_vbox.addStretch(1)
|
||||
hbox = QHBoxLayout()
|
||||
hbox.addLayout(icon_vbox)
|
||||
hbox.addSpacing(5)
|
||||
hbox.addWidget(scroll)
|
||||
hbox.setStretchFactor(scroll, 1)
|
||||
outer_vbox.addLayout(hbox)
|
||||
outer_vbox.addLayout(Buttons(self.back_button, self.next_button))
|
||||
self.set_icon('electrum.png')
|
||||
self.show()
|
||||
self.raise_()
|
||||
|
||||
QTimer.singleShot(40, self.strt)
|
||||
|
||||
# TODO: re-test if needed on macOS
|
||||
# self.refresh_gui() # Need for QT on MacOSX. Lame.
|
||||
|
||||
# def refresh_gui(self):
|
||||
# # For some reason, to refresh the GUI this needs to be called twice
|
||||
# self.app.processEvents()
|
||||
# self.app.processEvents()
|
||||
|
||||
def strt(self):
|
||||
view = self.start_wizard()
|
||||
self.load_next_component(view)
|
||||
|
||||
def load_next_component(self, view, wdata={}):
|
||||
comp = self.view_to_component(view)
|
||||
page = comp(self.main_widget)
|
||||
page.wizard_data = wdata
|
||||
page.updated.connect(self.on_page_updated)
|
||||
self._logger.debug(f'{page!r}')
|
||||
self.main_widget.setCurrentIndex(self.main_widget.addWidget(page))
|
||||
page.apply()
|
||||
self.update(page.wizard_data)
|
||||
|
||||
@pyqtSlot(object)
|
||||
def on_page_updated(self, page):
|
||||
page.apply()
|
||||
self.update(page.wizard_data)
|
||||
|
||||
def set_icon(self, filename):
|
||||
prior_filename, self.icon_filename = self.icon_filename, filename
|
||||
self.logo.setPixmap(QPixmap(icon_path(filename))
|
||||
.scaledToWidth(60, mode=Qt.SmoothTransformation))
|
||||
return prior_filename
|
||||
|
||||
def can_go_back(self):
|
||||
return len(self._stack) > 0
|
||||
|
||||
def update(self, wdata: dict):
|
||||
self.back_button.setText(_('Back') if self.can_go_back() else _('Cancel'))
|
||||
self.next_button.setText(_('Next') if not self.is_last(wdata) else _('Finish'))
|
||||
|
||||
def on_back_button_clicked(self):
|
||||
if self.can_go_back():
|
||||
wdata = self.prev()
|
||||
self.main_widget.removeWidget(self.main_widget.currentWidget())
|
||||
self.update(wdata)
|
||||
else:
|
||||
self.close()
|
||||
|
||||
def on_next_button_clicked(self):
|
||||
wc = self.main_widget.currentWidget()
|
||||
wc.apply()
|
||||
wd = wc.wizard_data.copy()
|
||||
if self.is_last(wd):
|
||||
self.finished(wd)
|
||||
self.close()
|
||||
else:
|
||||
next = self.submit(wd)
|
||||
self.load_next_component(next['view'], wd)
|
||||
|
||||
def start_wizard(self) -> str:
|
||||
self.start()
|
||||
return self._current.view
|
||||
|
||||
def view_to_component(self, view) -> QWidget:
|
||||
return self.navmap[view]['gui']
|
||||
|
||||
def submit(self, wizard_data) -> dict:
|
||||
wdata = wizard_data.copy()
|
||||
self.log_state(wdata)
|
||||
view = self.resolve_next(self._current.view, wdata)
|
||||
return {
|
||||
'view': view.view,
|
||||
'wizard_data': view.wizard_data
|
||||
}
|
||||
|
||||
def prev(self) -> dict:
|
||||
viewstate = self.resolve_prev()
|
||||
return viewstate.wizard_data
|
||||
|
||||
def is_last(self, wizard_data: dict) -> bool:
|
||||
wdata = wizard_data.copy()
|
||||
return self.is_last_view(self._current.view, wdata)
|
||||
|
||||
|
||||
### support classes
|
||||
|
||||
|
||||
class WizardComponent(QWidget):
|
||||
updated = pyqtSignal(object)
|
||||
|
||||
def __init__(self, parent: QWidget = None, *, title: str = None, layout: QLayout = None):
|
||||
super().__init__(parent)
|
||||
self.setLayout(layout if layout else QVBoxLayout(self))
|
||||
self.wizard_data = {}
|
||||
self.title = title if title is not None else 'No title'
|
||||
self._valid = False
|
||||
|
||||
@property
|
||||
def valid(self):
|
||||
return self._valid
|
||||
|
||||
@abstractmethod
|
||||
def apply(self):
|
||||
pass
|
||||
|
||||
@pyqtSlot()
|
||||
def on_updated(self, *args):
|
||||
self.updated.emit(self)
|
||||
|
||||
Reference in New Issue
Block a user