4 changed files with 346 additions and 0 deletions
@ -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 |
||||||
@ -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)) |
||||||
@ -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…
Reference in new issue