__init__.py (15724B)
1 #!/usr/bin/env python 2 # 3 # Electrum - lightweight Bitcoin client 4 # Copyright (C) 2012 thomasv@gitorious 5 # 6 # Permission is hereby granted, free of charge, to any person 7 # obtaining a copy of this software and associated documentation files 8 # (the "Software"), to deal in the Software without restriction, 9 # including without limitation the rights to use, copy, modify, merge, 10 # publish, distribute, sublicense, and/or sell copies of the Software, 11 # and to permit persons to whom the Software is furnished to do so, 12 # subject to the following conditions: 13 # 14 # The above copyright notice and this permission notice shall be 15 # included in all copies or substantial portions of the Software. 16 # 17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 21 # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 22 # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 23 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 # SOFTWARE. 25 26 import os 27 import signal 28 import sys 29 import traceback 30 import threading 31 from typing import Optional, TYPE_CHECKING, List 32 33 34 try: 35 import PyQt5 36 except Exception: 37 sys.exit("Error: Could not import PyQt5 on Linux systems, you may try 'sudo apt-get install python3-pyqt5'") 38 39 from PyQt5.QtGui import QGuiApplication 40 from PyQt5.QtWidgets import (QApplication, QSystemTrayIcon, QWidget, QMenu, 41 QMessageBox) 42 from PyQt5.QtCore import QObject, pyqtSignal, QTimer 43 import PyQt5.QtCore as QtCore 44 45 from electrum.i18n import _, set_language 46 from electrum.plugin import run_hook 47 from electrum.base_wizard import GoBack 48 from electrum.util import (UserCancelled, profiler, 49 WalletFileException, BitcoinException, get_new_wallet_name) 50 from electrum.wallet import Wallet, Abstract_Wallet 51 from electrum.wallet_db import WalletDB 52 from electrum.logging import Logger 53 54 from .installwizard import InstallWizard, WalletAlreadyOpenInMemory 55 from .util import get_default_language, read_QIcon, ColorScheme, custom_message_box 56 from .main_window import ElectrumWindow 57 from .network_dialog import NetworkDialog 58 from .stylesheet_patcher import patch_qt_stylesheet 59 from .lightning_dialog import LightningDialog 60 from .watchtower_dialog import WatchtowerDialog 61 62 if TYPE_CHECKING: 63 from electrum.daemon import Daemon 64 from electrum.simple_config import SimpleConfig 65 from electrum.plugin import Plugins 66 67 68 class OpenFileEventFilter(QObject): 69 def __init__(self, windows): 70 self.windows = windows 71 super(OpenFileEventFilter, self).__init__() 72 73 def eventFilter(self, obj, event): 74 if event.type() == QtCore.QEvent.FileOpen: 75 if len(self.windows) >= 1: 76 self.windows[0].pay_to_URI(event.url().toString()) 77 return True 78 return False 79 80 81 class QElectrumApplication(QApplication): 82 new_window_signal = pyqtSignal(str, object) 83 84 85 class QNetworkUpdatedSignalObject(QObject): 86 network_updated_signal = pyqtSignal(str, object) 87 88 89 class ElectrumGui(Logger): 90 91 @profiler 92 def __init__(self, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins'): 93 set_language(config.get('language', get_default_language())) 94 Logger.__init__(self) 95 self.logger.info(f"Qt GUI starting up... Qt={QtCore.QT_VERSION_STR}, PyQt={QtCore.PYQT_VERSION_STR}") 96 # Uncomment this call to verify objects are being properly 97 # GC-ed when windows are closed 98 #network.add_jobs([DebugMem([Abstract_Wallet, SPV, Synchronizer, 99 # ElectrumWindow], interval=5)]) 100 QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) 101 if hasattr(QtCore.Qt, "AA_ShareOpenGLContexts"): 102 QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts) 103 if hasattr(QGuiApplication, 'setDesktopFileName'): 104 QGuiApplication.setDesktopFileName('electrum.desktop') 105 self.gui_thread = threading.current_thread() 106 self.config = config 107 self.daemon = daemon 108 self.plugins = plugins 109 self.windows = [] # type: List[ElectrumWindow] 110 self.efilter = OpenFileEventFilter(self.windows) 111 self.app = QElectrumApplication(sys.argv) 112 self.app.installEventFilter(self.efilter) 113 self.app.setWindowIcon(read_QIcon("electrum.png")) 114 # timer 115 self.timer = QTimer(self.app) 116 self.timer.setSingleShot(False) 117 self.timer.setInterval(500) # msec 118 119 self.network_dialog = None 120 self.lightning_dialog = None 121 self.watchtower_dialog = None 122 self.network_updated_signal_obj = QNetworkUpdatedSignalObject() 123 self._num_wizards_in_progress = 0 124 self._num_wizards_lock = threading.Lock() 125 # init tray 126 self.dark_icon = self.config.get("dark_icon", False) 127 self.tray = QSystemTrayIcon(self.tray_icon(), None) 128 self.tray.setToolTip('Electrum') 129 self.tray.activated.connect(self.tray_activated) 130 self.build_tray_menu() 131 self.tray.show() 132 self.app.new_window_signal.connect(self.start_new_window) 133 self.set_dark_theme_if_needed() 134 run_hook('init_qt', self) 135 136 def set_dark_theme_if_needed(self): 137 use_dark_theme = self.config.get('qt_gui_color_theme', 'default') == 'dark' 138 if use_dark_theme: 139 try: 140 import qdarkstyle 141 self.app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5()) 142 except BaseException as e: 143 use_dark_theme = False 144 self.logger.warning(f'Error setting dark theme: {repr(e)}') 145 # Apply any necessary stylesheet patches 146 patch_qt_stylesheet(use_dark_theme=use_dark_theme) 147 # Even if we ourselves don't set the dark theme, 148 # the OS/window manager/etc might set *a dark theme*. 149 # Hence, try to choose colors accordingly: 150 ColorScheme.update_from_widget(QWidget(), force_dark=use_dark_theme) 151 152 def build_tray_menu(self): 153 # Avoid immediate GC of old menu when window closed via its action 154 if self.tray.contextMenu() is None: 155 m = QMenu() 156 self.tray.setContextMenu(m) 157 else: 158 m = self.tray.contextMenu() 159 m.clear() 160 network = self.daemon.network 161 m.addAction(_("Network"), self.show_network_dialog) 162 if network and network.lngossip: 163 m.addAction(_("Lightning Network"), self.show_lightning_dialog) 164 if network and network.local_watchtower: 165 m.addAction(_("Local Watchtower"), self.show_watchtower_dialog) 166 for window in self.windows: 167 name = window.wallet.basename() 168 submenu = m.addMenu(name) 169 submenu.addAction(_("Show/Hide"), window.show_or_hide) 170 submenu.addAction(_("Close"), window.close) 171 m.addAction(_("Dark/Light"), self.toggle_tray_icon) 172 m.addSeparator() 173 m.addAction(_("Exit Electrum"), self.close) 174 175 def tray_icon(self): 176 if self.dark_icon: 177 return read_QIcon('electrum_dark_icon.png') 178 else: 179 return read_QIcon('electrum_light_icon.png') 180 181 def toggle_tray_icon(self): 182 self.dark_icon = not self.dark_icon 183 self.config.set_key("dark_icon", self.dark_icon, True) 184 self.tray.setIcon(self.tray_icon()) 185 186 def tray_activated(self, reason): 187 if reason == QSystemTrayIcon.DoubleClick: 188 if all([w.is_hidden() for w in self.windows]): 189 for w in self.windows: 190 w.bring_to_top() 191 else: 192 for w in self.windows: 193 w.hide() 194 195 def close(self): 196 for window in self.windows: 197 window.close() 198 if self.network_dialog: 199 self.network_dialog.close() 200 if self.lightning_dialog: 201 self.lightning_dialog.close() 202 if self.watchtower_dialog: 203 self.watchtower_dialog.close() 204 self.app.quit() 205 206 def new_window(self, path, uri=None): 207 # Use a signal as can be called from daemon thread 208 self.app.new_window_signal.emit(path, uri) 209 210 def show_lightning_dialog(self): 211 if not self.daemon.network.has_channel_db(): 212 return 213 if not self.lightning_dialog: 214 self.lightning_dialog = LightningDialog(self) 215 self.lightning_dialog.bring_to_top() 216 217 def show_watchtower_dialog(self): 218 if not self.watchtower_dialog: 219 self.watchtower_dialog = WatchtowerDialog(self) 220 self.watchtower_dialog.bring_to_top() 221 222 def show_network_dialog(self): 223 if self.network_dialog: 224 self.network_dialog.on_update() 225 self.network_dialog.show() 226 self.network_dialog.raise_() 227 return 228 self.network_dialog = NetworkDialog(self.daemon.network, self.config, 229 self.network_updated_signal_obj) 230 self.network_dialog.show() 231 232 def _create_window_for_wallet(self, wallet): 233 w = ElectrumWindow(self, wallet) 234 self.windows.append(w) 235 self.build_tray_menu() 236 w.warn_if_testnet() 237 w.warn_if_watching_only() 238 return w 239 240 def count_wizards_in_progress(func): 241 def wrapper(self: 'ElectrumGui', *args, **kwargs): 242 with self._num_wizards_lock: 243 self._num_wizards_in_progress += 1 244 try: 245 return func(self, *args, **kwargs) 246 finally: 247 with self._num_wizards_lock: 248 self._num_wizards_in_progress -= 1 249 return wrapper 250 251 @count_wizards_in_progress 252 def start_new_window(self, path, uri, *, app_is_starting=False): 253 '''Raises the window for the wallet if it is open. Otherwise 254 opens the wallet and creates a new window for it''' 255 wallet = None 256 try: 257 wallet = self.daemon.load_wallet(path, None) 258 except BaseException as e: 259 self.logger.exception('') 260 custom_message_box(icon=QMessageBox.Warning, 261 parent=None, 262 title=_('Error'), 263 text=_('Cannot load wallet') + ' (1):\n' + repr(e)) 264 # if app is starting, still let wizard to appear 265 if not app_is_starting: 266 return 267 if not wallet: 268 try: 269 wallet = self._start_wizard_to_select_or_create_wallet(path) 270 except (WalletFileException, BitcoinException) as e: 271 self.logger.exception('') 272 custom_message_box(icon=QMessageBox.Warning, 273 parent=None, 274 title=_('Error'), 275 text=_('Cannot load wallet') + ' (2):\n' + repr(e)) 276 if not wallet: 277 return 278 # create or raise window 279 try: 280 for window in self.windows: 281 if window.wallet.storage.path == wallet.storage.path: 282 break 283 else: 284 window = self._create_window_for_wallet(wallet) 285 except BaseException as e: 286 self.logger.exception('') 287 custom_message_box(icon=QMessageBox.Warning, 288 parent=None, 289 title=_('Error'), 290 text=_('Cannot create window for wallet') + ':\n' + repr(e)) 291 if app_is_starting: 292 wallet_dir = os.path.dirname(path) 293 path = os.path.join(wallet_dir, get_new_wallet_name(wallet_dir)) 294 self.start_new_window(path, uri) 295 return 296 if uri: 297 window.pay_to_URI(uri) 298 window.bring_to_top() 299 window.setWindowState(window.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) 300 301 window.activateWindow() 302 return window 303 304 def _start_wizard_to_select_or_create_wallet(self, path) -> Optional[Abstract_Wallet]: 305 wizard = InstallWizard(self.config, self.app, self.plugins, gui_object=self) 306 try: 307 path, storage = wizard.select_storage(path, self.daemon.get_wallet) 308 # storage is None if file does not exist 309 if storage is None: 310 wizard.path = path # needed by trustedcoin plugin 311 wizard.run('new') 312 storage, db = wizard.create_storage(path) 313 else: 314 db = WalletDB(storage.read(), manual_upgrades=False) 315 wizard.run_upgrades(storage, db) 316 except (UserCancelled, GoBack): 317 return 318 except WalletAlreadyOpenInMemory as e: 319 return e.wallet 320 finally: 321 wizard.terminate() 322 # return if wallet creation is not complete 323 if storage is None or db.get_action(): 324 return 325 wallet = Wallet(db, storage, config=self.config) 326 wallet.start_network(self.daemon.network) 327 self.daemon.add_wallet(wallet) 328 return wallet 329 330 def close_window(self, window: ElectrumWindow): 331 if window in self.windows: 332 self.windows.remove(window) 333 self.build_tray_menu() 334 # save wallet path of last open window 335 if not self.windows: 336 self.config.save_last_wallet(window.wallet) 337 run_hook('on_close_window', window) 338 self.daemon.stop_wallet(window.wallet.storage.path) 339 340 def init_network(self): 341 # Show network dialog if config does not exist 342 if self.daemon.network: 343 if self.config.get('auto_connect') is None: 344 wizard = InstallWizard(self.config, self.app, self.plugins, gui_object=self) 345 wizard.init_network(self.daemon.network) 346 wizard.terminate() 347 348 def main(self): 349 try: 350 self.init_network() 351 except UserCancelled: 352 return 353 except GoBack: 354 return 355 except BaseException as e: 356 self.logger.exception('') 357 return 358 self.timer.start() 359 360 path = self.config.get_wallet_path(use_gui_last_wallet=True) 361 if not self.start_new_window(path, self.config.get('url'), app_is_starting=True): 362 return 363 signal.signal(signal.SIGINT, lambda *args: self.app.quit()) 364 365 def quit_after_last_window(): 366 # keep daemon running after close 367 if self.config.get('daemon'): 368 return 369 # check if a wizard is in progress 370 with self._num_wizards_lock: 371 if self._num_wizards_in_progress > 0 or len(self.windows) > 0: 372 return 373 if self.config.get('persist_daemon'): 374 return 375 self.app.quit() 376 self.app.setQuitOnLastWindowClosed(False) # so _we_ can decide whether to quit 377 self.app.lastWindowClosed.connect(quit_after_last_window) 378 379 def clean_up(): 380 # Shut down the timer cleanly 381 self.timer.stop() 382 # clipboard persistence. see http://www.mail-archive.com/pyqt@riverbankcomputing.com/msg17328.html 383 event = QtCore.QEvent(QtCore.QEvent.Clipboard) 384 self.app.sendEvent(self.app.clipboard(), event) 385 self.tray.hide() 386 self.app.aboutToQuit.connect(clean_up) 387 388 # main loop 389 self.app.exec_() 390 # on some platforms the exec_ call may not return, so use clean_up() 391 392 def stop(self): 393 self.logger.info('closing GUI') 394 self.app.quit()