t__init__.py - electrum - Electrum Bitcoin wallet
HTML git clone https://git.parazyd.org/electrum
DIR Log
DIR Files
DIR Refs
DIR Submodules
---
t__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()