Browse Source

qt: add initial wizard classes for desktop client

master
Sander van Grieken 2 years ago
parent
commit
8f0cb38af2
  1. 0
      electrum/gui/qt/wizard/__init__.py
  2. 82
      electrum/gui/qt/wizard/server_connect.py
  3. 90
      electrum/gui/qt/wizard/wallet.py
  4. 174
      electrum/gui/qt/wizard/wizard.py

0
electrum/gui/qt/wizard/__init__.py

82
electrum/gui/qt/wizard/server_connect.py

@ -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

@ -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

@ -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)
Loading…
Cancel
Save