2013-09-11 13:43:37 +02:00
|
|
|
#!/usr/bin/env python
|
|
|
|
|
#
|
|
|
|
|
# Electrum - lightweight Bitcoin client
|
|
|
|
|
# Copyright (C) 2012 thomasv@gitorious
|
|
|
|
|
#
|
2016-02-23 11:36:42 +01:00
|
|
|
# Permission is hereby granted, free of charge, to any person
|
|
|
|
|
# obtaining a copy of this software and associated documentation files
|
|
|
|
|
# (the "Software"), to deal in the Software without restriction,
|
|
|
|
|
# including without limitation the rights to use, copy, modify, merge,
|
|
|
|
|
# publish, distribute, sublicense, and/or sell copies of the Software,
|
|
|
|
|
# and to permit persons to whom the Software is furnished to do so,
|
|
|
|
|
# subject to the following conditions:
|
2013-09-11 13:43:37 +02:00
|
|
|
#
|
2016-02-23 11:36:42 +01:00
|
|
|
# The above copyright notice and this permission notice shall be
|
|
|
|
|
# included in all copies or substantial portions of the Software.
|
2013-09-11 13:43:37 +02:00
|
|
|
#
|
2016-02-23 11:36:42 +01:00
|
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
|
|
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
|
|
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
|
|
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
|
|
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
|
|
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
|
|
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
|
|
# SOFTWARE.
|
2013-09-11 13:43:37 +02:00
|
|
|
|
2019-02-11 20:21:24 +01:00
|
|
|
import os
|
2014-08-06 13:15:53 +02:00
|
|
|
import signal
|
2017-11-12 22:54:04 -06:00
|
|
|
import sys
|
2018-02-14 18:10:58 +01:00
|
|
|
import traceback
|
2018-11-14 22:39:49 +01:00
|
|
|
import threading
|
2022-06-30 20:27:03 +02:00
|
|
|
from typing import Optional, TYPE_CHECKING, List, Sequence
|
2017-11-12 22:54:04 -06:00
|
|
|
|
2022-04-11 16:53:25 +02:00
|
|
|
from electrum import GuiImportError
|
2013-09-11 13:43:37 +02:00
|
|
|
|
|
|
|
|
try:
|
2017-09-23 05:54:38 +02:00
|
|
|
import PyQt5
|
2022-04-11 17:37:10 +02:00
|
|
|
import PyQt5.QtGui
|
2022-04-11 16:53:25 +02:00
|
|
|
except Exception as e:
|
|
|
|
|
raise GuiImportError(
|
|
|
|
|
"Error: Could not import PyQt5 on Linux systems, "
|
|
|
|
|
"you may try 'sudo apt-get install python3-pyqt5'") from e
|
2013-09-11 13:43:37 +02:00
|
|
|
|
2019-02-11 20:21:24 +01:00
|
|
|
from PyQt5.QtGui import QGuiApplication
|
|
|
|
|
from PyQt5.QtWidgets import (QApplication, QSystemTrayIcon, QWidget, QMenu,
|
|
|
|
|
QMessageBox)
|
2021-06-25 15:48:22 +02:00
|
|
|
from PyQt5.QtCore import QObject, pyqtSignal, QTimer, Qt
|
2017-09-23 05:54:38 +02:00
|
|
|
import PyQt5.QtCore as QtCore
|
2013-09-11 13:43:37 +02:00
|
|
|
|
2022-03-21 15:50:28 +01:00
|
|
|
try:
|
|
|
|
|
# Preload QtMultimedia at app start, if available.
|
|
|
|
|
# We use QtMultimedia on some platforms for camera-handling, and
|
|
|
|
|
# lazy-loading it later led to some crashes. Maybe due to bugs in PyQt5. (see #7725)
|
|
|
|
|
from PyQt5.QtMultimedia import QCameraInfo; del QCameraInfo
|
|
|
|
|
except ImportError as e:
|
|
|
|
|
pass # failure is ok; it is an optional dependency.
|
|
|
|
|
|
2014-09-08 11:02:55 +02:00
|
|
|
from electrum.i18n import _, set_language
|
2018-07-11 17:38:47 +02:00
|
|
|
from electrum.plugin import run_hook
|
2018-05-01 14:39:01 +02:00
|
|
|
from electrum.base_wizard import GoBack
|
2021-07-07 19:19:43 +02:00
|
|
|
from electrum.util import (UserCancelled, profiler, send_exception_to_crash_reporter,
|
2018-12-11 21:29:23 +01:00
|
|
|
WalletFileException, BitcoinException, get_new_wallet_name)
|
2019-03-04 02:08:23 +01:00
|
|
|
from electrum.wallet import Wallet, Abstract_Wallet
|
2020-02-05 15:13:37 +01:00
|
|
|
from electrum.wallet_db import WalletDB
|
2019-04-26 18:52:26 +02:00
|
|
|
from electrum.logging import Logger
|
2021-11-05 20:21:50 +01:00
|
|
|
from electrum.gui import BaseElectrumGui
|
2023-05-24 17:41:44 +00:00
|
|
|
from electrum.simple_config import SimpleConfig
|
2017-01-22 21:25:24 +03:00
|
|
|
|
2019-03-04 02:08:23 +01:00
|
|
|
from .installwizard import InstallWizard, WalletAlreadyOpenInMemory
|
2023-05-05 17:00:18 +00:00
|
|
|
from .util import read_QIcon, ColorScheme, custom_message_box, MessageBoxMixin
|
2017-01-22 21:25:24 +03:00
|
|
|
from .main_window import ElectrumWindow
|
2017-03-15 12:13:20 +01:00
|
|
|
from .network_dialog import NetworkDialog
|
2019-05-05 02:14:07 +02:00
|
|
|
from .stylesheet_patcher import patch_qt_stylesheet
|
2019-05-07 09:10:23 +02:00
|
|
|
from .lightning_dialog import LightningDialog
|
2019-10-12 14:30:52 +02:00
|
|
|
from .watchtower_dialog import WatchtowerDialog
|
2021-07-07 19:19:43 +02:00
|
|
|
from .exception_window import Exception_Hook
|
2013-09-11 13:43:37 +02:00
|
|
|
|
2019-09-09 22:19:36 +02:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
from electrum.daemon import Daemon
|
|
|
|
|
from electrum.plugin import Plugins
|
|
|
|
|
|
2013-09-11 13:43:37 +02:00
|
|
|
|
|
|
|
|
class OpenFileEventFilter(QObject):
|
2022-06-30 20:27:03 +02:00
|
|
|
def __init__(self, windows: Sequence[ElectrumWindow]):
|
2013-09-11 13:43:37 +02:00
|
|
|
self.windows = windows
|
|
|
|
|
super(OpenFileEventFilter, self).__init__()
|
|
|
|
|
|
|
|
|
|
def eventFilter(self, obj, event):
|
|
|
|
|
if event.type() == QtCore.QEvent.FileOpen:
|
|
|
|
|
if len(self.windows) >= 1:
|
2023-03-19 13:32:43 +01:00
|
|
|
self.windows[0].set_payment_identifier(event.url().toString())
|
2013-09-11 13:43:37 +02:00
|
|
|
return True
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
2017-09-23 05:54:38 +02:00
|
|
|
class QElectrumApplication(QApplication):
|
|
|
|
|
new_window_signal = pyqtSignal(str, object)
|
2021-11-05 17:24:03 +01:00
|
|
|
quit_signal = pyqtSignal()
|
2022-04-29 16:56:19 +02:00
|
|
|
refresh_tabs_signal = pyqtSignal()
|
|
|
|
|
refresh_amount_edits_signal = pyqtSignal()
|
|
|
|
|
update_status_signal = pyqtSignal()
|
|
|
|
|
update_fiat_signal = pyqtSignal()
|
|
|
|
|
alias_received_signal = pyqtSignal()
|
2017-09-23 05:54:38 +02:00
|
|
|
|
|
|
|
|
|
2015-08-26 17:44:19 +02:00
|
|
|
|
2021-11-05 20:21:50 +01:00
|
|
|
class ElectrumGui(BaseElectrumGui, Logger):
|
2013-09-11 13:43:37 +02:00
|
|
|
|
2021-04-03 19:17:32 +02:00
|
|
|
network_dialog: Optional['NetworkDialog']
|
|
|
|
|
lightning_dialog: Optional['LightningDialog']
|
|
|
|
|
watchtower_dialog: Optional['WatchtowerDialog']
|
|
|
|
|
|
2018-11-16 14:39:22 +01:00
|
|
|
@profiler
|
2021-11-05 20:21:50 +01:00
|
|
|
def __init__(self, *, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins'):
|
|
|
|
|
BaseElectrumGui.__init__(self, config=config, daemon=daemon, plugins=plugins)
|
2019-04-26 18:52:26 +02:00
|
|
|
Logger.__init__(self)
|
2020-05-14 18:49:18 +02:00
|
|
|
self.logger.info(f"Qt GUI starting up... Qt={QtCore.QT_VERSION_STR}, PyQt={QtCore.PYQT_VERSION_STR}")
|
2015-11-13 23:11:43 +09:00
|
|
|
# Uncomment this call to verify objects are being properly
|
|
|
|
|
# GC-ed when windows are closed
|
|
|
|
|
#network.add_jobs([DebugMem([Abstract_Wallet, SPV, Synchronizer,
|
|
|
|
|
# ElectrumWindow], interval=5)])
|
2018-01-13 16:03:40 +00:00
|
|
|
if hasattr(QtCore.Qt, "AA_ShareOpenGLContexts"):
|
|
|
|
|
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts)
|
2018-02-08 17:33:57 +01:00
|
|
|
if hasattr(QGuiApplication, 'setDesktopFileName'):
|
|
|
|
|
QGuiApplication.setDesktopFileName('electrum.desktop')
|
2018-12-08 04:07:46 +01:00
|
|
|
self.gui_thread = threading.current_thread()
|
2020-05-25 17:31:13 +02:00
|
|
|
self.windows = [] # type: List[ElectrumWindow]
|
2013-09-11 13:43:37 +02:00
|
|
|
self.efilter = OpenFileEventFilter(self.windows)
|
2017-09-23 05:54:38 +02:00
|
|
|
self.app = QElectrumApplication(sys.argv)
|
2013-09-11 13:43:37 +02:00
|
|
|
self.app.installEventFilter(self.efilter)
|
2019-02-04 18:07:14 +01:00
|
|
|
self.app.setWindowIcon(read_QIcon("electrum.png"))
|
2021-04-03 19:17:32 +02:00
|
|
|
self._cleaned_up = False
|
2018-12-12 00:53:55 +02:00
|
|
|
# timer
|
|
|
|
|
self.timer = QTimer(self.app)
|
|
|
|
|
self.timer.setSingleShot(False)
|
|
|
|
|
self.timer.setInterval(500) # msec
|
|
|
|
|
|
2019-05-07 09:10:23 +02:00
|
|
|
self.network_dialog = None
|
|
|
|
|
self.lightning_dialog = None
|
2019-10-12 14:30:52 +02:00
|
|
|
self.watchtower_dialog = None
|
2018-11-14 22:39:49 +01:00
|
|
|
self._num_wizards_in_progress = 0
|
|
|
|
|
self._num_wizards_lock = threading.Lock()
|
2023-05-24 17:41:44 +00:00
|
|
|
self.dark_icon = self.config.GUI_QT_DARK_TRAY_ICON
|
2021-04-03 05:42:32 +02:00
|
|
|
self.tray = None
|
|
|
|
|
self._init_tray()
|
|
|
|
|
self.app.new_window_signal.connect(self.start_new_window)
|
2021-11-05 17:24:03 +01:00
|
|
|
self.app.quit_signal.connect(self.app.quit, Qt.QueuedConnection)
|
2022-02-18 18:08:38 +01:00
|
|
|
# maybe set dark theme
|
|
|
|
|
self._default_qtstylesheet = self.app.styleSheet()
|
|
|
|
|
self.reload_app_stylesheet()
|
|
|
|
|
|
2021-04-03 05:42:32 +02:00
|
|
|
run_hook('init_qt', self)
|
|
|
|
|
|
|
|
|
|
def _init_tray(self):
|
2015-09-02 11:45:05 +09:00
|
|
|
self.tray = QSystemTrayIcon(self.tray_icon(), None)
|
|
|
|
|
self.tray.setToolTip('Electrum')
|
|
|
|
|
self.tray.activated.connect(self.tray_activated)
|
|
|
|
|
self.build_tray_menu()
|
|
|
|
|
self.tray.show()
|
2018-06-25 16:45:56 +02:00
|
|
|
|
2022-02-18 18:08:38 +01:00
|
|
|
def reload_app_stylesheet(self):
|
2022-02-18 22:17:44 +01:00
|
|
|
"""Set the Qt stylesheet and custom colors according to the user-selected
|
|
|
|
|
light/dark theme.
|
|
|
|
|
TODO this can ~almost be used to change the theme at runtime (without app restart),
|
|
|
|
|
except for util.ColorScheme... widgets already created with colors set using
|
|
|
|
|
ColorSchemeItem.as_stylesheet() and similar will not get recolored.
|
|
|
|
|
See e.g.
|
|
|
|
|
- in Coins tab, the color for "frozen" UTXOs, or
|
|
|
|
|
- in TxDialog, the receiving/change address colors
|
|
|
|
|
"""
|
2023-05-24 17:41:44 +00:00
|
|
|
use_dark_theme = self.config.GUI_QT_COLOR_THEME == 'dark'
|
2018-06-25 16:45:56 +02:00
|
|
|
if use_dark_theme:
|
|
|
|
|
try:
|
|
|
|
|
import qdarkstyle
|
|
|
|
|
self.app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5())
|
|
|
|
|
except BaseException as e:
|
|
|
|
|
use_dark_theme = False
|
2019-04-26 18:52:26 +02:00
|
|
|
self.logger.warning(f'Error setting dark theme: {repr(e)}')
|
2022-02-18 18:08:38 +01:00
|
|
|
else:
|
|
|
|
|
self.app.setStyleSheet(self._default_qtstylesheet)
|
2019-05-05 02:14:07 +02:00
|
|
|
# Apply any necessary stylesheet patches
|
|
|
|
|
patch_qt_stylesheet(use_dark_theme=use_dark_theme)
|
2018-06-25 16:45:56 +02:00
|
|
|
# Even if we ourselves don't set the dark theme,
|
|
|
|
|
# the OS/window manager/etc might set *a dark theme*.
|
|
|
|
|
# Hence, try to choose colors accordingly:
|
|
|
|
|
ColorScheme.update_from_widget(QWidget(), force_dark=use_dark_theme)
|
2015-08-26 17:44:19 +02:00
|
|
|
|
2014-04-25 11:22:16 +02:00
|
|
|
def build_tray_menu(self):
|
2021-04-03 05:42:32 +02:00
|
|
|
if not self.tray:
|
|
|
|
|
return
|
2015-09-02 11:45:05 +09:00
|
|
|
# Avoid immediate GC of old menu when window closed via its action
|
2017-10-12 12:25:39 -04:00
|
|
|
if self.tray.contextMenu() is None:
|
|
|
|
|
m = QMenu()
|
|
|
|
|
self.tray.setContextMenu(m)
|
|
|
|
|
else:
|
|
|
|
|
m = self.tray.contextMenu()
|
|
|
|
|
m.clear()
|
2019-11-23 11:02:31 +01:00
|
|
|
network = self.daemon.network
|
|
|
|
|
m.addAction(_("Network"), self.show_network_dialog)
|
2020-01-09 18:21:48 +01:00
|
|
|
if network and network.lngossip:
|
2019-11-23 11:02:31 +01:00
|
|
|
m.addAction(_("Lightning Network"), self.show_lightning_dialog)
|
2020-01-09 18:21:48 +01:00
|
|
|
if network and network.local_watchtower:
|
2019-11-23 11:02:31 +01:00
|
|
|
m.addAction(_("Local Watchtower"), self.show_watchtower_dialog)
|
2015-09-02 11:45:05 +09:00
|
|
|
for window in self.windows:
|
2019-01-30 17:24:43 +01:00
|
|
|
name = window.wallet.basename()
|
|
|
|
|
submenu = m.addMenu(name)
|
2015-09-02 11:45:05 +09:00
|
|
|
submenu.addAction(_("Show/Hide"), window.show_or_hide)
|
|
|
|
|
submenu.addAction(_("Close"), window.close)
|
2014-04-25 11:22:16 +02:00
|
|
|
m.addAction(_("Dark/Light"), self.toggle_tray_icon)
|
|
|
|
|
m.addSeparator()
|
2021-04-04 01:05:23 +02:00
|
|
|
m.addAction(_("Exit Electrum"), self.app.quit)
|
2014-04-25 11:22:16 +02:00
|
|
|
|
2015-09-02 11:45:05 +09:00
|
|
|
def tray_icon(self):
|
|
|
|
|
if self.dark_icon:
|
2019-02-01 19:01:21 +01:00
|
|
|
return read_QIcon('electrum_dark_icon.png')
|
2015-09-02 11:45:05 +09:00
|
|
|
else:
|
2019-02-01 19:01:21 +01:00
|
|
|
return read_QIcon('electrum_light_icon.png')
|
2015-09-02 11:45:05 +09:00
|
|
|
|
2014-04-25 11:22:16 +02:00
|
|
|
def toggle_tray_icon(self):
|
2021-04-03 05:42:32 +02:00
|
|
|
if not self.tray:
|
|
|
|
|
return
|
2014-04-25 11:22:16 +02:00
|
|
|
self.dark_icon = not self.dark_icon
|
2023-05-24 17:41:44 +00:00
|
|
|
self.config.GUI_QT_DARK_TRAY_ICON = self.dark_icon
|
2015-09-02 11:45:05 +09:00
|
|
|
self.tray.setIcon(self.tray_icon())
|
2014-04-25 11:22:16 +02:00
|
|
|
|
|
|
|
|
def tray_activated(self, reason):
|
|
|
|
|
if reason == QSystemTrayIcon.DoubleClick:
|
2015-09-02 11:45:05 +09:00
|
|
|
if all([w.is_hidden() for w in self.windows]):
|
|
|
|
|
for w in self.windows:
|
|
|
|
|
w.bring_to_top()
|
2014-04-25 11:22:16 +02:00
|
|
|
else:
|
2015-09-02 11:45:05 +09:00
|
|
|
for w in self.windows:
|
|
|
|
|
w.hide()
|
2014-04-25 11:22:16 +02:00
|
|
|
|
2021-04-03 19:17:32 +02:00
|
|
|
def _cleanup_before_exit(self):
|
|
|
|
|
if self._cleaned_up:
|
|
|
|
|
return
|
|
|
|
|
self._cleaned_up = True
|
|
|
|
|
self.app.new_window_signal.disconnect()
|
|
|
|
|
self.efilter = None
|
|
|
|
|
# If there are still some open windows, try to clean them up.
|
|
|
|
|
for window in list(self.windows):
|
2015-09-02 11:45:05 +09:00
|
|
|
window.close()
|
2021-04-03 19:17:32 +02:00
|
|
|
window.clean_up()
|
2019-05-07 09:10:23 +02:00
|
|
|
if self.network_dialog:
|
|
|
|
|
self.network_dialog.close()
|
2021-04-03 19:17:32 +02:00
|
|
|
self.network_dialog.clean_up()
|
|
|
|
|
self.network_dialog = None
|
2019-05-07 09:10:23 +02:00
|
|
|
if self.lightning_dialog:
|
|
|
|
|
self.lightning_dialog.close()
|
2021-04-03 19:17:32 +02:00
|
|
|
self.lightning_dialog = None
|
2019-10-12 14:30:52 +02:00
|
|
|
if self.watchtower_dialog:
|
|
|
|
|
self.watchtower_dialog.close()
|
2021-04-03 19:17:32 +02:00
|
|
|
self.watchtower_dialog = None
|
|
|
|
|
# Shut down the timer cleanly
|
|
|
|
|
self.timer.stop()
|
|
|
|
|
self.timer = None
|
|
|
|
|
# clipboard persistence. see http://www.mail-archive.com/pyqt@riverbankcomputing.com/msg17328.html
|
|
|
|
|
event = QtCore.QEvent(QtCore.QEvent.Clipboard)
|
|
|
|
|
self.app.sendEvent(self.app.clipboard(), event)
|
|
|
|
|
if self.tray:
|
|
|
|
|
self.tray.hide()
|
|
|
|
|
self.tray.deleteLater()
|
|
|
|
|
self.tray = None
|
|
|
|
|
|
2021-04-04 00:49:25 +02:00
|
|
|
def _maybe_quit_if_no_windows_open(self) -> None:
|
|
|
|
|
"""Check if there are any open windows and decide whether we should quit."""
|
|
|
|
|
# keep daemon running after close
|
|
|
|
|
if self.config.get('daemon'):
|
|
|
|
|
return
|
|
|
|
|
# check if a wizard is in progress
|
|
|
|
|
with self._num_wizards_lock:
|
|
|
|
|
if self._num_wizards_in_progress > 0 or len(self.windows) > 0:
|
|
|
|
|
return
|
|
|
|
|
self.app.quit()
|
|
|
|
|
|
2015-09-03 11:27:33 +02:00
|
|
|
def new_window(self, path, uri=None):
|
2015-09-02 20:34:40 +09:00
|
|
|
# Use a signal as can be called from daemon thread
|
2017-09-23 05:54:38 +02:00
|
|
|
self.app.new_window_signal.emit(path, uri)
|
2015-09-01 12:16:07 +02:00
|
|
|
|
2019-05-07 09:10:23 +02:00
|
|
|
def show_lightning_dialog(self):
|
2020-10-08 06:36:02 +02:00
|
|
|
if not self.daemon.network.has_channel_db():
|
2020-03-06 11:23:26 +01:00
|
|
|
return
|
2019-05-07 09:10:23 +02:00
|
|
|
if not self.lightning_dialog:
|
|
|
|
|
self.lightning_dialog = LightningDialog(self)
|
|
|
|
|
self.lightning_dialog.bring_to_top()
|
2019-10-12 14:30:52 +02:00
|
|
|
|
|
|
|
|
def show_watchtower_dialog(self):
|
|
|
|
|
if not self.watchtower_dialog:
|
|
|
|
|
self.watchtower_dialog = WatchtowerDialog(self)
|
|
|
|
|
self.watchtower_dialog.bring_to_top()
|
2019-01-30 17:24:43 +01:00
|
|
|
|
2019-11-23 11:02:31 +01:00
|
|
|
def show_network_dialog(self):
|
2019-05-07 09:10:23 +02:00
|
|
|
if self.network_dialog:
|
2022-06-16 12:05:05 +02:00
|
|
|
self.network_dialog.on_event_network_updated()
|
2019-05-07 09:10:23 +02:00
|
|
|
self.network_dialog.show()
|
|
|
|
|
self.network_dialog.raise_()
|
2017-07-08 15:23:00 +02:00
|
|
|
return
|
2021-04-03 19:17:32 +02:00
|
|
|
self.network_dialog = NetworkDialog(
|
|
|
|
|
network=self.daemon.network,
|
2022-06-16 12:05:05 +02:00
|
|
|
config=self.config)
|
2019-05-07 09:10:23 +02:00
|
|
|
self.network_dialog.show()
|
2017-07-08 15:23:00 +02:00
|
|
|
|
2019-03-04 02:08:23 +01:00
|
|
|
def _create_window_for_wallet(self, wallet):
|
2015-12-31 12:02:16 +09:00
|
|
|
w = ElectrumWindow(self, wallet)
|
|
|
|
|
self.windows.append(w)
|
|
|
|
|
self.build_tray_menu()
|
2019-04-28 06:31:01 +02:00
|
|
|
w.warn_if_testnet()
|
2018-12-04 11:52:31 +01:00
|
|
|
w.warn_if_watching_only()
|
2015-12-31 12:02:16 +09:00
|
|
|
return w
|
|
|
|
|
|
2018-11-14 22:39:49 +01:00
|
|
|
def count_wizards_in_progress(func):
|
|
|
|
|
def wrapper(self: 'ElectrumGui', *args, **kwargs):
|
|
|
|
|
with self._num_wizards_lock:
|
|
|
|
|
self._num_wizards_in_progress += 1
|
|
|
|
|
try:
|
|
|
|
|
return func(self, *args, **kwargs)
|
|
|
|
|
finally:
|
|
|
|
|
with self._num_wizards_lock:
|
|
|
|
|
self._num_wizards_in_progress -= 1
|
2021-04-04 00:49:25 +02:00
|
|
|
self._maybe_quit_if_no_windows_open()
|
2018-11-14 22:39:49 +01:00
|
|
|
return wrapper
|
|
|
|
|
|
|
|
|
|
@count_wizards_in_progress
|
2022-03-23 03:58:33 +01:00
|
|
|
def start_new_window(
|
|
|
|
|
self,
|
|
|
|
|
path,
|
|
|
|
|
uri: Optional[str],
|
|
|
|
|
*,
|
|
|
|
|
app_is_starting: bool = False,
|
|
|
|
|
force_wizard: bool = False,
|
|
|
|
|
) -> Optional[ElectrumWindow]:
|
2023-02-02 15:25:15 +00:00
|
|
|
"""Raises the window for the wallet if it is open.
|
|
|
|
|
Otherwise, opens the wallet and creates a new window for it.
|
|
|
|
|
Warning: the returned window might be for a completely different wallet
|
|
|
|
|
than the provided path, as we allow user interaction to change the path.
|
|
|
|
|
"""
|
2019-03-04 02:08:23 +01:00
|
|
|
wallet = None
|
2022-03-23 03:58:33 +01:00
|
|
|
# Try to open with daemon first. If this succeeds, there won't be a wizard at all
|
|
|
|
|
# (the wallet main window will appear directly).
|
|
|
|
|
if not force_wizard:
|
|
|
|
|
try:
|
|
|
|
|
wallet = self.daemon.load_wallet(path, None)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
self.logger.exception('')
|
2023-05-11 13:48:54 +00:00
|
|
|
err_text = str(e) if isinstance(e, WalletFileException) else repr(e)
|
2022-03-23 03:58:33 +01:00
|
|
|
custom_message_box(icon=QMessageBox.Warning,
|
|
|
|
|
parent=None,
|
|
|
|
|
title=_('Error'),
|
2023-05-11 13:48:54 +00:00
|
|
|
text=_('Cannot load wallet') + ' (1):\n' + err_text)
|
|
|
|
|
if isinstance(e, WalletFileException) and e.should_report_crash:
|
|
|
|
|
send_exception_to_crash_reporter(e)
|
2022-03-23 03:58:33 +01:00
|
|
|
# if app is starting, still let wizard appear
|
|
|
|
|
if not app_is_starting:
|
|
|
|
|
return
|
|
|
|
|
# Open a wizard window. This lets the user e.g. enter a password, or select
|
|
|
|
|
# a different wallet.
|
2018-03-14 23:46:23 -06:00
|
|
|
try:
|
2022-03-15 14:23:30 +01:00
|
|
|
if not wallet:
|
|
|
|
|
wallet = self._start_wizard_to_select_or_create_wallet(path)
|
|
|
|
|
if not wallet:
|
|
|
|
|
return
|
|
|
|
|
# create or raise window
|
2019-03-04 02:20:34 +01:00
|
|
|
for window in self.windows:
|
|
|
|
|
if window.wallet.storage.path == wallet.storage.path:
|
2018-10-25 00:18:14 +02:00
|
|
|
break
|
|
|
|
|
else:
|
2019-03-04 02:20:34 +01:00
|
|
|
window = self._create_window_for_wallet(wallet)
|
2021-04-03 01:47:44 +02:00
|
|
|
except Exception as e:
|
2019-04-26 18:52:26 +02:00
|
|
|
self.logger.exception('')
|
2023-05-11 13:48:54 +00:00
|
|
|
err_text = str(e) if isinstance(e, WalletFileException) else repr(e)
|
2019-05-13 23:59:29 +02:00
|
|
|
custom_message_box(icon=QMessageBox.Warning,
|
|
|
|
|
parent=None,
|
|
|
|
|
title=_('Error'),
|
2023-05-11 13:48:54 +00:00
|
|
|
text=_('Cannot load wallet') + '(2) :\n' + err_text)
|
|
|
|
|
if isinstance(e, WalletFileException) and e.should_report_crash:
|
|
|
|
|
send_exception_to_crash_reporter(e)
|
2018-12-11 21:29:23 +01:00
|
|
|
if app_is_starting:
|
2022-03-23 03:58:33 +01:00
|
|
|
# If we raise in this context, there are no more fallbacks, we will shut down.
|
|
|
|
|
# Worst case scenario, we might have gotten here without user interaction,
|
|
|
|
|
# in which case, if we raise now without user interaction, the same sequence of
|
|
|
|
|
# events is likely to repeat when the user restarts the process.
|
|
|
|
|
# So we play it safe: clear path, clear uri, force a wizard to appear.
|
|
|
|
|
try:
|
|
|
|
|
wallet_dir = os.path.dirname(path)
|
|
|
|
|
filename = get_new_wallet_name(wallet_dir)
|
|
|
|
|
except OSError:
|
|
|
|
|
path = self.config.get_fallback_wallet_path()
|
|
|
|
|
else:
|
|
|
|
|
path = os.path.join(wallet_dir, filename)
|
2023-02-02 15:25:15 +00:00
|
|
|
return self.start_new_window(path, uri=None, force_wizard=True)
|
2018-03-14 23:46:23 -06:00
|
|
|
return
|
2019-03-04 02:20:34 +01:00
|
|
|
window.bring_to_top()
|
|
|
|
|
window.setWindowState(window.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive)
|
|
|
|
|
window.activateWindow()
|
2022-08-16 15:27:13 +00:00
|
|
|
if uri:
|
2023-07-07 20:48:29 +02:00
|
|
|
window.show_send_tab()
|
2023-03-19 13:32:43 +01:00
|
|
|
window.send_tab.set_payment_identifier(uri)
|
2019-03-04 02:20:34 +01:00
|
|
|
return window
|
2014-04-25 11:22:16 +02:00
|
|
|
|
2019-03-04 02:08:23 +01:00
|
|
|
def _start_wizard_to_select_or_create_wallet(self, path) -> Optional[Abstract_Wallet]:
|
2020-04-01 18:42:06 +02:00
|
|
|
wizard = InstallWizard(self.config, self.app, self.plugins, gui_object=self)
|
2019-03-04 02:08:23 +01:00
|
|
|
try:
|
|
|
|
|
path, storage = wizard.select_storage(path, self.daemon.get_wallet)
|
|
|
|
|
# storage is None if file does not exist
|
|
|
|
|
if storage is None:
|
|
|
|
|
wizard.path = path # needed by trustedcoin plugin
|
|
|
|
|
wizard.run('new')
|
2020-02-05 15:13:37 +01:00
|
|
|
storage, db = wizard.create_storage(path)
|
2019-03-04 02:08:23 +01:00
|
|
|
else:
|
2020-02-05 15:13:37 +01:00
|
|
|
db = WalletDB(storage.read(), manual_upgrades=False)
|
|
|
|
|
wizard.run_upgrades(storage, db)
|
2019-03-04 02:08:23 +01:00
|
|
|
except (UserCancelled, GoBack):
|
|
|
|
|
return
|
|
|
|
|
except WalletAlreadyOpenInMemory as e:
|
|
|
|
|
return e.wallet
|
|
|
|
|
finally:
|
|
|
|
|
wizard.terminate()
|
|
|
|
|
# return if wallet creation is not complete
|
2020-02-05 15:13:37 +01:00
|
|
|
if storage is None or db.get_action():
|
2019-03-04 02:08:23 +01:00
|
|
|
return
|
2020-02-05 15:13:37 +01:00
|
|
|
wallet = Wallet(db, storage, config=self.config)
|
2019-03-04 02:08:23 +01:00
|
|
|
wallet.start_network(self.daemon.network)
|
|
|
|
|
self.daemon.add_wallet(wallet)
|
|
|
|
|
return wallet
|
|
|
|
|
|
2019-05-14 17:04:03 +02:00
|
|
|
def close_window(self, window: ElectrumWindow):
|
2018-08-30 14:35:01 +00:00
|
|
|
if window in self.windows:
|
2021-04-03 01:44:42 +02:00
|
|
|
self.windows.remove(window)
|
2015-09-02 11:45:05 +09:00
|
|
|
self.build_tray_menu()
|
2015-12-05 16:53:56 +01:00
|
|
|
# save wallet path of last open window
|
2016-03-08 11:10:04 +01:00
|
|
|
if not self.windows:
|
|
|
|
|
self.config.save_last_wallet(window.wallet)
|
2016-01-12 20:19:21 +09:00
|
|
|
run_hook('on_close_window', window)
|
2018-09-13 16:25:56 +02:00
|
|
|
self.daemon.stop_wallet(window.wallet.storage.path)
|
2014-04-25 11:22:16 +02:00
|
|
|
|
2016-10-14 14:05:24 +02:00
|
|
|
def init_network(self):
|
2023-03-29 22:09:46 +00:00
|
|
|
"""Start the network, including showing a first-start network dialog if config does not exist."""
|
2016-10-14 14:05:24 +02:00
|
|
|
if self.daemon.network:
|
2023-03-29 22:09:46 +00:00
|
|
|
# first-start network-setup
|
2023-05-24 17:41:44 +00:00
|
|
|
if not self.config.cv.NETWORK_AUTO_CONNECT.is_set():
|
2020-04-01 18:42:06 +02:00
|
|
|
wizard = InstallWizard(self.config, self.app, self.plugins, gui_object=self)
|
2016-10-14 14:05:24 +02:00
|
|
|
wizard.init_network(self.daemon.network)
|
|
|
|
|
wizard.terminate()
|
2023-03-29 22:09:46 +00:00
|
|
|
# start network
|
|
|
|
|
self.daemon.start_network()
|
2016-10-14 14:05:24 +02:00
|
|
|
|
2015-08-26 17:44:19 +02:00
|
|
|
def main(self):
|
2021-04-06 18:27:28 +02:00
|
|
|
# setup Ctrl-C handling and tear-down code first, so that user can easily exit whenever
|
|
|
|
|
self.app.setQuitOnLastWindowClosed(False) # so _we_ can decide whether to quit
|
|
|
|
|
self.app.lastWindowClosed.connect(self._maybe_quit_if_no_windows_open)
|
|
|
|
|
self.app.aboutToQuit.connect(self._cleanup_before_exit)
|
|
|
|
|
signal.signal(signal.SIGINT, lambda *args: self.app.quit())
|
2021-07-07 19:19:43 +02:00
|
|
|
# hook for crash reporter
|
|
|
|
|
Exception_Hook.maybe_setup(config=self.config)
|
2023-03-29 22:09:46 +00:00
|
|
|
# start network, and maybe show first-start network-setup
|
2016-10-14 14:05:24 +02:00
|
|
|
try:
|
|
|
|
|
self.init_network()
|
2016-10-24 14:57:02 +02:00
|
|
|
except UserCancelled:
|
|
|
|
|
return
|
|
|
|
|
except GoBack:
|
|
|
|
|
return
|
2021-04-03 01:47:44 +02:00
|
|
|
except Exception as e:
|
2019-04-26 18:52:26 +02:00
|
|
|
self.logger.exception('')
|
2016-10-14 14:05:24 +02:00
|
|
|
return
|
2021-04-06 18:27:28 +02:00
|
|
|
# start wizard to select/create wallet
|
2015-08-26 17:44:19 +02:00
|
|
|
self.timer.start()
|
2019-09-10 17:10:52 +02:00
|
|
|
path = self.config.get_wallet_path(use_gui_last_wallet=True)
|
2021-07-07 19:19:43 +02:00
|
|
|
try:
|
|
|
|
|
if not self.start_new_window(path, self.config.get('url'), app_is_starting=True):
|
|
|
|
|
return
|
|
|
|
|
except Exception as e:
|
|
|
|
|
self.logger.error("error loading wallet (or creating window for it)")
|
|
|
|
|
send_exception_to_crash_reporter(e)
|
|
|
|
|
# Let Qt event loop start properly so that crash reporter window can appear.
|
|
|
|
|
# We will shutdown when the user closes that window, via lastWindowClosed signal.
|
2015-08-26 17:44:19 +02:00
|
|
|
# main loop
|
2021-07-07 19:19:43 +02:00
|
|
|
self.logger.info("starting Qt main loop")
|
2013-09-11 13:43:37 +02:00
|
|
|
self.app.exec_()
|
2021-07-07 19:19:43 +02:00
|
|
|
# on some platforms the exec_ call may not return, so use _cleanup_before_exit
|
2018-09-13 16:25:56 +02:00
|
|
|
|
|
|
|
|
def stop(self):
|
2019-04-26 18:52:26 +02:00
|
|
|
self.logger.info('closing GUI')
|
2021-11-05 17:24:03 +01:00
|
|
|
self.app.quit_signal.emit()
|
2022-04-11 17:05:26 +02:00
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def version_info(cls):
|
|
|
|
|
ret = {
|
|
|
|
|
"qt.version": QtCore.QT_VERSION_STR,
|
|
|
|
|
"pyqt.version": QtCore.PYQT_VERSION_STR,
|
|
|
|
|
}
|
|
|
|
|
if hasattr(PyQt5, "__path__"):
|
|
|
|
|
ret["pyqt.path"] = ", ".join(PyQt5.__path__ or [])
|
|
|
|
|
return ret
|