You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
303 lines
10 KiB
303 lines
10 KiB
import copy |
|
import threading |
|
from abc import abstractmethod |
|
from typing import TYPE_CHECKING |
|
|
|
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, pyqtSlot, QSize, QMetaObject |
|
from PyQt5.QtGui import QPixmap |
|
from PyQt5.QtWidgets import (QDialog, QPushButton, QWidget, QLabel, QVBoxLayout, QScrollArea, |
|
QHBoxLayout, QLayout) |
|
|
|
from electrum.i18n import _ |
|
from electrum.logging import get_logger |
|
from electrum.gui.qt.util import Buttons, icon_path, MessageBoxMixin, WWLabel, ResizableStackedWidget, AbstractQWidget |
|
|
|
if TYPE_CHECKING: |
|
from electrum.simple_config import SimpleConfig |
|
from electrum.gui.qt import QElectrumApplication |
|
from electrum.wizard import WizardViewState |
|
|
|
|
|
class QEAbstractWizard(QDialog, MessageBoxMixin): |
|
""" Concrete subclasses of QEAbstractWizard must also inherit from a concrete AbstractWizard subclass. |
|
QEAbstractWizard forms the base for all QtWidgets GUI based wizards, while AbstractWizard defines |
|
the base for non-gui wizard flow navigation functionality. |
|
""" |
|
_logger = get_logger(__name__) |
|
|
|
requestNext = pyqtSignal() |
|
requestPrev = pyqtSignal() |
|
|
|
def __init__(self, config: 'SimpleConfig', app: 'QElectrumApplication', *, start_viewstate: 'WizardViewState' = None): |
|
QDialog.__init__(self, None) |
|
self.app = app |
|
self.config = config |
|
|
|
# compat |
|
self.gui_thread = threading.current_thread() |
|
|
|
self.setMinimumSize(600, 400) |
|
|
|
self.title = QLabel() |
|
self.window_title = '' |
|
self.finish_label = _('Finish') |
|
|
|
self.main_widget = ResizableStackedWidget(self) |
|
|
|
self.back_button = QPushButton(_("Back"), self) |
|
self.back_button.clicked.connect(self.on_back_button_clicked) |
|
self.back_button.setEnabled(False) |
|
self.next_button = QPushButton(_("Next"), self) |
|
self.next_button.clicked.connect(self.on_next_button_clicked) |
|
self.next_button.setEnabled(False) |
|
self.next_button.setDefault(True) |
|
self.requestPrev.connect(self.on_back_button_clicked) |
|
self.requestNext.connect(self.on_next_button_clicked) |
|
self.logo = QLabel() |
|
|
|
please_wait_layout = QVBoxLayout() |
|
please_wait_layout.addStretch(1) |
|
self.please_wait_l = QLabel(_("Please wait...")) |
|
self.please_wait_l.setAlignment(Qt.AlignCenter) |
|
please_wait_layout.addWidget(self.please_wait_l) |
|
please_wait_layout.addStretch(1) |
|
self.please_wait = QWidget() |
|
self.please_wait.setVisible(False) |
|
self.please_wait.setLayout(please_wait_layout) |
|
|
|
error_layout = QVBoxLayout() |
|
error_layout.addStretch(1) |
|
error_icon = QLabel() |
|
error_icon.setPixmap(QPixmap(icon_path('warning.png')).scaledToWidth(48, mode=Qt.SmoothTransformation)) |
|
error_icon.setAlignment(Qt.AlignCenter) |
|
error_layout.addWidget(error_icon) |
|
self.error_msg = WWLabel() |
|
self.error_msg.setAlignment(Qt.AlignCenter) |
|
error_layout.addWidget(self.error_msg) |
|
error_layout.addStretch(1) |
|
self.error = QWidget() |
|
self.error.setVisible(False) |
|
self.error.setLayout(error_layout) |
|
|
|
outer_vbox = QVBoxLayout(self) |
|
inner_vbox = QVBoxLayout() |
|
inner_vbox.addWidget(self.title) |
|
inner_vbox.addWidget(self.main_widget) |
|
inner_vbox.addWidget(self.please_wait) |
|
inner_vbox.addWidget(self.error) |
|
|
|
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.icon_filename = None |
|
self.set_icon('electrum.png') |
|
|
|
self.start_viewstate = start_viewstate |
|
|
|
self.show() |
|
self.raise_() |
|
|
|
QMetaObject.invokeMethod(self, 'strt', Qt.QueuedConnection) # call strt after subclass constructor(s) |
|
|
|
def sizeHint(self) -> QSize: |
|
return QSize(600, 400) |
|
|
|
@pyqtSlot() |
|
def strt(self): |
|
if self.start_viewstate is not None: |
|
viewstate = self._current = self.start_viewstate |
|
else: |
|
viewstate = self.start_wizard() |
|
self.load_next_component(viewstate.view, viewstate.wizard_data, viewstate.params) |
|
# 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 load_next_component(self, view, wdata=None, params=None): |
|
if wdata is None: |
|
wdata = {} |
|
if params is None: |
|
params = {} |
|
|
|
comp = self.view_to_component(view) |
|
try: |
|
page = comp(self.main_widget, self) |
|
except Exception as e: |
|
self._logger.error(f'not a class: {comp!r}') |
|
raise e |
|
page.wizard_data = copy.deepcopy(wdata) |
|
page.params = params |
|
page.on_ready() # call before component emits any signals |
|
|
|
self._logger.debug(f'load_next_component: {page=!r}') |
|
|
|
page.updated.connect(self.on_page_updated) |
|
|
|
# add to stack and update wizard |
|
self.main_widget.setCurrentIndex(self.main_widget.addWidget(page)) |
|
page.apply() |
|
self.update() |
|
|
|
@pyqtSlot(object) |
|
def on_page_updated(self, page): |
|
page.apply() |
|
if page == self.main_widget.currentWidget(): |
|
self.update() |
|
|
|
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) -> bool: |
|
return len(self._stack) > 0 |
|
|
|
def update(self): |
|
page = self.main_widget.currentWidget() |
|
self.setWindowTitle(page.wizard_title if page.wizard_title else self.window_title) |
|
self.title.setText(f'<b>{page.title}</b>' if page.title else '') |
|
self.back_button.setText(_('Back') if self.can_go_back() else _('Cancel')) |
|
self.back_button.setEnabled(not page.busy) |
|
self.next_button.setText(_('Next') if not self.is_last(page.wizard_data) else self.finish_label) |
|
self.next_button.setEnabled(not page.busy and page.valid) |
|
self.main_widget.setVisible(not page.busy and not bool(page.error)) |
|
self.please_wait.setVisible(page.busy) |
|
self.please_wait_l.setText(page.busy_msg if page.busy_msg else _("Please wait...")) |
|
self.error_msg.setText(str(page.error)) |
|
self.error.setVisible(not page.busy and bool(page.error)) |
|
icon = page.params.get('icon', icon_path('electrum.png')) |
|
if icon and icon != self.icon_filename: |
|
self.set_icon(icon) |
|
self.logo.setVisible(True) |
|
else: |
|
self.logo.setVisible(False) |
|
|
|
def on_back_button_clicked(self): |
|
if self.can_go_back(): |
|
self.prev() |
|
widget = self.main_widget.currentWidget() |
|
self.main_widget.removeWidget(widget) |
|
widget.deleteLater() |
|
self.update() |
|
else: |
|
self.close() |
|
|
|
def on_next_button_clicked(self): |
|
page = self.main_widget.currentWidget() |
|
page.apply() |
|
wd = page.wizard_data.copy() |
|
if self.is_last(wd): |
|
self.submit(wd) |
|
if self.is_finalized(wd): |
|
self.accept() |
|
else: |
|
self.prev() # rollback the submit above |
|
else: |
|
next = self.submit(wd) |
|
self.load_next_component(next.view, next.wizard_data, next.params) |
|
|
|
def start_wizard(self) -> 'WizardViewState': |
|
self.start() |
|
return self._current |
|
|
|
def view_to_component(self, view) -> QWidget: |
|
return self.navmap[view]['gui'] |
|
|
|
def submit(self, wizard_data) -> dict: |
|
wdata = wizard_data.copy() |
|
view = self.resolve_next(self._current.view, wdata) |
|
return view |
|
|
|
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) |
|
|
|
def is_finalized(self, wizard_data: dict) -> bool: |
|
''' Final check before closing the wizard. ''' |
|
return True |
|
|
|
|
|
class WizardComponent(AbstractQWidget): |
|
updated = pyqtSignal(object) |
|
|
|
def __init__(self, parent: QWidget, wizard: QEAbstractWizard, *, 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.wizard_title = None |
|
self.busy_msg = '' |
|
self.wizard = wizard |
|
self._error = '' |
|
self._valid = False |
|
self._busy = False |
|
|
|
@property |
|
def valid(self): |
|
return self._valid |
|
|
|
@valid.setter |
|
def valid(self, is_valid): |
|
if self._valid != is_valid: |
|
self._valid = is_valid |
|
self.on_updated() |
|
|
|
@property |
|
def busy(self): |
|
return self._busy |
|
|
|
@busy.setter |
|
def busy(self, is_busy): |
|
if self._busy != is_busy: |
|
self._busy = is_busy |
|
self.on_updated() |
|
|
|
@property |
|
def error(self): |
|
return self._error |
|
|
|
@error.setter |
|
def error(self, error): |
|
if self._error != error: |
|
self._error = error |
|
self.on_updated() |
|
|
|
@abstractmethod |
|
def apply(self): |
|
# called to apply UI component values to wizard_data |
|
pass |
|
|
|
def on_ready(self): |
|
# called when wizard_data is available |
|
pass |
|
|
|
@pyqtSlot() |
|
def on_updated(self, *args): |
|
try: |
|
self.updated.emit(self) |
|
except RuntimeError: |
|
pass
|
|
|