class ElectrumGui(Logger): network_dialog: Optional['NetworkDialog'] @profiler def __init__(self, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins'): set_language(config.get('language', get_default_language())) Logger.__init__(self) self.logger.info( f"Qt GUI starting up... Qt={QtCore.QT_VERSION_STR}, PyQt={QtCore.PYQT_VERSION_STR}" ) # 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)]) QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) if hasattr(QtCore.Qt, "AA_ShareOpenGLContexts"): QtCore.QCoreApplication.setAttribute( QtCore.Qt.AA_ShareOpenGLContexts) if hasattr(QGuiApplication, 'setDesktopFileName'): QGuiApplication.setDesktopFileName('electrum-dash.desktop') self.gui_thread = threading.current_thread() self.config = config self.daemon = daemon self.plugins = plugins self.windows = [] # type: List[ElectrumWindow] self.efilter = OpenFileEventFilter(self.windows) self.app = QElectrumApplication(sys.argv) self.app.installEventFilter(self.efilter) self.app.setWindowIcon(read_QIcon("electrum-dash.png")) self._cleaned_up = False # timer self.timer = QTimer(self.app) self.timer.setSingleShot(False) self.timer.setInterval(500) # msec self.network_dialog = None self.dash_net_dialog = None self.network_updated_signal_obj = QNetworkUpdatedSignalObject() self.dash_net_sobj = QDashNetSignalsObject() self._num_wizards_in_progress = 0 self._num_wizards_lock = threading.Lock() self.dark_icon = self.config.get("dark_icon", False) self.tray = None self._init_tray() self.app.new_window_signal.connect(self.start_new_window) self.set_dark_theme_if_needed() run_hook('init_qt', self) def _init_tray(self): self.tray = QSystemTrayIcon(self.tray_icon(), None) self.tray.setToolTip('Dash Electrum') self.tray.activated.connect(self.tray_activated) self.build_tray_menu() self.tray.show() def set_dark_theme_if_needed(self): use_dark_theme = self.config.get('qt_gui_color_theme', 'default') == 'dark' self.app.setStyle('Fusion') if use_dark_theme: from .dark_dash_style import dash_stylesheet self.app.setStyleSheet(dash_stylesheet) else: from .dash_style import dash_stylesheet self.app.setStyleSheet(dash_stylesheet) # Apply any necessary stylesheet patches patch_qt_stylesheet(use_dark_theme=use_dark_theme) # 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) def build_tray_menu(self): if not self.tray: return # Avoid immediate GC of old menu when window closed via its action if self.tray.contextMenu() is None: m = QMenu() self.tray.setContextMenu(m) else: m = self.tray.contextMenu() m.clear() m.addAction(_("Network"), self.show_network_dialog) for window in self.windows: name = window.wallet.basename() submenu = m.addMenu(name) submenu.addAction(_("Show/Hide"), window.show_or_hide) submenu.addAction(_("Close"), window.close) m.addAction(_("Dark/Light"), self.toggle_tray_icon) m.addSeparator() m.addAction(_("Exit Dash Electrum"), self.app.quit) def tray_icon(self): if self.dark_icon: return read_QIcon('electrum_dark_icon.png') else: return read_QIcon('electrum_light_icon.png') def toggle_tray_icon(self): if not self.tray: return self.dark_icon = not self.dark_icon self.config.set_key("dark_icon", self.dark_icon, True) self.tray.setIcon(self.tray_icon()) def tray_activated(self, reason): if reason == QSystemTrayIcon.DoubleClick: if all([w.is_hidden() for w in self.windows]): for w in self.windows: w.bring_to_top() else: for w in self.windows: w.hide() 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): window.close() window.clean_up() if self.network_dialog: self.network_dialog.close() self.network_dialog.clean_up() self.network_dialog = None self.network_updated_signal_obj = None if self.dash_net_dialog: self.dash_net_dialog.close() self.dash_net_dialog.clean_up() self.dash_net_dialog = None self.dash_net_sobj = None # Shut down the timer cleanly self.timer.stop() self.timer = None # clipboard persistence. see http://www.mail-archive.com/[email protected]/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 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() def new_window(self, path, uri=None): # Use a signal as can be called from daemon thread self.app.new_window_signal.emit(path, uri) def show_network_dialog(self): if self.network_dialog: self.network_dialog.on_update() self.network_dialog.show() self.network_dialog.raise_() return self.network_dialog = NetworkDialog( network=self.daemon.network, config=self.config, network_updated_signal_obj=self.network_updated_signal_obj) self.network_dialog.show() def show_dash_net_dialog(self): if self.dash_net_dialog: self.dash_net_dialog.on_updated() self.dash_net_dialog.show() self.dash_net_dialog.raise_() return self.dash_net_dialog = DashNetDialog(network=self.daemon.network, config=self.config, dash_net_sobj=self.dash_net_sobj) self.dash_net_dialog.show() def _create_window_for_wallet(self, wallet): w = ElectrumWindow(self, wallet) self.windows.append(w) self.build_tray_menu() w.warn_if_testnet() w.warn_if_watching_only() return w 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 self._maybe_quit_if_no_windows_open() return wrapper @count_wizards_in_progress def start_new_window(self, path, uri, *, app_is_starting=False) -> Optional[ElectrumWindow]: '''Raises the window for the wallet if it is open. Otherwise opens the wallet and creates a new window for it''' wallet = None if self.config.get('tor_auto_on', True): network = self.daemon.network if network: proxy_modifiable = self.config.is_modifiable('proxy') if not proxy_modifiable or not network.detect_tor_proxy(): warn_d = TorWarnDialog(self, path) warn_d.exec_() if warn_d.result() < 0: return try: wallet = self.daemon.load_wallet(path, None) except Exception as e: self.logger.exception('') custom_message_box(icon=QMessageBox.Warning, parent=None, title=_('Error'), text=_('Cannot load wallet') + ' (1):\n' + repr(e)) # if app is starting, still let wizard to appear if not app_is_starting: return if not wallet: try: wallet = self._start_wizard_to_select_or_create_wallet(path) except (WalletFileException, BitcoinException) as e: self.logger.exception('') custom_message_box(icon=QMessageBox.Warning, parent=None, title=_('Error'), text=_('Cannot load wallet') + ' (2):\n' + repr(e)) if not wallet: return # create or raise window try: for window in self.windows: if window.wallet.storage.path == wallet.storage.path: break else: window = self._create_window_for_wallet(wallet) except Exception as e: self.logger.exception('') custom_message_box(icon=QMessageBox.Warning, parent=None, title=_('Error'), text=_('Cannot create window for wallet') + ':\n' + repr(e)) if app_is_starting: wallet_dir = os.path.dirname(path) path = os.path.join(wallet_dir, get_new_wallet_name(wallet_dir)) self.start_new_window(path, uri) return if uri: window.pay_to_URI(uri) window.bring_to_top() window.setWindowState(window.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) window.activateWindow() return window def _start_wizard_to_select_or_create_wallet( self, path) -> Optional[Abstract_Wallet]: wizard = InstallWizard(self.config, self.app, self.plugins, gui_object=self) 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') storage, db = wizard.create_storage(path) if db.check_unfinished_multisig(): wizard.show_message(_('Saved unfinished multisig wallet')) return else: db = WalletDB(storage.read(), manual_upgrades=False) if db.upgrade_done: storage.backup_old_version() wizard.run_upgrades(storage, db) if getattr(storage, 'backup_message', None): custom_message_box(icon=QMessageBox.Warning, parent=None, title=_('Information'), text=storage.backup_message) storage.backup_message = '' if db.check_unfinished_multisig(): wizard.continue_multisig_setup(storage) storage, db = wizard.create_storage(storage.path) if db.check_unfinished_multisig(): wizard.show_message(_('Saved unfinished multisig wallet')) return except (UserCancelled, GoBack): return except WalletAlreadyOpenInMemory as e: return e.wallet finally: wizard.terminate() # return if wallet creation is not complete if storage is None or db.get_action(): return wallet = Wallet(db, storage, config=self.config) wallet.start_network(self.daemon.network) self.daemon.add_wallet(wallet) return wallet def close_window(self, window: ElectrumWindow): if window in self.windows: self.windows.remove(window) self.build_tray_menu() # save wallet path of last open window if not self.windows: self.config.save_last_wallet(window.wallet) run_hook('on_close_window', window) self.daemon.stop_wallet(window.wallet.storage.path) def init_network(self): # Show network dialog if config does not exist if self.daemon.network: if self.config.get('auto_connect') is None: wizard = InstallWizard(self.config, self.app, self.plugins, gui_object=self) wizard.init_network(self.daemon.network) wizard.terminate() def main(self): # 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()) # hook for crash reporter Exception_Hook.maybe_setup(config=self.config) # first-start network-setup try: self.init_network() except UserCancelled: return except GoBack: return except Exception as e: self.logger.exception('') return # start wizard to select/create wallet self.timer.start() path = self.config.get_wallet_path(use_gui_last_wallet=True) 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. # main loop self.logger.info("starting Qt main loop") self.app.exec_() # on some platforms the exec_ call may not return, so use _cleanup_before_exit def stop(self): self.logger.info('closing GUI') self.app.quit()
class SVApplication(QApplication): # Signals need to be on a QObject create_new_window_signal = pyqtSignal(str, object) cosigner_received_signal = pyqtSignal(object, object) labels_changed_signal = pyqtSignal(object) window_opened_signal = pyqtSignal(object) window_closed_signal = pyqtSignal(object) # Async tasks async_tasks_done = pyqtSignal() # Logging new_category = pyqtSignal(str) new_log = pyqtSignal(object) # Preferences updates fiat_ccy_changed = pyqtSignal() custom_fee_changed = pyqtSignal() op_return_enabled_changed = pyqtSignal() num_zeros_changed = pyqtSignal() base_unit_changed = pyqtSignal() fiat_history_changed = pyqtSignal() fiat_balance_changed = pyqtSignal() update_check_signal = pyqtSignal(bool, object) def __init__(self, argv): super().__init__(argv) self.windows = [] self.log_handler = SVLogHandler() self.log_window = None self.net_dialog = None self.timer = QTimer() self.exception_hook = None # A floating point number, e.g. 129.1 self.dpi = self.primaryScreen().physicalDotsPerInch() # init tray self.dark_icon = app_state.config.get("dark_icon", False) self.tray = QSystemTrayIcon(self._tray_icon(), None) self.tray.setToolTip('ElectrumSV') self.tray.activated.connect(self._tray_activated) self._build_tray_menu() self.tray.show() # FIXME set_language(app_state.config.get('language')) logs.add_handler(self.log_handler) self._start() def _start(self): QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) if hasattr(QtCore.Qt, "AA_ShareOpenGLContexts"): QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts) if hasattr(QGuiApplication, 'setDesktopFileName'): QGuiApplication.setDesktopFileName('electrum-sv.desktop') self.setWindowIcon(read_QIcon("electrum-sv.png")) self.installEventFilter(OpenFileEventFilter(self.windows)) self.create_new_window_signal.connect(self.start_new_window) self.async_tasks_done.connect(app_state.async_.run_pending_callbacks) self.num_zeros_changed.connect(partial(self._signal_all, 'on_num_zeros_changed')) self.fiat_ccy_changed.connect(partial(self._signal_all, 'on_fiat_ccy_changed')) self.base_unit_changed.connect(partial(self._signal_all, 'on_base_unit_changed')) self.fiat_history_changed.connect(partial(self._signal_all, 'on_fiat_history_changed')) self.fiat_balance_changed.connect(partial(self._signal_all, 'on_fiat_balance_changed')) self.update_check_signal.connect(partial(self._signal_all, 'on_update_check')) ColorScheme.update_from_widget(QWidget()) def _signal_all(self, method, *args): for window in self.windows: getattr(window, method)(*args) def _close(self): for window in self.windows: window.close() def close_window(self, window): app_state.daemon.stop_wallet_at_path(window.wallet.storage.path) self.windows.remove(window) self.window_closed_signal.emit(window) self._build_tray_menu() # save wallet path of last open window if not self.windows: app_state.config.save_last_wallet(window.wallet) self._last_window_closed() def _build_tray_menu(self): # Avoid immediate GC of old menu when window closed via its action if self.tray.contextMenu() is None: m = QMenu() self.tray.setContextMenu(m) else: m = self.tray.contextMenu() m.clear() for window in self.windows: submenu = m.addMenu(window.wallet.basename()) submenu.addAction(_("Show/Hide"), window.show_or_hide) submenu.addAction(_("Close"), window.close) m.addAction(_("Dark/Light"), self._toggle_tray_icon) m.addSeparator() m.addAction(_("Exit ElectrumSV"), self._close) self.tray.setContextMenu(m) def _tray_icon(self): if self.dark_icon: return read_QIcon('electrumsv_dark_icon.png') else: return read_QIcon('electrumsv_light_icon.png') def _toggle_tray_icon(self): self.dark_icon = not self.dark_icon app_state.config.set_key("dark_icon", self.dark_icon, True) self.tray.setIcon(self._tray_icon()) def _tray_activated(self, reason): if reason == QSystemTrayIcon.DoubleClick: if all([w.is_hidden() for w in self.windows]): for w in self.windows: w.bring_to_top() else: for w in self.windows: w.hide() def new_window(self, path, uri=None): # Use a signal as can be called from daemon thread self.create_new_window_signal.emit(path, uri) def show_network_dialog(self, parent): if not app_state.daemon.network: parent.show_warning(_('You are using ElectrumSV in offline mode; restart ' 'ElectrumSV if you want to get connected'), title=_('Offline')) return if self.net_dialog: self.net_dialog.on_update() self.net_dialog.show() self.net_dialog.raise_() return self.net_dialog = NetworkDialog(app_state.daemon.network, app_state.config) self.net_dialog.show() def show_log_viewer(self): if self.log_window is None: self.log_window = SVLogWindow(None, self.log_handler) self.log_window.show() def _last_window_closed(self): for dialog in (self.net_dialog, self.log_window): if dialog: dialog.accept() def _maybe_choose_server(self): # Show network dialog if config does not exist if app_state.daemon.network and app_state.config.get('auto_connect') is None: try: wizard = InstallWizard(None) wizard.init_network(app_state.daemon.network) wizard.terminate() except Exception as e: if not isinstance(e, (UserCancelled, GoBack)): logger.exception("") self.quit() def on_label_change(self, wallet: Abstract_Wallet, name: str, text: str) -> None: self.label_sync.set_label(wallet, name, text) def _create_window_for_wallet(self, wallet: Abstract_Wallet): if wallet.is_hardware_wallet(): dialogs.show_named('hardware-wallet-quality') w = ElectrumWindow(wallet) self.windows.append(w) self._build_tray_menu() self.window_opened_signal.emit(w) return w def get_wallet_window(self, path: str) -> Optional[ElectrumWindow]: for w in self.windows: if w.wallet.storage.path == path: return w def start_new_window(self, path, uri, is_startup=False): '''Raises the window for the wallet if it is open. Otherwise opens the wallet and creates a new window for it.''' for w in self.windows: if w.wallet.storage.path == path: w.bring_to_top() break else: try: wallet = app_state.daemon.load_wallet(path, None) if not wallet: wizard = InstallWizard(None) try: if wizard.select_storage(path, is_startup=is_startup): wallet = wizard.run_and_get_wallet() except UserQuit: pass except UserCancelled: pass except GoBack as e: logger.error('[start_new_window] Exception caught (GoBack) %s', e) finally: wizard.terminate() if not wallet: return app_state.daemon.start_wallet(wallet) except Exception as e: logger.exception("") error_str = str(e) if '2fa' in error_str: error_str = _('2FA wallets are not supported') msg = '\n'.join((_('Cannot load wallet "{}"').format(path), error_str)) MessageBox.show_error(msg) return w = self._create_window_for_wallet(wallet) if uri: w.pay_to_URI(uri) w.bring_to_top() w.setWindowState(w.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) # this will activate the window w.activateWindow() return w def update_check(self) -> None: if (not app_state.config.get('check_updates', True) or app_state.config.get("offline", False)): return def f(): import requests try: response = requests.request( 'GET', "https://electrumsv.io/release.json", headers={'User-Agent' : 'ElectrumSV'}, timeout=10) result = response.json() self._on_update_check(True, result) except Exception: self._on_update_check(False, sys.exc_info()) t = threading.Thread(target=f) t.setDaemon(True) t.start() def _on_update_check(self, success: bool, result: dict) -> None: if success: when_checked = datetime.datetime.now().astimezone().isoformat() app_state.config.set_key('last_update_check', result) app_state.config.set_key('last_update_check_time', when_checked, True) self.update_check_signal.emit(success, result) def initial_dialogs(self) -> None: '''Suppressible dialogs that are shown when first opening the app.''' dialogs.show_named('welcome-ESV-1.3.0a1') # This needs to be reworked or removed, as non-advanced users aren't sure whether # it is safe, and likely many people aren't quite sure if it should be done. # old_items = [] # headers_path = os.path.join(app_state.config.path, 'blockchain_headers') # if os.path.exists(headers_path): # old_items.append((_('the file "blockchain_headers"'), os.remove, headers_path)) # forks_dir = os.path.join(app_state.config.path, 'forks') # if os.path.exists(forks_dir): # old_items.append((_('the directory "forks/"'), shutil.rmtree, forks_dir)) # if old_items: # main_text = _('Delete the following obsolete items in <br>{}?' # .format(app_state.config.path)) # info_text = '<ul>{}</ul>'.format(''.join('<li>{}</li>'.format(text) # for text, *rest in old_items)) # if dialogs.show_named('delete-obsolete-headers', main_text=main_text, # info_text=info_text): # try: # for _text, rm_func, *args in old_items: # rm_func(*args) # except OSError as e: # logger.exception('deleting obsolete files') # dialogs.error_dialog(_('Error deleting files:'), info_text=str(e)) def event_loop_started(self) -> None: self.cosigner_pool = CosignerPool() self.label_sync = LabelSync() if app_state.config.get("show_crash_reporter", default=True): self.exception_hook = Exception_Hook(self) self.timer.start() signal.signal(signal.SIGINT, lambda *args: self.quit()) self.initial_dialogs() self._maybe_choose_server() app_state.config.open_last_wallet() path = app_state.config.get_wallet_path() if not self.start_new_window(path, app_state.config.get('url'), is_startup=True): self.quit() def run_app(self) -> None: when_started = datetime.datetime.now().astimezone().isoformat() app_state.config.set_key('previous_start_time', app_state.config.get("start_time")) app_state.config.set_key('start_time', when_started, True) self.update_check() threading.current_thread().setName('GUI') self.timer.setSingleShot(False) self.timer.setInterval(500) # msec self.timer.timeout.connect(app_state.device_manager.timeout_clients) QTimer.singleShot(0, self.event_loop_started) self.exec_() logs.remove_handler(self.log_handler) # Shut down the timer cleanly self.timer.stop() # clipboard persistence # see http://www.mail-archive.com/[email protected]/msg17328.html event = QtCore.QEvent(QtCore.QEvent.Clipboard) self.sendEvent(self.clipboard(), event) self.tray.hide() def run_coro(self, coro, *args, on_done=None): '''Run a coroutine. on_done, if given, is passed the future containing the reuslt or exception, and is guaranteed to be called in the context of the GUI thread. ''' def task_done(future): self.async_tasks_done.emit() future = app_state.async_.spawn(coro, *args, on_done=on_done) future.add_done_callback(task_done) return future def run_in_thread(self, func, *args, on_done=None): '''Run func(*args) in a thread. on_done, if given, is passed the future containing the reuslt or exception, and is guaranteed to be called in the context of the GUI thread. ''' return self.run_coro(run_in_thread, func, *args, on_done=on_done)
class ElectrumGui(Logger): @profiler def __init__(self, config, daemon, plugins): set_language(config.get('language', get_default_language())) Logger.__init__(self) # 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)]) QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) if hasattr(QtCore.Qt, "AA_ShareOpenGLContexts"): QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts) if hasattr(QGuiApplication, 'setDesktopFileName'): QGuiApplication.setDesktopFileName('electrum.desktop') self.gui_thread = threading.current_thread() self.config = config self.daemon = daemon self.plugins = plugins self.windows = [] self.efilter = OpenFileEventFilter(self.windows) self.app = QElectrumApplication(sys.argv) self.app.installEventFilter(self.efilter) self.app.setWindowIcon(read_QIcon("electrum.png")) # timer self.timer = QTimer(self.app) self.timer.setSingleShot(False) self.timer.setInterval(500) # msec self.network_dialog = None self.lightning_dialog = None self.network_updated_signal_obj = QNetworkUpdatedSignalObject() self._num_wizards_in_progress = 0 self._num_wizards_lock = threading.Lock() # init tray self.dark_icon = self.config.get("dark_icon", False) 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() self.app.new_window_signal.connect(self.start_new_window) self.set_dark_theme_if_needed() run_hook('init_qt', self) def set_dark_theme_if_needed(self): use_dark_theme = self.config.get('qt_gui_color_theme', 'default') == 'dark' if use_dark_theme: try: import qdarkstyle self.app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5()) except BaseException as e: use_dark_theme = False self.logger.warning(f'Error setting dark theme: {repr(e)}') # Apply any necessary stylesheet patches patch_qt_stylesheet(use_dark_theme=use_dark_theme) # 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) def build_tray_menu(self): # Avoid immediate GC of old menu when window closed via its action if self.tray.contextMenu() is None: m = QMenu() self.tray.setContextMenu(m) else: m = self.tray.contextMenu() m.clear() if self.config.get('lightning'): m.addAction(_("Lightning"), self.show_lightning_dialog) for window in self.windows: name = window.wallet.basename() submenu = m.addMenu(name) submenu.addAction(_("Show/Hide"), window.show_or_hide) submenu.addAction(_("Close"), window.close) m.addAction(_("Dark/Light"), self.toggle_tray_icon) m.addSeparator() m.addAction(_("Exit Electrum"), self.close) def tray_icon(self): if self.dark_icon: return read_QIcon('electrum_dark_icon.png') else: return read_QIcon('electrum_light_icon.png') def toggle_tray_icon(self): self.dark_icon = not self.dark_icon self.config.set_key("dark_icon", self.dark_icon, True) self.tray.setIcon(self.tray_icon()) def tray_activated(self, reason): if reason == QSystemTrayIcon.DoubleClick: if all([w.is_hidden() for w in self.windows]): for w in self.windows: w.bring_to_top() else: for w in self.windows: w.hide() def close(self): for window in self.windows: window.close() if self.network_dialog: self.network_dialog.close() if self.lightning_dialog: self.lightning_dialog.close() def new_window(self, path, uri=None): # Use a signal as can be called from daemon thread self.app.new_window_signal.emit(path, uri) def show_lightning_dialog(self): if not self.lightning_dialog: self.lightning_dialog = LightningDialog(self) self.lightning_dialog.bring_to_top() def show_network_dialog(self, parent): if not self.daemon.network: parent.show_warning(_('You are using Electrum in offline mode; restart Electrum if you want to get connected'), title=_('Offline')) return if self.network_dialog: self.network_dialog.on_update() self.network_dialog.show() self.network_dialog.raise_() return self.network_dialog = NetworkDialog(self.daemon.network, self.config, self.network_updated_signal_obj) self.network_dialog.show() def _create_window_for_wallet(self, wallet): w = ElectrumWindow(self, wallet) self.windows.append(w) self.build_tray_menu() # FIXME: Remove in favour of the load_wallet hook run_hook('on_new_window', w) w.warn_if_testnet() w.warn_if_watching_only() return w 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 return wrapper @count_wizards_in_progress def start_new_window(self, path, uri, *, app_is_starting=False): '''Raises the window for the wallet if it is open. Otherwise opens the wallet and creates a new window for it''' wallet = None try: wallet = self.daemon.load_wallet(path, None) except BaseException as e: self.logger.exception('') custom_message_box(icon=QMessageBox.Warning, parent=None, title=_('Error'), text=_('Cannot load wallet') + ' (1):\n' + repr(e)) # if app is starting, still let wizard to appear if not app_is_starting: return if not wallet: try: wallet = self._start_wizard_to_select_or_create_wallet(path) except (WalletFileException, BitcoinException) as e: self.logger.exception('') custom_message_box(icon=QMessageBox.Warning, parent=None, title=_('Error'), text=_('Cannot load wallet') + ' (2):\n' + repr(e)) if not wallet: return # create or raise window try: for window in self.windows: if window.wallet.storage.path == wallet.storage.path: break else: window = self._create_window_for_wallet(wallet) except BaseException as e: self.logger.exception('') custom_message_box(icon=QMessageBox.Warning, parent=None, title=_('Error'), text=_('Cannot create window for wallet') + ':\n' + repr(e)) if app_is_starting: wallet_dir = os.path.dirname(path) path = os.path.join(wallet_dir, get_new_wallet_name(wallet_dir)) self.start_new_window(path, uri) return if uri: window.pay_to_URI(uri) window.bring_to_top() window.setWindowState(window.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) window.activateWindow() return window def _start_wizard_to_select_or_create_wallet(self, path) -> Optional[Abstract_Wallet]: wizard = InstallWizard(self.config, self.app, self.plugins) 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') storage = wizard.create_storage(path) else: wizard.run_upgrades(storage) except (UserCancelled, GoBack): return except WalletAlreadyOpenInMemory as e: return e.wallet finally: wizard.terminate() # return if wallet creation is not complete if storage is None or storage.get_action(): return wallet = Wallet(storage) wallet.start_network(self.daemon.network) self.daemon.add_wallet(wallet) return wallet def close_window(self, window: ElectrumWindow): if window in self.windows: self.windows.remove(window) self.build_tray_menu() # save wallet path of last open window if not self.windows: self.config.save_last_wallet(window.wallet) run_hook('on_close_window', window) self.daemon.stop_wallet(window.wallet.storage.path) def init_network(self): # Show network dialog if config does not exist if self.daemon.network: if self.config.get('auto_connect') is None: wizard = InstallWizard(self.config, self.app, self.plugins) wizard.init_network(self.daemon.network) wizard.terminate() def main(self): try: self.init_network() except UserCancelled: return except GoBack: return except BaseException as e: self.logger.exception('') return self.timer.start() self.config.open_last_wallet() path = self.config.get_wallet_path() if not self.start_new_window(path, self.config.get('url'), app_is_starting=True): return signal.signal(signal.SIGINT, lambda *args: self.app.quit()) def quit_after_last_window(): # 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 if self.config.get('persist_daemon'): return self.app.quit() self.app.setQuitOnLastWindowClosed(False) # so _we_ can decide whether to quit self.app.lastWindowClosed.connect(quit_after_last_window) def clean_up(): # Shut down the timer cleanly self.timer.stop() # clipboard persistence. see http://www.mail-archive.com/[email protected]/msg17328.html event = QtCore.QEvent(QtCore.QEvent.Clipboard) self.app.sendEvent(self.app.clipboard(), event) self.tray.hide() self.app.aboutToQuit.connect(clean_up) # main loop self.app.exec_() # on some platforms the exec_ call may not return, so use clean_up() def stop(self): self.logger.info('closing GUI') self.app.quit()
class MainWindow(QMainWindow): def __init__(self): super(MainWindow, self).__init__() self.setupTrayicon() self.setupVariables() self.setupUi() self.setupConnections() self.show() def setupVariables(self): settings = QSettings() self.workEndTime = QTime( int(settings.value(workHoursKey, 0)), int(settings.value(workMinutesKey, 25)), int(settings.value(workSecondsKey, 0)), ) self.restEndTime = QTime( int(settings.value(restHoursKey, 0)), int(settings.value(restMinutesKey, 5)), int(settings.value(restSecondsKey, 0)), ) self.timeFormat = "hh:mm:ss" self.time = QTime(0, 0, 0, 0) self.workTime = QTime(0, 0, 0, 0) self.restTime = QTime(0, 0, 0, 0) self.totalTime = QTime(0, 0, 0, 0) self.currentMode = Mode.work self.maxRepetitions = -1 self.currentRepetitions = 0 def setupConnections(self): """ Create button connections """ self.startButton.clicked.connect(self.startTimer) self.startButton.clicked.connect( lambda: self.startButton.setDisabled(True)) self.startButton.clicked.connect( lambda: self.pauseButton.setDisabled(False)) self.startButton.clicked.connect( lambda: self.resetButton.setDisabled(False)) self.pauseButton.clicked.connect(self.pauseTimer) self.pauseButton.clicked.connect( lambda: self.startButton.setDisabled(False)) self.pauseButton.clicked.connect( lambda: self.pauseButton.setDisabled(True)) self.pauseButton.clicked.connect( lambda: self.resetButton.setDisabled(False)) self.pauseButton.clicked.connect( lambda: self.startButton.setText("continue")) self.resetButton.clicked.connect(self.resetTimer) self.resetButton.clicked.connect( lambda: self.startButton.setDisabled(False)) self.resetButton.clicked.connect( lambda: self.pauseButton.setDisabled(True)) self.resetButton.clicked.connect( lambda: self.resetButton.setDisabled(True)) self.resetButton.clicked.connect( lambda: self.startButton.setText("start")) self.acceptTaskButton.pressed.connect(self.insertTask) self.deleteTaskButton.pressed.connect(self.deleteTask) """ Create spinbox connections """ self.workHoursSpinBox.valueChanged.connect(self.updateWorkEndTime) self.workMinutesSpinBox.valueChanged.connect(self.updateWorkEndTime) self.workSecondsSpinBox.valueChanged.connect(self.updateWorkEndTime) self.restHoursSpinBox.valueChanged.connect(self.updateRestEndTime) self.restMinutesSpinBox.valueChanged.connect(self.updateRestEndTime) self.restSecondsSpinBox.valueChanged.connect(self.updateRestEndTime) self.repetitionsSpinBox.valueChanged.connect(self.updateMaxRepetitions) """ Create combobox connections """ self.modeComboBox.currentTextChanged.connect(self.updateCurrentMode) """ Create tablewidget connections """ self.tasksTableWidget.cellDoubleClicked.connect( self.markTaskAsFinished) def setupUi(self): self.size_policy = sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) """ Create tabwidget """ self.tabWidget = QTabWidget() """ Create tab widgets """ timerWidget = self.setupTimerTab() tasksWidget = self.setupTasksTab() statisticsWidget = self.setupStatisticsTab() """ add tab widgets to tabwidget""" self.timerTab = self.tabWidget.addTab(timerWidget, makeIcon("timer"), "Timer") self.tasksTab = self.tabWidget.addTab(tasksWidget, makeIcon("tasks"), "Tasks") self.statisticsTab = self.tabWidget.addTab(statisticsWidget, makeIcon("statistics"), "Statistics") """ Set mainwindows central widget """ self.setCentralWidget(self.tabWidget) def setupTimerTab(self): settings = QSettings() self.timerContainer = QWidget(self) self.timerContainerLayout = QVBoxLayout(self.timerContainer) self.timerContainer.setLayout(self.timerContainerLayout) """ Create work groupbox""" self.workGroupBox = QGroupBox("Work") self.workGroupBoxLayout = QHBoxLayout(self.workGroupBox) self.workGroupBox.setLayout(self.workGroupBoxLayout) self.workHoursSpinBox = QSpinBox( minimum=0, maximum=24, value=int(settings.value(workHoursKey, 0)), suffix="h", sizePolicy=self.size_policy, ) self.workMinutesSpinBox = QSpinBox( minimum=0, maximum=60, value=int(settings.value(workMinutesKey, 25)), suffix="m", sizePolicy=self.size_policy, ) self.workSecondsSpinBox = QSpinBox( minimum=0, maximum=60, value=int(settings.value(workSecondsKey, 0)), suffix="s", sizePolicy=self.size_policy, ) """ Create rest groupbox""" self.restGroupBox = QGroupBox("Rest") self.restGroupBoxLayout = QHBoxLayout(self.restGroupBox) self.restGroupBox.setLayout(self.restGroupBoxLayout) self.restHoursSpinBox = QSpinBox( minimum=0, maximum=24, value=int(settings.value(restHoursKey, 0)), suffix="h", sizePolicy=self.size_policy, ) self.restMinutesSpinBox = QSpinBox( minimum=0, maximum=60, value=int(settings.value(restMinutesKey, 5)), suffix="m", sizePolicy=self.size_policy, ) self.restSecondsSpinBox = QSpinBox( minimum=0, maximum=60, value=int(settings.value(restSecondsKey, 0)), suffix="s", sizePolicy=self.size_policy, ) self.restGroupBoxLayout.addWidget(self.restHoursSpinBox) self.restGroupBoxLayout.addWidget(self.restMinutesSpinBox) self.restGroupBoxLayout.addWidget(self.restSecondsSpinBox) """ Create other groupbox""" self.otherGroupBox = QGroupBox("Other") self.otherGroupBoxLayout = QHBoxLayout(self.otherGroupBox) self.otherGroupBox.setLayout(self.otherGroupBoxLayout) self.repetitionsLabel = QLabel("Repetitions") self.repetitionsSpinBox = QSpinBox( minimum=0, maximum=10000, value=0, sizePolicy=self.size_policy, specialValueText="∞", ) self.modeLabel = QLabel("Mode") self.modeComboBox = QComboBox(sizePolicy=self.size_policy) self.modeComboBox.addItems(["work", "rest"]) self.otherGroupBoxLayout.addWidget(self.repetitionsLabel) self.otherGroupBoxLayout.addWidget(self.repetitionsSpinBox) self.otherGroupBoxLayout.addWidget(self.modeLabel) self.otherGroupBoxLayout.addWidget(self.modeComboBox) """ Create timer groupbox""" self.lcdDisplayGroupBox = QGroupBox("Time") self.lcdDisplayGroupBoxLayout = QHBoxLayout(self.lcdDisplayGroupBox) self.lcdDisplayGroupBox.setLayout(self.lcdDisplayGroupBoxLayout) self.timeDisplay = QLCDNumber(8, sizePolicy=self.size_policy) self.timeDisplay.setFixedHeight(100) self.timeDisplay.display("00:00:00") self.lcdDisplayGroupBoxLayout.addWidget(self.timeDisplay) """ Create pause, start and reset buttons""" self.buttonContainer = QWidget() self.buttonContainerLayout = QHBoxLayout(self.buttonContainer) self.buttonContainer.setLayout(self.buttonContainerLayout) self.startButton = self.makeButton("start", disabled=False) self.resetButton = self.makeButton("reset") self.pauseButton = self.makeButton("pause") """ Add widgets to container """ self.workGroupBoxLayout.addWidget(self.workHoursSpinBox) self.workGroupBoxLayout.addWidget(self.workMinutesSpinBox) self.workGroupBoxLayout.addWidget(self.workSecondsSpinBox) self.timerContainerLayout.addWidget(self.workGroupBox) self.timerContainerLayout.addWidget(self.restGroupBox) self.timerContainerLayout.addWidget(self.otherGroupBox) self.timerContainerLayout.addWidget(self.lcdDisplayGroupBox) self.buttonContainerLayout.addWidget(self.pauseButton) self.buttonContainerLayout.addWidget(self.startButton) self.buttonContainerLayout.addWidget(self.resetButton) self.timerContainerLayout.addWidget(self.buttonContainer) return self.timerContainer def setupTasksTab(self): settings = QSettings() """ Create vertical tasks container """ self.tasksWidget = QWidget(self.tabWidget) self.tasksWidgetLayout = QVBoxLayout(self.tasksWidget) self.tasksWidget.setLayout(self.tasksWidgetLayout) """ Create horizontal input container """ self.inputContainer = QWidget() self.inputContainer.setFixedHeight(50) self.inputContainerLayout = QHBoxLayout(self.inputContainer) self.inputContainerLayout.setContentsMargins(0, 0, 0, 0) self.inputContainer.setLayout(self.inputContainerLayout) """ Create text edit """ self.taskTextEdit = QTextEdit( placeholderText="Describe your task briefly.", undoRedoEnabled=True) """ Create vertical buttons container """ self.inputButtonContainer = QWidget() self.inputButtonContainerLayout = QVBoxLayout( self.inputButtonContainer) self.inputButtonContainerLayout.setContentsMargins(0, 0, 0, 0) self.inputButtonContainer.setLayout(self.inputButtonContainerLayout) """ Create buttons """ self.acceptTaskButton = QToolButton(icon=makeIcon("check")) self.deleteTaskButton = QToolButton(icon=makeIcon("trash")) """ Create tasks tablewidget """ self.tasksTableWidget = QTableWidget(0, 1) self.tasksTableWidget.setHorizontalHeaderLabels(["Tasks"]) self.tasksTableWidget.horizontalHeader().setStretchLastSection(True) self.tasksTableWidget.verticalHeader().setVisible(False) self.tasksTableWidget.setWordWrap(True) self.tasksTableWidget.setTextElideMode(Qt.ElideNone) self.tasksTableWidget.setEditTriggers(QAbstractItemView.NoEditTriggers) self.tasksTableWidget.setSelectionMode( QAbstractItemView.SingleSelection) self.insertTasks(*settings.value(tasksKey, [])) """ Add widgets to container widgets """ self.inputButtonContainerLayout.addWidget(self.acceptTaskButton) self.inputButtonContainerLayout.addWidget(self.deleteTaskButton) self.inputContainerLayout.addWidget(self.taskTextEdit) self.inputContainerLayout.addWidget(self.inputButtonContainer) self.tasksWidgetLayout.addWidget(self.inputContainer) self.tasksWidgetLayout.addWidget(self.tasksTableWidget) return self.tasksWidget def setupStatisticsTab(self): """ Create statistics container """ self.statisticsContainer = QWidget() self.statisticsContainerLayout = QVBoxLayout(self.statisticsContainer) self.statisticsContainer.setLayout(self.statisticsContainerLayout) """ Create work time groupbox """ self.statisticsWorkTimeGroupBox = QGroupBox("Work Time") self.statisticsWorkTimeGroupBoxLayout = QHBoxLayout() self.statisticsWorkTimeGroupBox.setLayout( self.statisticsWorkTimeGroupBoxLayout) self.statisticsWorkTimeDisplay = QLCDNumber(8) self.statisticsWorkTimeDisplay.display("00:00:00") self.statisticsWorkTimeGroupBoxLayout.addWidget( self.statisticsWorkTimeDisplay) """ Create rest time groupbox """ self.statisticsRestTimeGroupBox = QGroupBox("Rest Time") self.statisticsRestTimeGroupBoxLayout = QHBoxLayout() self.statisticsRestTimeGroupBox.setLayout( self.statisticsRestTimeGroupBoxLayout) self.statisticsRestTimeDisplay = QLCDNumber(8) self.statisticsRestTimeDisplay.display("00:00:00") self.statisticsRestTimeGroupBoxLayout.addWidget( self.statisticsRestTimeDisplay) """ Create total time groupbox """ self.statisticsTotalTimeGroupBox = QGroupBox("Total Time") self.statisticsTotalTimeGroupBoxLayout = QHBoxLayout() self.statisticsTotalTimeGroupBox.setLayout( self.statisticsTotalTimeGroupBoxLayout) self.statisticsTotalTimeDisplay = QLCDNumber(8) self.statisticsTotalTimeDisplay.display("00:00:00") self.statisticsTotalTimeGroupBoxLayout.addWidget( self.statisticsTotalTimeDisplay) """ Add widgets to container """ self.statisticsContainerLayout.addWidget( self.statisticsTotalTimeGroupBox) self.statisticsContainerLayout.addWidget( self.statisticsWorkTimeGroupBox) self.statisticsContainerLayout.addWidget( self.statisticsRestTimeGroupBox) return self.statisticsContainer def setupTrayicon(self): self.trayIcon = QSystemTrayIcon(makeIcon("tomato")) self.trayIcon.setContextMenu(QMenu()) self.quitAction = self.trayIcon.contextMenu().addAction( makeIcon("exit"), "Quit", self.exit) self.quitAction.triggered.connect(self.exit) self.trayIcon.activated.connect(self.onActivate) self.trayIcon.show() def leaveEvent(self, event): super(MainWindow, self).leaveEvent(event) self.tasksTableWidget.clearSelection() def closeEvent(self, event): super(MainWindow, self).closeEvent(event) settings = QSettings() settings.setValue(workHoursKey, self.workHoursSpinBox.value()) settings.setValue( workMinutesKey, self.workMinutesSpinBox.value(), ) settings.setValue( workSecondsKey, self.workSecondsSpinBox.value(), ) settings.setValue(restHoursKey, self.restHoursSpinBox.value()) settings.setValue( restMinutesKey, self.restMinutesSpinBox.value(), ) settings.setValue( restSecondsKey, self.restSecondsSpinBox.value(), ) tasks = [] for i in range(self.tasksTableWidget.rowCount()): item = self.tasksTableWidget.item(i, 0) if not item.font().strikeOut(): tasks.append(item.text()) settings.setValue(tasksKey, tasks) def startTimer(self): try: if not self.timer.isActive(): self.createTimer() except: self.createTimer() def createTimer(self): self.timer = QTimer() self.timer.timeout.connect(self.updateTime) self.timer.timeout.connect(self.maybeChangeMode) self.timer.setInterval(1000) self.timer.setSingleShot(False) self.timer.start() def pauseTimer(self): try: self.timer.stop() self.timer.disconnect() except: pass def resetTimer(self): try: self.pauseTimer() self.time = QTime(0, 0, 0, 0) self.displayTime() except: pass def maybeStartTimer(self): if self.currentRepetitions != self.maxRepetitions: self.startTimer() started = True else: self.currentRepetitions = 0 started = False return started def updateWorkEndTime(self): self.workEndTime = QTime( self.workHoursSpinBox.value(), self.workMinutesSpinBox.value(), self.workSecondsSpinBox.value(), ) def updateRestEndTime(self): self.restEndTime = QTime( self.restHoursSpinBox.value(), self.restMinutesSpinBox.value(), self.restSecondsSpinBox.value(), ) def updateCurrentMode(self, mode: str): self.currentMode = Mode.work if mode == "work" else Mode.rest def updateTime(self): self.time = self.time.addSecs(1) self.totalTime = self.totalTime.addSecs(1) if self.modeComboBox.currentText() == "work": self.workTime = self.workTime.addSecs(1) else: self.restTime = self.restTime.addSecs(1) self.displayTime() def updateMaxRepetitions(self, value): if value == 0: self.currentRepetitions = 0 self.maxRepetitions = -1 else: self.maxRepetitions = 2 * value def maybeChangeMode(self): if self.currentMode is Mode.work and self.time >= self.workEndTime: self.resetTimer() self.modeComboBox.setCurrentIndex(1) self.incrementCurrentRepetitions() started = self.maybeStartTimer() self.showWindowMessage( Status.workFinished if started else Status.repetitionsReached) elif self.currentMode is Mode.rest and self.time >= self.restEndTime: self.resetTimer() self.modeComboBox.setCurrentIndex(0) self.incrementCurrentRepetitions() started = self.maybeStartTimer() self.showWindowMessage( Status.restFinished if started else Status.repetitionsReached) def incrementCurrentRepetitions(self): if self.maxRepetitions > 0: self.currentRepetitions += 1 def insertTask(self): task = self.taskTextEdit.toPlainText() self.insertTasks(task) def insertTasks(self, *tasks): for task in tasks: if task: rowCount = self.tasksTableWidget.rowCount() self.tasksTableWidget.setRowCount(rowCount + 1) self.tasksTableWidget.setItem(rowCount, 0, QTableWidgetItem(task)) self.tasksTableWidget.resizeRowsToContents() self.taskTextEdit.clear() def deleteTask(self): selectedIndexes = self.tasksTableWidget.selectedIndexes() if selectedIndexes: self.tasksTableWidget.removeRow(selectedIndexes[0].row()) def markTaskAsFinished(self, row, col): item = self.tasksTableWidget.item(row, col) font = self.tasksTableWidget.item(row, col).font() font.setStrikeOut(False if item.font().strikeOut() else True) item.setFont(font) def displayTime(self): self.timeDisplay.display(self.time.toString(self.timeFormat)) self.statisticsRestTimeDisplay.display( self.restTime.toString(self.timeFormat)) self.statisticsWorkTimeDisplay.display( self.workTime.toString(self.timeFormat)) self.statisticsTotalTimeDisplay.display( self.totalTime.toString(self.timeFormat)) def showWindowMessage(self, status): if status is Status.workFinished: self.trayIcon.showMessage("Break", choice(work_finished_phrases), makeIcon("tomato")) elif status is Status.restFinished: self.trayIcon.showMessage("Work", choice(rest_finished_phrases), makeIcon("tomato")) else: self.trayIcon.showMessage("Finished", choice(pomodoro_finished_phrases), makeIcon("tomato")) self.resetButton.click() def makeButton(self, text, iconName=None, disabled=True): button = QPushButton(text, sizePolicy=self.size_policy) if iconName: button.setIcon(makeIcon(iconName)) button.setDisabled(disabled) return button def exit(self): self.close() app = QApplication.instance() if app: app.quit() def onActivate(self, reason): if reason == QSystemTrayIcon.Trigger: self.show()
class LoginDialog(QDialog): def __init__(self, steam, theme): super().__init__() self.steam = steam self.theme = theme self.trayicon = None self.wait_task = None self.process = None self._exit = False self._login = None self.user_widgets = [] self.setLayout(QVBoxLayout()) self.setWindowTitle("Steam Acolyte") self.setWindowIcon(theme.window_icon) self.setStyleSheet(theme.window_style) steam.command_received.connect(lambda *_: self.activateWindow()) self.update_userlist() def update_userlist(self): """Update the user list widget from the config file.""" self.clear_layout() users = sorted(self.steam.users(), key=lambda u: (u.persona_name.lower(), u.account_name.lower())) users.append(SteamUser('', '', '', '')) for user in users: self.layout().addWidget(UserWidget(self, user)) def clear_layout(self): """Remove all users from the user list widget.""" # The safest way I found to clear a QLayout is to reparent it to a # temporary widget. This also recursively reparents, hides and later # destroys any child widgets. layout = self.layout() if layout is not None: dump = QWidget() dump.setLayout(layout) dump.deleteLater() self.setLayout(QVBoxLayout()) @trace.method def wait_for_lock(self): """Start waiting for the steam instance lock asynchronously, and show/activate the window when we acquire the lock.""" if self._exit: self.close() return self.wait_task = AsyncTask(self.steam.wait_for_lock) self.wait_task.finished.connect(self._on_locked) self.wait_task.start() @trace.method def _on_locked(self): """Executed when steam instance lock is acquired. Executes any queued login command, or activates the user list widget if no command was queued.""" if self._exit: self.close() return self.stopAction.setEnabled(False) self.wait_task = None self.steam.store_login_cookie() self.update_userlist() if self._login: self.run_steam(self._login) self._login = None return self.show() @trace.method def show_trayicon(self): """Create and show the tray icon.""" # The conversion to QPixmap and back to QIcon is needed to prevent a # bug that leads to the icon not being displayed in plasma. See: # - https://github.com/coldfix/steam-acolyte/issues/8 # - https://bugreports.qt.io/browse/QTBUG-53550 icon = QIcon(self.theme.window_icon.pixmap(64)) self.trayicon = QSystemTrayIcon(icon) self.trayicon.setVisible(True) self.trayicon.setToolTip("acolyte - lightweight steam account manager") self.trayicon.activated.connect(self.trayicon_clicked) self.trayicon.setContextMenu(self.createMenu()) @trace.method def hide_trayicon(self): """Hide and destroy the tray icon.""" if self.trayicon is not None: self.trayicon.setVisible(False) self.trayicon.deleteLater() self.trayicon = None @trace.method def trayicon_clicked(self, reason): """Activate window when tray icon is left-clicked.""" if reason == QSystemTrayIcon.Trigger: if self.steam.has_steam_lock(): self.activateWindow() def createMenu(self): """Compose tray menu.""" style = self.style() stop = self.stopAction = QAction('&Exit Steam', self) stop.setToolTip('Signal steam to exit.') stop.setIcon(style.standardIcon(QStyle.SP_MediaStop)) stop.triggered.connect(self.exit_steam) stop.setEnabled(False) exit = QAction('&Quit', self) exit.setToolTip('Exit acolyte.') exit.setIcon(style.standardIcon(QStyle.SP_DialogCloseButton)) exit.triggered.connect(self._on_exit, QueuedConnection) self.newUserAction = make_user_action(self, SteamUser('', '', '', '')) self.userActions = [] menu = QMenu() menu.addSection('Login') menu.addAction(self.newUserAction) menu.addSeparator() menu.addAction(stop) menu.addAction(exit) menu.aboutToShow.connect(self.update_menu, QueuedConnection) return menu def update_menu(self): """Update menu just before showing: populate with current user list and set position from tray icon.""" self.populate_menu() self.position_menu() def populate_menu(self): """Update user list menuitems in tray menu.""" menu = self.trayicon.contextMenu() for action in self.userActions: menu.removeAction(action) users = sorted(self.steam.users(), key=lambda u: (u.persona_name.lower(), u.account_name.lower())) self.userActions = [make_user_action(self, user) for user in users] menu.insertActions(self.newUserAction, self.userActions) def position_menu(self): """Set menu position from tray icon.""" menu = self.trayicon.contextMenu() desktop = QApplication.desktop() screen = QApplication.screens()[desktop.screenNumber(menu)] screen_geom = screen.availableGeometry() menu_size = menu.sizeHint() icon_geom = self.trayicon.geometry() if icon_geom.left() + menu_size.width() <= screen_geom.right(): left = icon_geom.left() elif icon_geom.right() - menu_size.width() >= screen_geom.left(): left = icon_geom.right() - menu_size.width() else: return if icon_geom.bottom() + menu_size.height() <= screen_geom.bottom(): top = icon_geom.bottom() elif icon_geom.top() - menu_size.height() >= screen_geom.top(): top = icon_geom.top() - menu_size.height() else: return menu.move(left, top) @trace.method def exit_steam(self): """Send shutdown command to steam.""" self.stopAction.setEnabled(False) self.steam.stop() @trace.method def _on_exit(self): """Exit acolyte.""" # We can't quit if steam is still running because QProcess would # terminate the child with us. In this case, we hide the trayicon and # set an exit flag to remind us about to exit as soon as steam is # finished. self.hide_trayicon() if self.steam.has_steam_lock(): self.close() else: self._exit = True self.steam.unlock() self.steam.release_acolyte_instance_lock() @trace.method def login(self, username): """ Exit steam if open, and login the user with the given username. """ if self.steam.has_steam_lock(): self.run_steam(username) else: self._login = username self.exit_steam() @trace.method def run_steam(self, username): """Run steam as the given user.""" # Close and recreate after steam is finished. This serves two purposes: # 1. update user list and widget state # 2. fix ":hover" selector not working on linux after hide+show self.hide() self.steam.switch_user(username) self.steam.unlock() self.stopAction.setEnabled(True) self.process = self.steam.run() self.process.finished.connect(self.wait_for_lock) @trace.method def show_waiting_message(self): """If we are in the background, show waiting message as balloon.""" if self.trayicon is not None: self.trayicon.showMessage("steam-acolyte", "The damned stand ready.")
class ElectrumGui(BaseElectrumGui, Logger): network_dialog: Optional['NetworkDialog'] lightning_dialog: Optional['LightningDialog'] watchtower_dialog: Optional['WatchtowerDialog'] @profiler def __init__(self, *, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins'): set_language(config.get('language', get_default_language())) BaseElectrumGui.__init__(self, config=config, daemon=daemon, plugins=plugins) Logger.__init__(self) self.logger.info( f"Qt GUI starting up... Qt={QtCore.QT_VERSION_STR}, PyQt={QtCore.PYQT_VERSION_STR}" ) # 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)]) QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) if hasattr(QtCore.Qt, "AA_ShareOpenGLContexts"): QtCore.QCoreApplication.setAttribute( QtCore.Qt.AA_ShareOpenGLContexts) if hasattr(QGuiApplication, 'setDesktopFileName'): QGuiApplication.setDesktopFileName('electrum-mona.desktop') self.gui_thread = threading.current_thread() self.windows = [] # type: List[ElectrumWindow] self.efilter = OpenFileEventFilter(self.windows) self.app = QElectrumApplication(sys.argv) self.app.installEventFilter(self.efilter) self.app.setWindowIcon(read_QIcon("electrum.png")) self._cleaned_up = False # timer self.timer = QTimer(self.app) self.timer.setSingleShot(False) self.timer.setInterval(500) # msec self.network_dialog = None self.lightning_dialog = None self.watchtower_dialog = None self.network_updated_signal_obj = QNetworkUpdatedSignalObject() self._num_wizards_in_progress = 0 self._num_wizards_lock = threading.Lock() self.dark_icon = self.config.get("dark_icon", False) self.tray = None self._init_tray() self.app.new_window_signal.connect(self.start_new_window) self.app.quit_signal.connect(self.app.quit, Qt.QueuedConnection) # maybe set dark theme self._default_qtstylesheet = self.app.styleSheet() self.reload_app_stylesheet() run_hook('init_qt', self) def _init_tray(self): 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() def reload_app_stylesheet(self): """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 """ use_dark_theme = self.config.get('qt_gui_color_theme', 'default') == 'dark' if use_dark_theme: try: import qdarkstyle self.app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5()) except BaseException as e: use_dark_theme = False self.logger.warning(f'Error setting dark theme: {repr(e)}') else: self.app.setStyleSheet(self._default_qtstylesheet) # Apply any necessary stylesheet patches patch_qt_stylesheet(use_dark_theme=use_dark_theme) # 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) def build_tray_menu(self): if not self.tray: return # Avoid immediate GC of old menu when window closed via its action if self.tray.contextMenu() is None: m = QMenu() self.tray.setContextMenu(m) else: m = self.tray.contextMenu() m.clear() network = self.daemon.network m.addAction(_("Network"), self.show_network_dialog) if network and network.lngossip: m.addAction(_("Lightning Network"), self.show_lightning_dialog) if network and network.local_watchtower: m.addAction(_("Local Watchtower"), self.show_watchtower_dialog) for window in self.windows: name = window.wallet.basename() submenu = m.addMenu(name) submenu.addAction(_("Show/Hide"), window.show_or_hide) submenu.addAction(_("Close"), window.close) m.addAction(_("Dark/Light"), self.toggle_tray_icon) m.addSeparator() m.addAction(_("Exit Electrum"), self.app.quit) def tray_icon(self): if self.dark_icon: return read_QIcon('electrum_dark_icon.png') else: return read_QIcon('electrum_light_icon.png') def toggle_tray_icon(self): if not self.tray: return self.dark_icon = not self.dark_icon self.config.set_key("dark_icon", self.dark_icon, True) self.tray.setIcon(self.tray_icon()) def tray_activated(self, reason): if reason == QSystemTrayIcon.DoubleClick: if all([w.is_hidden() for w in self.windows]): for w in self.windows: w.bring_to_top() else: for w in self.windows: w.hide() 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): window.close() window.clean_up() if self.network_dialog: self.network_dialog.close() self.network_dialog.clean_up() self.network_dialog = None self.network_updated_signal_obj = None if self.lightning_dialog: self.lightning_dialog.close() self.lightning_dialog = None if self.watchtower_dialog: self.watchtower_dialog.close() self.watchtower_dialog = None # Shut down the timer cleanly self.timer.stop() self.timer = None # clipboard persistence. see http://www.mail-archive.com/[email protected]/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 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() def new_window(self, path, uri=None): # Use a signal as can be called from daemon thread self.app.new_window_signal.emit(path, uri) def show_lightning_dialog(self): if not self.daemon.network.has_channel_db(): return if not self.lightning_dialog: self.lightning_dialog = LightningDialog(self) self.lightning_dialog.bring_to_top() def show_watchtower_dialog(self): if not self.watchtower_dialog: self.watchtower_dialog = WatchtowerDialog(self) self.watchtower_dialog.bring_to_top() def show_network_dialog(self): if self.network_dialog: self.network_dialog.on_update() self.network_dialog.show() self.network_dialog.raise_() return self.network_dialog = NetworkDialog( network=self.daemon.network, config=self.config, network_updated_signal_obj=self.network_updated_signal_obj) self.network_dialog.show() def _create_window_for_wallet(self, wallet): w = ElectrumWindow(self, wallet) self.windows.append(w) self.build_tray_menu() w.warn_if_testnet() w.warn_if_watching_only() return w 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 self._maybe_quit_if_no_windows_open() return wrapper @count_wizards_in_progress def start_new_window( self, path, uri: Optional[str], *, app_is_starting: bool = False, force_wizard: bool = False, ) -> Optional[ElectrumWindow]: '''Raises the window for the wallet if it is open. Otherwise opens the wallet and creates a new window for it''' wallet = None # 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('') custom_message_box(icon=QMessageBox.Warning, parent=None, title=_('Error'), text=_('Cannot load wallet') + ' (1):\n' + repr(e)) # 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. try: if not wallet: wallet = self._start_wizard_to_select_or_create_wallet(path) if not wallet: return # create or raise window for window in self.windows: if window.wallet.storage.path == wallet.storage.path: break else: window = self._create_window_for_wallet(wallet) except Exception as e: self.logger.exception('') custom_message_box(icon=QMessageBox.Warning, parent=None, title=_('Error'), text=_('Cannot load wallet') + '(2) :\n' + repr(e)) if app_is_starting: # 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) self.start_new_window(path, uri=None, force_wizard=True) return if uri: window.pay_to_URI(uri) window.bring_to_top() window.setWindowState(window.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) window.activateWindow() return window def _start_wizard_to_select_or_create_wallet( self, path) -> Optional[Abstract_Wallet]: wizard = InstallWizard(self.config, self.app, self.plugins, gui_object=self) 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') storage, db = wizard.create_storage(path) else: db = WalletDB(storage.read(), manual_upgrades=False) wizard.run_upgrades(storage, db) except (UserCancelled, GoBack): return except WalletAlreadyOpenInMemory as e: return e.wallet finally: wizard.terminate() # return if wallet creation is not complete if storage is None or db.get_action(): return wallet = Wallet(db, storage, config=self.config) wallet.start_network(self.daemon.network) self.daemon.add_wallet(wallet) return wallet def close_window(self, window: ElectrumWindow): if window in self.windows: self.windows.remove(window) self.build_tray_menu() # save wallet path of last open window if not self.windows: self.config.save_last_wallet(window.wallet) run_hook('on_close_window', window) self.daemon.stop_wallet(window.wallet.storage.path) def init_network(self): # Show network dialog if config does not exist if self.daemon.network: if self.config.get('auto_connect') is None: wizard = InstallWizard(self.config, self.app, self.plugins, gui_object=self) wizard.init_network(self.daemon.network) wizard.terminate() def main(self): # 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()) # hook for crash reporter Exception_Hook.maybe_setup(config=self.config) # first-start network-setup try: self.init_network() except UserCancelled: return except GoBack: return except Exception as e: self.logger.exception('') return # start wizard to select/create wallet self.timer.start() path = self.config.get_wallet_path(use_gui_last_wallet=True) 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. # main loop self.logger.info("starting Qt main loop") self.app.exec_() # on some platforms the exec_ call may not return, so use _cleanup_before_exit def stop(self): self.logger.info('closing GUI') self.app.quit_signal.emit()
class SVApplication(QApplication): # Signals need to be on a QObject create_new_window_signal = pyqtSignal(str, object) cosigner_received_signal = pyqtSignal(object, object) labels_changed_signal = pyqtSignal(object, object, object) window_opened_signal = pyqtSignal(object) window_closed_signal = pyqtSignal(object) # Async tasks async_tasks_done = pyqtSignal() # Logging new_category = pyqtSignal(str) new_log = pyqtSignal(object) # Preferences updates fiat_ccy_changed = pyqtSignal() custom_fee_changed = pyqtSignal() op_return_enabled_changed = pyqtSignal() num_zeros_changed = pyqtSignal() base_unit_changed = pyqtSignal() fiat_history_changed = pyqtSignal() fiat_balance_changed = pyqtSignal() update_check_signal = pyqtSignal(bool, object) # Contact events contact_added_signal = pyqtSignal(object, object) contact_removed_signal = pyqtSignal(object) identity_added_signal = pyqtSignal(object, object) identity_removed_signal = pyqtSignal(object, object) new_notification = pyqtSignal(object, object) def __init__(self, argv): QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) if hasattr(QtCore.Qt, "AA_ShareOpenGLContexts"): QtCore.QCoreApplication.setAttribute( QtCore.Qt.AA_ShareOpenGLContexts) if hasattr(QGuiApplication, 'setDesktopFileName'): QGuiApplication.setDesktopFileName('electrum-sv.desktop') super().__init__(argv) self.windows = [] self.log_handler = SVLogHandler() self.log_window = None self.net_dialog = None self.timer = QTimer() self.exception_hook = None # A floating point number, e.g. 129.1 self.dpi = self.primaryScreen().physicalDotsPerInch() # init tray self.dark_icon = app_state.config.get("dark_icon", False) self.tray = QSystemTrayIcon(self._tray_icon(), None) self.tray.setToolTip('ElectrumSV') self.tray.activated.connect(self._tray_activated) self._build_tray_menu() self.tray.show() # FIXME Fix what.. what needs to be fixed here? set_language(app_state.config.get('language', get_default_language())) logs.add_handler(self.log_handler) self._start() def _start(self): self.setWindowIcon(read_QIcon("electrum-sv.png")) self.installEventFilter(OpenFileEventFilter(self.windows)) self.create_new_window_signal.connect(self.start_new_window) self.async_tasks_done.connect(app_state.async_.run_pending_callbacks) self.num_zeros_changed.connect( partial(self._signal_all, 'on_num_zeros_changed')) self.fiat_ccy_changed.connect( partial(self._signal_all, 'on_fiat_ccy_changed')) self.base_unit_changed.connect( partial(self._signal_all, 'on_base_unit_changed')) self.fiat_history_changed.connect( partial(self._signal_all, 'on_fiat_history_changed')) # Toggling of showing addresses in the fiat preferences. self.fiat_balance_changed.connect( partial(self._signal_all, 'on_fiat_balance_changed')) self.update_check_signal.connect( partial(self._signal_all, 'on_update_check')) ColorScheme.update_from_widget(QWidget()) def _signal_all(self, method, *args): for window in self.windows: getattr(window, method)(*args) def _close(self): for window in self.windows: window.close() def close_window(self, window) -> None: # NOTE: `ElectrumWindow` removes references to itself while it is closing. This creates # a problem where it gets garbage collected before it's Qt5 `closeEvent` handling is # completed and on Linux/MacOS it segmentation faults. On Windows, it is fine. QTimer.singleShot(0, partial(self._close_window, window)) logger.debug("app.close_window.queued") def _close_window(self, window): logger.debug(f"app.close_window.executing {window!r}") app_state.daemon.stop_wallet_at_path(window._wallet.get_storage_path()) self.windows.remove(window) self.window_closed_signal.emit(window) self._build_tray_menu() if not self.windows: self._last_window_closed() def setup_app(self): # app_state.daemon is initialised after app. Setup things dependent on daemon here. pass def _build_tray_menu(self): # Avoid immediate GC of old menu when window closed via its action if self.tray.contextMenu() is None: m = QMenu() self.tray.setContextMenu(m) else: m = self.tray.contextMenu() m.clear() for window in self.windows: submenu = m.addMenu(window._wallet.name()) submenu.addAction(_("Show/Hide"), window.show_or_hide) submenu.addAction(_("Close"), window.close) m.addAction(_("Dark/Light"), self._toggle_tray_icon) m.addSeparator() m.addAction(_("Exit ElectrumSV"), self._close) self.tray.setContextMenu(m) def _tray_icon(self): if self.dark_icon: return read_QIcon('electrumsv_dark_icon.png') else: return read_QIcon('electrumsv_light_icon.png') def _toggle_tray_icon(self) -> None: self.dark_icon = not self.dark_icon app_state.config.set_key("dark_icon", self.dark_icon, True) self.tray.setIcon(self._tray_icon()) def _tray_activated(self, reason) -> None: if reason == QSystemTrayIcon.DoubleClick: if all([w.is_hidden() for w in self.windows]): for w in self.windows: w.bring_to_top() else: for w in self.windows: w.hide() def new_window(self, path: Optional[str], uri: Optional[str] = None) -> None: # Use a signal as can be called from daemon thread self.create_new_window_signal.emit(path, uri) def show_network_dialog(self, parent) -> None: if not app_state.daemon.network: parent.show_warning(_( 'You are using ElectrumSV in offline mode; restart ' 'ElectrumSV if you want to get connected'), title=_('Offline')) return if self.net_dialog: self.net_dialog.on_update() self.net_dialog.show() self.net_dialog.raise_() return from . import network_dialog # from importlib import reload # reload(network_dialog) self.net_dialog = network_dialog.NetworkDialog( app_state.daemon.network, app_state.config) self.net_dialog.show() def show_log_viewer(self) -> None: if self.log_window is None: self.log_window = SVLogWindow(None, self.log_handler) self.log_window.show() def _last_window_closed(self) -> None: for dialog in (self.net_dialog, self.log_window): if dialog: dialog.accept() def on_transaction_label_change(self, wallet: Wallet, tx_hash: bytes, text: str) -> None: self.label_sync.set_transaction_label(wallet, tx_hash, text) def on_keyinstance_label_change(self, account: AbstractAccount, key_id: int, text: str) -> None: self.label_sync.set_keyinstance_label(account, key_id, text) def _create_window_for_wallet(self, wallet: Wallet) -> ElectrumWindow: w = ElectrumWindow(wallet) self.windows.append(w) self._build_tray_menu() self._register_wallet_events(wallet) self.window_opened_signal.emit(w) return w def _register_wallet_events(self, wallet: Wallet) -> None: wallet.contacts._on_contact_added = self._on_contact_added wallet.contacts._on_contact_removed = self._on_contact_removed wallet.contacts._on_identity_added = self._on_identity_added wallet.contacts._on_identity_removed = self._on_identity_removed def _on_identity_added(self, contact: ContactEntry, identity: ContactIdentity) -> None: self.identity_added_signal.emit(contact, identity) def _on_identity_removed(self, contact: ContactEntry, identity: ContactIdentity) -> None: self.identity_removed_signal.emit(contact, identity) def _on_contact_added(self, contact: ContactEntry, identity: ContactIdentity) -> None: self.contact_added_signal.emit(contact, identity) def _on_contact_removed(self, contact: ContactEntry) -> None: self.contact_removed_signal.emit(contact) def on_new_wallet_event(self, wallet_path: str, row: WalletEventRow) -> None: self.new_notification.emit(wallet_path, row) def get_wallet_window(self, path: str) -> Optional[ElectrumWindow]: for w in self.windows: if w._wallet.get_storage_path() == path: return w def get_wallet_window_by_id(self, account_id: int) -> Optional[ElectrumWindow]: for w in self.windows: for account in w._wallet.get_accounts(): if account.get_id() == account_id: return w def start_new_window(self, wallet_path: Optional[str], uri: Optional[str] = None, is_startup: bool = False) -> Optional[ElectrumWindow]: '''Raises the window for the wallet if it is open. Otherwise opens the wallet and creates a new window for it.''' for w in self.windows: if w._wallet.get_storage_path() == wallet_path: w.bring_to_top() break else: wizard_window: Optional[WalletWizard] = None if wallet_path is not None: is_valid, was_aborted, wizard_window = WalletWizard.attempt_open( wallet_path) if was_aborted: return None if not is_valid: wallet_filename = os.path.basename(wallet_path) MessageBox.show_error( _("Unable to load file '{}'.").format(wallet_filename)) return None else: wizard_window = WalletWizard(is_startup=is_startup) if wizard_window is not None: result = wizard_window.run() if result != QDialog.Accepted: return None wallet_path = wizard_window.get_wallet_path() # We cannot rely on accept alone indicating success. if wallet_path is None: return None wallet = app_state.daemon.load_wallet(wallet_path) assert wallet is not None w = self._create_window_for_wallet(wallet) if uri: w.pay_to_URI(uri) w.bring_to_top() w.setWindowState(w.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) # this will activate the window w.activateWindow() return w def update_check(self) -> None: if (not app_state.config.get('check_updates', True) or app_state.config.get("offline", False)): return def f(): import requests try: response = requests.request( 'GET', "https://electrumsv.io/release.json", headers={'User-Agent': 'ElectrumSV'}, timeout=10) result = response.json() self._on_update_check(True, result) except Exception: self._on_update_check(False, sys.exc_info()) t = threading.Thread(target=f) t.setDaemon(True) t.start() def _on_update_check(self, success: bool, result: dict) -> None: if success: when_checked = datetime.datetime.now().astimezone().isoformat() app_state.config.set_key('last_update_check', result) app_state.config.set_key('last_update_check_time', when_checked, True) self.update_check_signal.emit(success, result) def initial_dialogs(self) -> None: '''Suppressible dialogs that are shown when first opening the app.''' dialogs.show_named('welcome-ESV-1.3.7') def event_loop_started(self) -> None: self.cosigner_pool = CosignerPool() self.label_sync = LabelSync() if app_state.config.get("show_crash_reporter", default=True): self.exception_hook = Exception_Hook(self) self.timer.start() signal.signal(signal.SIGINT, lambda *args: self.quit()) self.initial_dialogs() path = app_state.config.get_cmdline_wallet_filepath() if not self.start_new_window( path, app_state.config.get('url'), is_startup=True): self.quit() def run_app(self) -> None: when_started = datetime.datetime.now().astimezone().isoformat() app_state.config.set_key('previous_start_time', app_state.config.get("start_time")) app_state.config.set_key('start_time', when_started, True) self.update_check() threading.current_thread().setName('GUI') self.timer.setSingleShot(False) self.timer.setInterval(500) # msec self.timer.timeout.connect(app_state.device_manager.timeout_clients) QTimer.singleShot(0, self.event_loop_started) self.exec_() logs.remove_handler(self.log_handler) # Shut down the timer cleanly self.timer.stop() # clipboard persistence # see http://www.mail-archive.com/[email protected]/msg17328.html event = QtCore.QEvent(QtCore.QEvent.Clipboard) self.sendEvent(self.clipboard(), event) self.tray.hide() def run_coro(self, coro, *args, on_done=None): '''Run a coroutine. on_done, if given, is passed the future containing the reuslt or exception, and is guaranteed to be called in the context of the GUI thread. ''' def task_done(future): self.async_tasks_done.emit() future = app_state.async_.spawn(coro, *args, on_done=on_done) future.add_done_callback(task_done) return future def run_in_thread(self, func, *args, on_done: Optional[Callable[[concurrent.futures.Future], None]] = None): '''Run func(*args) in a thread. on_done, if given, is passed the future containing the reuslt or exception, and is guaranteed to be called in the context of the GUI thread. ''' return self.run_coro(run_in_thread, func, *args, on_done=on_done)
class ElectrumGui(PrintError): @profiler def __init__(self, config, daemon, plugins): set_language(config.get('language', get_default_language())) # 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)]) QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) if hasattr(QtCore.Qt, "AA_ShareOpenGLContexts"): QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts) if hasattr(QGuiApplication, 'setDesktopFileName'): QGuiApplication.setDesktopFileName('electrum.desktop') self.gui_thread = threading.current_thread() self.config = config self.daemon = daemon self.plugins = plugins self.windows = [] self.efilter = OpenFileEventFilter(self.windows) self.app = QElectrumApplication(sys.argv) self.app.installEventFilter(self.efilter) self.app.setWindowIcon(read_QIcon("electrum.png")) # timer self.timer = QTimer(self.app) self.timer.setSingleShot(False) self.timer.setInterval(500) # msec self.nd = None self.network_updated_signal_obj = QNetworkUpdatedSignalObject() self._num_wizards_in_progress = 0 self._num_wizards_lock = threading.Lock() # init tray self.dark_icon = self.config.get("dark_icon", False) 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() self.app.new_window_signal.connect(self.start_new_window) self.set_dark_theme_if_needed() run_hook('init_qt', self) def set_dark_theme_if_needed(self): use_dark_theme = self.config.get('qt_gui_color_theme', 'default') == 'dark' if use_dark_theme: try: import qdarkstyle self.app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5()) except BaseException as e: use_dark_theme = False self.print_error('Error setting dark theme: {}'.format(repr(e))) # 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) def build_tray_menu(self): # Avoid immediate GC of old menu when window closed via its action if self.tray.contextMenu() is None: m = QMenu() self.tray.setContextMenu(m) else: m = self.tray.contextMenu() m.clear() for window in self.windows: submenu = m.addMenu(window.wallet.basename()) submenu.addAction(_("Show/Hide"), window.show_or_hide) submenu.addAction(_("Close"), window.close) m.addAction(_("Dark/Light"), self.toggle_tray_icon) m.addSeparator() m.addAction(_("Exit Electrum"), self.close) def tray_icon(self): if self.dark_icon: return read_QIcon('electrum_dark_icon.png') else: return read_QIcon('electrum_light_icon.png') def toggle_tray_icon(self): self.dark_icon = not self.dark_icon self.config.set_key("dark_icon", self.dark_icon, True) self.tray.setIcon(self.tray_icon()) def tray_activated(self, reason): if reason == QSystemTrayIcon.DoubleClick: if all([w.is_hidden() for w in self.windows]): for w in self.windows: w.bring_to_top() else: for w in self.windows: w.hide() def close(self): for window in self.windows: window.close() def new_window(self, path, uri=None): # Use a signal as can be called from daemon thread self.app.new_window_signal.emit(path, uri) def show_network_dialog(self, parent): if not self.daemon.network: parent.show_warning(_('You are using Electrum in offline mode; restart Electrum if you want to get connected'), title=_('Offline')) return if self.nd: self.nd.on_update() self.nd.show() self.nd.raise_() return self.nd = NetworkDialog(self.daemon.network, self.config, self.network_updated_signal_obj) self.nd.show() def _create_window_for_wallet(self, wallet): w = ElectrumWindow(self, wallet) self.windows.append(w) self.build_tray_menu() # FIXME: Remove in favour of the load_wallet hook run_hook('on_new_window', w) w.warn_if_watching_only() return w 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 return wrapper @count_wizards_in_progress def start_new_window(self, path, uri, *, app_is_starting=False): '''Raises the window for the wallet if it is open. Otherwise opens the wallet and creates a new window for it''' wallet = None try: wallet = self.daemon.load_wallet(path, None) except BaseException as e: traceback.print_exc(file=sys.stdout) QMessageBox.warning(None, _('Error'), _('Cannot load wallet') + ' (1):\n' + str(e)) # if app is starting, still let wizard to appear if not app_is_starting: return if not wallet: wallet = self._start_wizard_to_select_or_create_wallet(path) if not wallet: return # create or raise window try: for window in self.windows: if window.wallet.storage.path == wallet.storage.path: break else: window = self._create_window_for_wallet(wallet) except BaseException as e: traceback.print_exc(file=sys.stdout) QMessageBox.warning(None, _('Error'), _('Cannot create window for wallet') + ':\n' + str(e)) if app_is_starting: wallet_dir = os.path.dirname(path) path = os.path.join(wallet_dir, get_new_wallet_name(wallet_dir)) self.start_new_window(path, uri) return if uri: window.pay_to_URI(uri) window.bring_to_top() window.setWindowState(window.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) window.activateWindow() return window def _start_wizard_to_select_or_create_wallet(self, path) -> Optional[Abstract_Wallet]: wizard = InstallWizard(self.config, self.app, self.plugins) 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') storage = wizard.create_storage(path) else: wizard.run_upgrades(storage) except (UserCancelled, GoBack): return except WalletAlreadyOpenInMemory as e: return e.wallet except (WalletFileException, BitcoinException) as e: traceback.print_exc(file=sys.stderr) QMessageBox.warning(None, _('Error'), _('Cannot load wallet') + ' (2):\n' + str(e)) return finally: wizard.terminate() # return if wallet creation is not complete if storage is None or storage.get_action(): return wallet = Wallet(storage) wallet.start_network(self.daemon.network) self.daemon.add_wallet(wallet) return wallet def close_window(self, window): if window in self.windows: self.windows.remove(window) self.build_tray_menu() # save wallet path of last open window if not self.windows: self.config.save_last_wallet(window.wallet) run_hook('on_close_window', window) self.daemon.stop_wallet(window.wallet.storage.path) def init_network(self): # Show network dialog if config does not exist if self.daemon.network: if self.config.get('auto_connect') is None: wizard = InstallWizard(self.config, self.app, self.plugins) wizard.init_network(self.daemon.network) wizard.terminate() def main(self): try: self.init_network() except UserCancelled: return except GoBack: return except BaseException as e: traceback.print_exc(file=sys.stdout) return self.timer.start() self.config.open_last_wallet() path = self.config.get_wallet_path() if not self.start_new_window(path, self.config.get('url'), app_is_starting=True): return signal.signal(signal.SIGINT, lambda *args: self.app.quit()) def quit_after_last_window(): # 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() self.app.setQuitOnLastWindowClosed(False) # so _we_ can decide whether to quit self.app.lastWindowClosed.connect(quit_after_last_window) def clean_up(): # Shut down the timer cleanly self.timer.stop() # clipboard persistence. see http://www.mail-archive.com/[email protected]/msg17328.html event = QtCore.QEvent(QtCore.QEvent.Clipboard) self.app.sendEvent(self.app.clipboard(), event) self.tray.hide() self.app.aboutToQuit.connect(clean_up) # main loop self.app.exec_() # on some platforms the exec_ call may not return, so use clean_up() def stop(self): self.print_error('closing GUI') self.app.quit()
class Parse99(QApplication): def __init__(self, *args): super(QApplication, self).__init__(*args) # Tray Icon self._system_tray = QSystemTrayIcon() self._system_tray.setIcon(QIcon('ui/icon.png')) self._system_tray.setToolTip("Parse99") self._system_tray.show() # Settings self.settings = settings.Settings("parse99") # Plugins self._plugins = {'maps': Maps(self.settings)} # Timer self._timer = QTimer() self._timer.timeout.connect(self._parse) # Thread self._thread = None # Menu self._system_tray.setContextMenu(self._get_menu()) # File self._log_file = "" self._file_size = 0 self._last_line_read = 0 self._log_new_lines = [] # Start self.toggle('on') def _settings_valid(self): valid = True if self.settings.get_value('general', 'first_run') is None: self._system_tray.showMessage( "Parse99", """It looks like this is the first time the program is being run. Please setup the application using the Settings option once you right click the system tray icon.""" ) self.edit_settings() self.settings.set_value('general', 'first_run', True) valid = False elif self.settings.get_value('general', 'eq_directory') is None: self._system_tray.showMessage( "Parse99", "Please enter the General settings and \ choose the location of your Everquest Installation." ) self.edit_settings() valid = False elif self.settings.get_value('characters', None) is None: self._system_tray.showMessage( "Parse99", "No characters have been made. \ Please create at least one character using settings." ) self.edit_settings(tab="characters") valid = False elif self.settings.get_value('general', 'current_character') is None: self._system_tray.showMessage( "Parse99", "No character has been selected. \ Please choose a character from the Character menu." ) valid = False return valid def toggle(self, switch): if switch == 'off': if self._thread is not None: self._timer.stop() self._thread.stop() self._thread.join() elif switch == 'on': if self._settings_valid(): characters = self.settings.get_value('characters', None) log_file = characters[ self.settings.get_value('general', 'current_character') ]['log_file'] self._thread = FileReader( log_file, int(self.settings.get_value('general', 'parse_interval')) ) self._thread.start() self._timer.start( 1000 * int(self.settings.get_value('general', 'parse_interval')) ) def _parse(self): for line in self._thread.get_new_lines(): for plugin in self._plugins.keys(): if self._plugins[plugin].is_active(): self._plugins[plugin].parse(line) def _get_menu(self): # main menu menu = QMenu() main_menu_action_group = QActionGroup(menu) main_menu_action_group.setObjectName("main") # character menu map_action = QAction(menu) map_action.setText("Toggle Map") main_menu_action_group.addAction(map_action) separator = QAction(menu) separator.setSeparator(True) main_menu_action_group.addAction(separator) characters_action = QAction(menu) characters_action.setText("Switch Characters") main_menu_action_group.addAction(characters_action) separator = QAction(menu) separator.setSeparator(True) main_menu_action_group.addAction(separator) settings_action = QAction(menu) settings_action.setText("Settings") main_menu_action_group.addAction(settings_action) quit_action = QAction(menu) quit_action.setText("Quit") main_menu_action_group.addAction(quit_action) menu.addActions(main_menu_action_group.actions()) menu.triggered[QAction].connect(self._menu_actions) return menu def update_menu(self): self._system_tray.contextMenu().disconnect() self._system_tray.setContextMenu(self._get_menu()) def _menu_actions(self, action): # ag = action group, at = action text ag = action.actionGroup().objectName() at = action.text().lower() if ag == "main": if at == "quit": try: self.toggle('off') self._system_tray.setVisible(False) for plugin in self._plugins.keys(): self._plugins[plugin].close() self.quit() except Exception as e: print("menu actions, quit", e) elif at == "settings": self.edit_settings(tab="general") elif at == "switch characters": print("switch characters") self.edit_settings(tab="characters") elif at == "toggle map": self._plugins['maps'].toggle() def edit_settings(self, **kwargs): try: if not self.settings.gui.isVisible(): self.toggle('off') self.settings.gui.set_show_tab(kwargs.get('tab', None)) self.settings.gui.exec() self.toggle('on') except Exception as e: print("parse99.edit_settings():", e)
class MainWindow(QMainWindow): def __init__(self): super(MainWindow, self).__init__() settings = QSettings() self.setup_trayicon() self.setup_ui() self.update_work_end_time() self.update_rest_end_time() self.setup_connections() self.timeFormat = "hh:mm:ss" self.time = QTime(0, 0, 0, 0) self.workTime = QTime(0, 0, 0, 0) self.restTime = QTime(0, 0, 0, 0) self.totalTime = QTime(0, 0, 0, 0) self.currentMode = Mode.work self.maxRepetitions = -1 self.currentRepetitions = 0 self.show() def leaveEvent(self, event): super(MainWindow, self).leaveEvent(event) self.tasksTableWidget.clearSelection() def closeEvent(self, event): super(MainWindow, self).closeEvent(event) settings = QSettings() settings.setValue("timer/work/hours", self.workHoursSpinBox.value()) settings.setValue("timer/work/minutes", self.workMinutesSpinBox.value()) settings.setValue("timer/work/seconds", self.workSecondsSpinBox.value()) settings.setValue("timer/rest/hours", self.restHoursSpinBox.value()) settings.setValue("timer/rest/minutes", self.restMinutesSpinBox.value()) settings.setValue("timer/rest/seconds", self.restSecondsSpinBox.value()) tasks = [] for i in range(self.tasksTableWidget.rowCount()): item = self.tasksTableWidget.item(i, 0) if not item.font().strikeOut(): tasks.append(item.text()) settings.setValue("tasks/tasks", tasks) def start_timer(self): try: if not self.timer.isActive(): self.create_timer() except: self.create_timer() def create_timer(self): self.timer = QTimer() self.timer.timeout.connect(self.update_time) self.timer.timeout.connect(self.maybe_change_mode) self.timer.setInterval(1000) self.timer.setSingleShot(False) self.timer.start() def pause_timer(self): try: self.timer.stop() self.timer.disconnect() except: pass def reset_timer(self): try: self.pause_timer() self.time = QTime(0, 0, 0, 0) self.display_time() except: pass def maybe_start_timer(self): if self.currentRepetitions != self.maxRepetitions: self.start_timer() started = True else: self.currentRepetitions = 0 started = False return started def update_work_end_time(self): self.workEndTime = QTime(self.workHoursSpinBox.value(), self.workMinutesSpinBox.value(), self.workSecondsSpinBox.value()) def update_rest_end_time(self): self.restEndTime = QTime(self.restHoursSpinBox.value(), self.restMinutesSpinBox.value(), self.restSecondsSpinBox.value()) def update_current_mode(self, mode: str): self.currentMode = Mode.work if mode == "work" else Mode.rest def update_time(self): self.time = self.time.addSecs(1) self.totalTime = self.totalTime.addSecs(1) if self.modeComboBox.currentText() == "work": self.workTime = self.workTime.addSecs(1) else: self.restTime = self.restTime.addSecs(1) self.display_time() def update_max_repetitions(self, value): if value == 0: self.currentRepetitions = 0 self.maxRepetitions = -1 else: self.maxRepetitions = 2 * value def maybe_change_mode(self): if self.currentMode is Mode.work and self.time >= self.workEndTime: self.reset_timer() self.modeComboBox.setCurrentIndex(1) self.increment_current_repetitions() started = self.maybe_start_timer() self.show_window_message( Status.workFinished if started else Status.repetitionsReached) elif self.currentMode is Mode.rest and self.time >= self.restEndTime: self.reset_timer() self.modeComboBox.setCurrentIndex(0) self.increment_current_repetitions() started = self.maybe_start_timer() self.show_window_message( Status.restFinished if started else Status.repetitionsReached) def increment_current_repetitions(self): if self.maxRepetitions > 0: self.currentRepetitions += 1 def insert_task(self): task = self.taskTextEdit.toPlainText() self.insert_tasks(task) def insert_tasks(self, *tasks): for task in tasks: if task: rowCount = self.tasksTableWidget.rowCount() self.tasksTableWidget.setRowCount(rowCount + 1) self.tasksTableWidget.setItem(rowCount, 0, QTableWidgetItem(task)) self.tasksTableWidget.resizeRowsToContents() self.taskTextEdit.clear() def delete_task(self): selectedIndexes = self.tasksTableWidget.selectedIndexes() if selectedIndexes: self.tasksTableWidget.removeRow(selectedIndexes[0].row()) def mark_task_as_finished(self, row, col): item = self.tasksTableWidget.item(row, col) font = self.tasksTableWidget.item(row, col).font() font.setStrikeOut(False if item.font().strikeOut() else True) item.setFont(font) def display_time(self): self.timeDisplay.display(self.time.toString(self.timeFormat)) self.statisticsRestTimeDisplay.display( self.restTime.toString(self.timeFormat)) self.statisticsWorkTimeDisplay.display( self.workTime.toString(self.timeFormat)) self.statisticsTotalTimeDisplay.display( self.totalTime.toString(self.timeFormat)) def show_window_message(self, status): if status is Status.workFinished: self.trayIcon.showMessage("Break", choice(work_finished_phrases), QIcon("icons/tomato.png")) elif status is Status.restFinished: self.trayIcon.showMessage("Work", choice(rest_finished_phrases), QIcon("icons/tomato.png")) else: self.trayIcon.showMessage("Finished", choice(pomodoro_finished_phrases), QIcon("icons/tomato.png")) self.resetButton.click() def setup_connections(self): self.playButton.clicked.connect(self.start_timer) self.playButton.clicked.connect( lambda: self.playButton.setDisabled(True)) self.playButton.clicked.connect( lambda: self.pauseButton.setDisabled(False)) self.playButton.clicked.connect( lambda: self.resetButton.setDisabled(False)) self.pauseButton.clicked.connect(self.pause_timer) self.pauseButton.clicked.connect( lambda: self.playButton.setDisabled(False)) self.pauseButton.clicked.connect( lambda: self.pauseButton.setDisabled(True)) self.pauseButton.clicked.connect( lambda: self.resetButton.setDisabled(False)) self.resetButton.clicked.connect(self.reset_timer) self.resetButton.clicked.connect( lambda: self.playButton.setDisabled(False)) self.resetButton.clicked.connect( lambda: self.pauseButton.setDisabled(True)) self.resetButton.clicked.connect( lambda: self.resetButton.setDisabled(True)) self.workHoursSpinBox.valueChanged.connect(self.update_work_end_time) self.workMinutesSpinBox.valueChanged.connect(self.update_work_end_time) self.workSecondsSpinBox.valueChanged.connect(self.update_work_end_time) self.restHoursSpinBox.valueChanged.connect(self.update_rest_end_time) self.restMinutesSpinBox.valueChanged.connect(self.update_rest_end_time) self.restSecondsSpinBox.valueChanged.connect(self.update_rest_end_time) self.modeComboBox.currentTextChanged.connect(self.update_current_mode) self.repetitionsSpinBox.valueChanged.connect( self.update_max_repetitions) self.acceptTaskButton.pressed.connect(self.insert_task) self.deleteTaskButton.pressed.connect(self.delete_task) self.tasksTableWidget.cellDoubleClicked.connect( self.mark_task_as_finished) def setup_ui(self): settings = QSettings() self.size_policy = sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) #TABWIDGET self.tabWidget = QTabWidget() self.pomodoroWidget = QWidget(self) self.pomodoroWidgetLayout = QVBoxLayout(self.pomodoroWidget) self.pomodoroWidget.setLayout(self.pomodoroWidgetLayout) # work self.workGroupBox = QGroupBox("Work") self.workGroupBoxLayout = QHBoxLayout(self.workGroupBox) self.workGroupBox.setLayout(self.workGroupBoxLayout) self.workHoursSpinBox = QSpinBox(minimum=0, maximum=24, value=settings.value( "timer/work/hours", 0), suffix="h", sizePolicy=self.size_policy) self.workMinutesSpinBox = QSpinBox(minimum=0, maximum=60, value=settings.value( "timer/work/minutes", 25), suffix="m", sizePolicy=self.size_policy) self.workSecondsSpinBox = QSpinBox(minimum=0, maximum=60, value=settings.value( "timer/work/seconds", 0), suffix="s", sizePolicy=self.size_policy) self.workGroupBoxLayout.addWidget(self.workHoursSpinBox) self.workGroupBoxLayout.addWidget(self.workMinutesSpinBox) self.workGroupBoxLayout.addWidget(self.workSecondsSpinBox) # rest self.restGroupBox = QGroupBox("Rest") self.restGroupBoxLayout = QHBoxLayout(self.restGroupBox) self.restGroupBox.setLayout(self.restGroupBoxLayout) self.restHoursSpinBox = QSpinBox(minimum=0, maximum=24, value=settings.value( "timer/rest/hours", 0), suffix="h", sizePolicy=self.size_policy) self.restMinutesSpinBox = QSpinBox(minimum=0, maximum=60, value=settings.value( "timer/rest/minutes", 5), suffix="m", sizePolicy=self.size_policy) self.restSecondsSpinBox = QSpinBox(minimum=0, maximum=60, value=settings.value( "timer/rest/seconds", 0), suffix="s", sizePolicy=self.size_policy) self.restGroupBoxLayout.addWidget(self.restHoursSpinBox) self.restGroupBoxLayout.addWidget(self.restMinutesSpinBox) self.restGroupBoxLayout.addWidget(self.restSecondsSpinBox) #OTHER self.otherGroupBox = QGroupBox("Other") self.otherGroupBoxLayout = QHBoxLayout(self.otherGroupBox) self.otherGroupBox.setLayout(self.otherGroupBoxLayout) self.repetitionsLabel = QLabel("Repetitions", sizePolicy=self.size_policy) self.repetitionsSpinBox = QSpinBox(minimum=0, maximum=10000, value=0, sizePolicy=self.size_policy, specialValueText="∞") self.modeLabel = QLabel("Mode", sizePolicy=self.size_policy) self.modeComboBox = QComboBox() self.modeComboBox.addItems(["work", "rest"]) self.otherGroupBoxLayout.addWidget(self.repetitionsLabel) self.otherGroupBoxLayout.addWidget(self.repetitionsSpinBox) self.otherGroupBoxLayout.addWidget(self.modeLabel) self.otherGroupBoxLayout.addWidget(self.modeComboBox) #LCDDISPLAY self.lcdDisplayGroupBox = QGroupBox("Time") self.lcdDisplayGroupBoxLayout = QHBoxLayout(self.lcdDisplayGroupBox) self.lcdDisplayGroupBox.setLayout(self.lcdDisplayGroupBoxLayout) self.timeDisplay = QLCDNumber(8, sizePolicy=self.size_policy) self.timeDisplay.setFixedHeight(100) self.timeDisplay.display("00:00:00") self.lcdDisplayGroupBoxLayout.addWidget(self.timeDisplay) #BUTTONS self.buttonWidget = QWidget() self.buttonWidgetLayout = QHBoxLayout(self.buttonWidget) self.buttonWidget.setLayout(self.buttonWidgetLayout) self.playButton = self.make_button("start", disabled=False) self.resetButton = self.make_button("reset") self.pauseButton = self.make_button("pause") self.buttonWidgetLayout.addWidget(self.pauseButton) self.buttonWidgetLayout.addWidget(self.playButton) self.buttonWidgetLayout.addWidget(self.resetButton) #CENTRALWIDGET self.pomodoroWidgetLayout.addWidget(self.workGroupBox) self.pomodoroWidgetLayout.addWidget(self.restGroupBox) self.pomodoroWidgetLayout.addWidget(self.otherGroupBox) self.pomodoroWidgetLayout.addWidget(self.lcdDisplayGroupBox) self.pomodoroWidgetLayout.addWidget(self.buttonWidget) #CREATE TASKS TAB self.tasksWidget = QWidget(self.tabWidget) self.tasksWidgetLayout = QVBoxLayout(self.tasksWidget) self.tasksWidget.setLayout(self.tasksWidgetLayout) self.inputWidget = QWidget() self.inputWidget.setFixedHeight(50) self.inputWidgetLayout = QHBoxLayout(self.inputWidget) self.inputWidgetLayout.setContentsMargins(0, 0, 0, 0) self.inputWidget.setLayout(self.inputWidgetLayout) self.taskTextEdit = QTextEdit( placeholderText="Describe your task briefly.", undoRedoEnabled=True) self.inputButtonContainer = QWidget() self.inputButtonContainerLayout = QVBoxLayout( self.inputButtonContainer) self.inputButtonContainerLayout.setContentsMargins(0, 0, 0, 0) self.inputButtonContainer.setLayout(self.inputButtonContainerLayout) self.acceptTaskButton = QToolButton(icon=QIcon("icons/check.png")) self.deleteTaskButton = QToolButton(icon=QIcon("icons/trash.png")) self.inputButtonContainerLayout.addWidget(self.acceptTaskButton) self.inputButtonContainerLayout.addWidget(self.deleteTaskButton) self.inputWidgetLayout.addWidget(self.taskTextEdit) self.inputWidgetLayout.addWidget(self.inputButtonContainer) self.tasksTableWidget = QTableWidget(0, 1) self.tasksTableWidget.setHorizontalHeaderLabels(["Tasks"]) self.tasksTableWidget.horizontalHeader().setStretchLastSection(True) self.tasksTableWidget.verticalHeader().setVisible(False) self.tasksTableWidget.setWordWrap(True) self.tasksTableWidget.setTextElideMode(Qt.ElideNone) self.tasksTableWidget.setEditTriggers(QAbstractItemView.NoEditTriggers) self.tasksTableWidget.setSelectionMode( QAbstractItemView.SingleSelection) self.insert_tasks(*settings.value("tasks/tasks", [])) self.tasksWidgetLayout.addWidget(self.inputWidget) self.tasksWidgetLayout.addWidget(self.tasksTableWidget) #CREATE STATISTICS TAB self.statisticsWidget = QWidget() self.statisticsWidgetLayout = QVBoxLayout(self.statisticsWidget) self.statisticsWidget.setLayout(self.statisticsWidgetLayout) self.statisticsWorkTimeGroupBox = QGroupBox("Work Time") self.statisticsWorkTimeGroupBoxLayout = QHBoxLayout() self.statisticsWorkTimeGroupBox.setLayout( self.statisticsWorkTimeGroupBoxLayout) self.statisticsWorkTimeDisplay = QLCDNumber(8) self.statisticsWorkTimeDisplay.display("00:00:00") self.statisticsWorkTimeGroupBoxLayout.addWidget( self.statisticsWorkTimeDisplay) self.statisticsRestTimeGroupBox = QGroupBox("Rest Time") self.statisticsRestTimeGroupBoxLayout = QHBoxLayout() self.statisticsRestTimeGroupBox.setLayout( self.statisticsRestTimeGroupBoxLayout) self.statisticsRestTimeDisplay = QLCDNumber(8) self.statisticsRestTimeDisplay.display("00:00:00") self.statisticsRestTimeGroupBoxLayout.addWidget( self.statisticsRestTimeDisplay) self.statisticsTotalTimeGroupBox = QGroupBox("Total Time") self.statisticsTotalTimeGroupBoxLayout = QHBoxLayout() self.statisticsTotalTimeGroupBox.setLayout( self.statisticsTotalTimeGroupBoxLayout) self.statisticsTotalTimeDisplay = QLCDNumber(8) self.statisticsTotalTimeDisplay.display("00:00:00") self.statisticsTotalTimeGroupBoxLayout.addWidget( self.statisticsTotalTimeDisplay) self.statisticsWidgetLayout.addWidget(self.statisticsTotalTimeGroupBox) self.statisticsWidgetLayout.addWidget(self.statisticsWorkTimeGroupBox) self.statisticsWidgetLayout.addWidget(self.statisticsRestTimeGroupBox) #ADD TABS self.timerTab = self.tabWidget.addTab(self.pomodoroWidget, QIcon("icons/timer.png"), "Timer") self.tasksTab = self.tabWidget.addTab(self.tasksWidget, QIcon("icons/tasks.png"), "Tasks") self.statisticsTab = self.tabWidget.addTab( self.statisticsWidget, QIcon("icons/statistics.png"), "Statistics") self.setCentralWidget(self.tabWidget) def make_button(self, text, iconPath=None, disabled=True): button = QPushButton(text, sizePolicy=self.size_policy) if iconPath: button.setIcon(QIcon(iconPath)) button.setDisabled(disabled) return button def setup_trayicon(self): self.trayIcon = QSystemTrayIcon(QIcon("icons/tomato.png")) self.trayIcon.setContextMenu(QMenu()) self.quitAction = self.trayIcon.contextMenu().addAction( QIcon("icons/exit.png"), "Quit", self.exit) self.quitAction.triggered.connect(self.exit) self.trayIcon.activated.connect(self.onActivate) self.trayIcon.show() def exit(self): self.close() app = QApplication.instance() if app: app.quit() def onActivate(self, reason): if reason == QSystemTrayIcon.Trigger: self.show()
class ElectrumGui: def __init__(self, config, daemon, plugins): set_language(config.get('language')) # 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)]) QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) if hasattr(QtCore.Qt, "AA_ShareOpenGLContexts"): QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts) if hasattr(QGuiApplication, 'setDesktopFileName'): QGuiApplication.setDesktopFileName('electrum.desktop') self.config = config self.daemon = daemon self.plugins = plugins self.windows = [] self.efilter = OpenFileEventFilter(self.windows) self.app = QElectrumApplication(sys.argv) self.app.installEventFilter(self.efilter) self.timer = Timer() self.nd = None self.network_updated_signal_obj = QNetworkUpdatedSignalObject() # init tray self.dark_icon = self.config.get("dark_icon", False) self.tray = QSystemTrayIcon(self.tray_icon(), None) self.tray.setToolTip('Ocean wallet') self.tray.activated.connect(self.tray_activated) self.build_tray_menu() self.tray.show() self.app.new_window_signal.connect(self.start_new_window) self.set_dark_theme_if_needed() run_hook('init_qt', self) def set_dark_theme_if_needed(self): use_dark_theme = self.config.get('qt_gui_color_theme', 'light') == 'dark' if use_dark_theme: try: file = QFile(":/dark.qss") file.open(QFile.ReadOnly | QFile.Text) stream = QTextStream(file) self.app.setStyleSheet(stream.readAll()) except BaseException as e: use_dark_theme = False print_error('Error setting dark theme: {}'.format(e)) else: try: file = QFile(":/light.qss") file.open(QFile.ReadOnly | QFile.Text) stream = QTextStream(file) self.app.setStyleSheet(stream.readAll()) except BaseException as e: print_error('Error setting light theme: {}'.format(e)) # 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) def build_tray_menu(self): # Avoid immediate GC of old menu when window closed via its action if self.tray.contextMenu() is None: m = QMenu() self.tray.setContextMenu(m) else: m = self.tray.contextMenu() m.clear() for window in self.windows: submenu = m.addMenu(window.wallet.basename()) submenu.addAction(_("Show/Hide"), window.show_or_hide) submenu.addAction(_("Close"), window.close) m.addAction(_("Dark/Light"), self.toggle_tray_icon) m.addSeparator() m.addAction(_("Exit Electrum"), self.close) def tray_icon(self): if self.dark_icon: return QIcon(':icons/electrum_dark_icon.png') else: return QIcon(':icons/electrum_light_icon.png') def toggle_tray_icon(self): self.dark_icon = not self.dark_icon self.config.set_key("dark_icon", self.dark_icon, True) self.tray.setIcon(self.tray_icon()) def tray_activated(self, reason): if reason == QSystemTrayIcon.DoubleClick: if all([w.is_hidden() for w in self.windows]): for w in self.windows: w.bring_to_top() else: for w in self.windows: w.hide() def close(self): for window in self.windows: window.close() def new_window(self, path, uri=None): # Use a signal as can be called from daemon thread self.app.new_window_signal.emit(path, uri) def show_network_dialog(self, parent): if not self.daemon.network: parent.show_warning(_('You are using Electrum in offline mode; restart Electrum if you want to get connected'), title=_('Offline')) return if self.nd: self.nd.on_update() self.nd.show() self.nd.raise_() return self.nd = NetworkDialog(self.daemon.network, self.config, self.network_updated_signal_obj) self.nd.show() def create_window_for_wallet(self, wallet): w = ElectrumWindow(self, wallet) self.windows.append(w) self.build_tray_menu() # FIXME: Remove in favour of the load_wallet hook run_hook('on_new_window', w) return w def start_new_window(self, path, uri, app_is_starting=False): '''Raises the window for the wallet if it is open. Otherwise opens the wallet and creates a new window for it''' try: wallet = self.daemon.load_wallet(path, None) except BaseException as e: traceback.print_exc(file=sys.stdout) d = QMessageBox(QMessageBox.Warning, _('Error'), _('Cannot load wallet') + ' (1):\n' + str(e)) d.exec_() if app_is_starting: # do not return so that the wizard can appear wallet = None else: return if not wallet: storage = WalletStorage(path, manual_upgrades=True) wizard = InstallWizard(self.config, self.app, self.plugins, storage) try: wallet = wizard.run_and_get_wallet(self.daemon.get_wallet) except UserCancelled: pass except GoBack as e: print_error('[start_new_window] Exception caught (GoBack)', e) except (WalletFileException, BitcoinException) as e: traceback.print_exc(file=sys.stderr) d = QMessageBox(QMessageBox.Warning, _('Error'), _('Cannot load wallet') + ' (2):\n' + str(e)) d.exec_() return finally: wizard.terminate() if not wallet: return if not self.daemon.get_wallet(wallet.storage.path): # wallet was not in memory wallet.start_threads(self.daemon.network) self.daemon.add_wallet(wallet) try: for w in self.windows: if w.wallet.storage.path == wallet.storage.path: w.bring_to_top() return w = self.create_window_for_wallet(wallet) except BaseException as e: traceback.print_exc(file=sys.stdout) d = QMessageBox(QMessageBox.Warning, _('Error'), _('Cannot create window for wallet') + ':\n' + str(e)) d.exec_() return if uri: w.pay_to_URI(uri) w.bring_to_top() w.setWindowState(w.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) # this will activate the window w.activateWindow() return w def close_window(self, window): self.windows.remove(window) self.build_tray_menu() # save wallet path of last open window if not self.windows: self.config.save_last_wallet(window.wallet) run_hook('on_close_window', window) def init_network(self): # Show network dialog if config does not exist if self.daemon.network: if self.config.get('auto_connect') is None: wizard = InstallWizard(self.config, self.app, self.plugins, None) wizard.init_network(self.daemon.network) wizard.terminate() def main(self): try: self.init_network() except UserCancelled: return except GoBack: return except BaseException as e: traceback.print_exc(file=sys.stdout) return self.timer.start() self.config.open_last_wallet() path = self.config.get_wallet_path() if not self.start_new_window(path, self.config.get('url'), app_is_starting=True): return signal.signal(signal.SIGINT, lambda *args: self.app.quit()) def quit_after_last_window(): # on some platforms, not only does exec_ not return but not even # aboutToQuit is emitted (but following this, it should be emitted) if self.app.quitOnLastWindowClosed(): self.app.quit() self.app.lastWindowClosed.connect(quit_after_last_window) def clean_up(): # Shut down the timer cleanly self.timer.stop() # clipboard persistence. see http://www.mail-archive.com/[email protected]/msg17328.html event = QtCore.QEvent(QtCore.QEvent.Clipboard) self.app.sendEvent(self.app.clipboard(), event) self.tray.hide() self.app.aboutToQuit.connect(clean_up) # main loop self.app.exec_()
class ElectrumGui: def __init__(self, config, daemon, plugins): set_language(config.get('language')) # 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)]) QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) if hasattr(QtCore.Qt, "AA_ShareOpenGLContexts"): QtCore.QCoreApplication.setAttribute( QtCore.Qt.AA_ShareOpenGLContexts) if hasattr(QGuiApplication, 'setDesktopFileName'): QGuiApplication.setDesktopFileName('electrum-sv.desktop') self.config = config self.daemon = daemon self.plugins = plugins self.windows = [] self.efilter = OpenFileEventFilter(self.windows) self.app = QElectrumApplication(sys.argv) self.app.installEventFilter(self.efilter) # timer self.timer = QTimer(self.app) self.timer.setSingleShot(False) self.timer.setInterval(500) # msec self.nd = None self.exception_hook = None # init tray self.dark_icon = self.config.get("dark_icon", False) self.tray = QSystemTrayIcon(self.tray_icon(), None) self.tray.setToolTip('ElectrumSV') self.tray.activated.connect(self.tray_activated) self.build_tray_menu() self.tray.show() self.app.new_window_signal.connect(self.start_new_window) run_hook('init_qt', self) ColorScheme.update_from_widget(QWidget()) def build_tray_menu(self): # Avoid immediate GC of old menu when window closed via its action if self.tray.contextMenu() is None: m = QMenu() self.tray.setContextMenu(m) else: m = self.tray.contextMenu() m.clear() for window in self.windows: submenu = m.addMenu(window.wallet.basename()) submenu.addAction(_("Show/Hide"), window.show_or_hide) submenu.addAction(_("Close"), window.close) m.addAction(_("Dark/Light"), self.toggle_tray_icon) m.addSeparator() m.addAction(_("Exit ElectrumSV"), self.close) self.tray.setContextMenu(m) def tray_icon(self): if self.dark_icon: return QIcon(':icons/electrumsv_dark_icon.png') else: return QIcon(':icons/electrumsv_light_icon.png') def toggle_tray_icon(self): self.dark_icon = not self.dark_icon self.config.set_key("dark_icon", self.dark_icon, True) self.tray.setIcon(self.tray_icon()) def tray_activated(self, reason): if reason == QSystemTrayIcon.DoubleClick: if all([w.is_hidden() for w in self.windows]): for w in self.windows: w.bring_to_top() else: for w in self.windows: w.hide() def close(self): for window in self.windows: window.close() def new_window(self, path, uri=None): # Use a signal as can be called from daemon thread self.app.new_window_signal.emit(path, uri) def show_network_dialog(self, parent): if not self.daemon.network: parent.show_warning(_( 'You are using ElectrumSV in offline mode; restart ' 'ElectrumSV if you want to get connected'), title=_('Offline')) return if self.nd: self.nd.on_update() self.nd.show() self.nd.raise_() return self.nd = NetworkDialog(self.daemon.network, self.config) self.nd.show() def create_window_for_wallet(self, wallet): w = ElectrumWindow(self, wallet) self.windows.append(w) self.build_tray_menu() # FIXME: Remove in favour of the load_wallet hook run_hook('on_new_window', w) return w def start_new_window(self, path, uri): '''Raises the window for the wallet if it is open. Otherwise opens the wallet and creates a new window for it.''' for w in self.windows: if w.wallet.storage.path == path: w.bring_to_top() break else: try: wallet = self.daemon.load_wallet(path, None) if not wallet: storage = WalletStorage(path, manual_upgrades=True) wizard = InstallWizard(self.config, self.app, self.plugins, storage) try: wallet = wizard.run_and_get_wallet() except UserCancelled: pass except GoBack as e: logging.error( '[start_new_window] Exception caught (GoBack) %s', e) finally: wizard.terminate() if not wallet: return wallet.start_threads(self.daemon.network) self.daemon.add_wallet(wallet) except BaseException as e: logging.exception("") if '2fa' in str(e): d = QMessageBox(QMessageBox.Warning, _('Error'), '2FA wallets are not unsupported.') d.exec_() else: d = QMessageBox(QMessageBox.Warning, _('Error'), 'Cannot load wallet:\n' + str(e)) d.exec_() return w = self.create_window_for_wallet(wallet) if uri: w.pay_to_URI(uri) w.bring_to_top() w.setWindowState(w.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) # this will activate the window w.activateWindow() return w def close_window(self, window): self.windows.remove(window) self.build_tray_menu() # save wallet path of last open window if not self.windows: self.config.save_last_wallet(window.wallet) run_hook('on_close_window', window) def maybe_choose_server(self): # Show network dialog if config does not exist if self.daemon.network and self.config.get('auto_connect') is None: try: wizard = InstallWizard(self.config, self.app, self.plugins, None) wizard.init_network(self.daemon.network) wizard.terminate() except Exception as e: if not isinstance(e, (UserCancelled, GoBack)): logging.exception("") self.app.quit() def event_loop_started(self): if self.config.get("show_crash_reporter", default=True): self.exception_hook = Exception_Hook(self.app) self.timer.start() signal.signal(signal.SIGINT, lambda *args: self.app.quit()) self.maybe_choose_server() self.config.open_last_wallet() path = self.config.get_wallet_path() if not self.start_new_window(path, self.config.get('url')): self.app.quit() def main(self): QTimer.singleShot(0, self.event_loop_started) self.app.exec_() # Shut down the timer cleanly self.timer.stop() # clipboard persistence # see http://www.mail-archive.com/[email protected]/msg17328.html event = QtCore.QEvent(QtCore.QEvent.Clipboard) self.app.sendEvent(self.app.clipboard(), event) self.tray.hide()