diff --git a/electrum/gui/qml/__init__.py b/electrum/gui/qml/__init__.py new file mode 100644 index 000000000..581ff0bda --- /dev/null +++ b/electrum/gui/qml/__init__.py @@ -0,0 +1,100 @@ +import os +import signal +import sys +import traceback +import threading +from typing import Optional, TYPE_CHECKING, List + +try: + import PyQt5 +except Exception: + sys.exit("Error: Could not import PyQt5 on Linux systems, you may try 'sudo apt-get install python3-pyqt5'") + +try: + import PyQt5.QtQml +except Exception: + sys.exit("Error: Could not import PyQt5.QtQml on Linux systems, you may try 'sudo apt-get install python3-pyqt5.qtquick'") + +from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject, QUrl +from PyQt5.QtGui import QGuiApplication +from PyQt5.QtQml import qmlRegisterType, QQmlComponent, QQmlApplicationEngine +from PyQt5.QtQuick import QQuickView +import PyQt5.QtCore as QtCore +import PyQt5.QtQml as QtQml + +from electrum.i18n import _, set_language +from electrum.plugin import run_hook +from electrum.base_wizard import GoBack +from electrum.util import (UserCancelled, profiler, + WalletFileException, BitcoinException, get_new_wallet_name) +from electrum.wallet import Wallet, Abstract_Wallet +from electrum.wallet_db import WalletDB +from electrum.logging import Logger + +if TYPE_CHECKING: + from electrum.daemon import Daemon + from electrum.simple_config import SimpleConfig + from electrum.plugin import Plugins + +from .qenetwork import QENetwork, QEDaemon, QEWalletListModel +from .qewallet import * +from .qeqr import QEQR + +class ElectrumQmlApplication(QGuiApplication): + def __init__(self, args, daemon): + super().__init__(args) + + qmlRegisterType(QEWalletListModel, 'QElectrum', 1, 0, 'QEWalletListModel') + qmlRegisterType(QEWallet, 'QElectrum', 1, 0, 'QEWallet') + + self.engine = QQmlApplicationEngine(parent=self) + self.context = self.engine.rootContext() + self.qen = QENetwork(daemon.network) + self.context.setContextProperty('Network', self.qen) + self.qed = QEDaemon(daemon) + self.context.setContextProperty('Daemon', self.qed) + self.qeqr = QEQR() + self.context.setContextProperty('QR', self.qeqr) + self.engine.load(QUrl('electrum/gui/qml/components/main.qml')) + +class ElectrumGui(Logger): + + @profiler + def __init__(self, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins'): + # TODO set_language(config.get('language', get_default_language())) + Logger.__init__(self) + self.logger.info(f"Qml 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.desktop') + self.gui_thread = threading.current_thread() + self.config = config + self.daemon = daemon + self.plugins = plugins + self.app = ElectrumQmlApplication(sys.argv, self.daemon) + + # TODO when plugin support. run_hook('init_qml', self) + + 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() +# if self.watchtower_dialog: +# self.watchtower_dialog.close() + self.app.quit() + + def main(self): + self.app.exec_() + + def stop(self): + self.logger.info('closing GUI') + self.app.quit() diff --git a/electrum/gui/qml/qenetwork.py b/electrum/gui/qml/qenetwork.py new file mode 100644 index 000000000..312021d9c --- /dev/null +++ b/electrum/gui/qml/qenetwork.py @@ -0,0 +1,128 @@ +from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl +from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex + +from electrum.util import register_callback +from electrum.logging import get_logger +from electrum.wallet import Wallet, Abstract_Wallet + +from .qewallet import QEWallet + +class QENetwork(QObject): + def __init__(self, network, parent=None): + super().__init__(parent) + self.network = network + register_callback(self.on_network_updated, ['network_updated']) + register_callback(self.on_blockchain_updated, ['blockchain_updated']) + register_callback(self.on_default_server_changed, ['default_server_changed']) + register_callback(self.on_proxy_set, ['proxy_set']) + register_callback(self.on_status, ['status']) + + _logger = get_logger(__name__) + + network_updated = pyqtSignal() + blockchain_updated = pyqtSignal() + default_server_changed = pyqtSignal() + proxy_set = pyqtSignal() + status_updated = pyqtSignal() + + _num_updates = 0 + _server = "" + _height = 0 + _status = "" + + def on_network_updated(self, event, *args): + self._num_updates = self._num_updates + 1 + self.network_updated.emit() + + def on_blockchain_updated(self, event, *args): + self._logger.info('chainupdate: ' + str(event) + str(args)) + self._height = self.network.get_local_height() + self.blockchain_updated.emit() + + def on_default_server_changed(self, event, *args): + netparams = self.network.get_parameters() + self._server = str(netparams.server) + self.default_server_changed.emit() + + def on_proxy_set(self, event, *args): + self._logger.info('proxy set') + self.proxy_set.emit() + + def on_status(self, event, *args): + self._logger.info('status updated') + self._status = self.network.connection_status + self.status_updated.emit() + + @pyqtProperty(int,notify=network_updated) + def updates(self): + return self._num_updates + + @pyqtProperty(int,notify=blockchain_updated) + def height(self): + return self._height + + @pyqtProperty('QString',notify=default_server_changed) + def server(self): + return self._server + + @pyqtProperty('QString',notify=status_updated) + def status(self): + return self._status + +class QEWalletListModel(QAbstractListModel): + def __init__(self, parent=None): + QAbstractListModel.__init__(self, parent) + self.wallets = [] + + def rowCount(self, index): + return len(self.wallets) + + def data(self, index, role): + if role == Qt.DisplayRole: + return self.wallets[index.row()].basename() + + def add_wallet(self, wallet: Abstract_Wallet = None): + if wallet == None: + return + self.beginInsertRows(QModelIndex(), len(self.wallets), len(self.wallets)); + self.wallets.append(wallet); + self.endInsertRows(); + + +class QEDaemon(QObject): + def __init__(self, daemon, parent=None): + super().__init__(parent) + self.daemon = daemon + + _logger = get_logger(__name__) + _wallet = '' + _loaded_wallets = QEWalletListModel() + + wallet_loaded = pyqtSignal() + + @pyqtSlot() + def load_wallet(self, path=None, password=None): + self._logger.info(str(self.daemon.get_wallets())) + if path == None: + path = self.daemon.config.get('recently_open')[0] + wallet = self.daemon.load_wallet(path, password) + if wallet != None: + self._loaded_wallets.add_wallet(wallet) + self._wallet = wallet.basename() + self._current_wallet = QEWallet(wallet) + self.wallet_loaded.emit() + self._logger.info(str(self.daemon.get_wallets())) + else: + self._logger.info('fail open wallet') + + @pyqtProperty('QString',notify=wallet_loaded) + def walletName(self): + return self._wallet + + @pyqtProperty(QEWalletListModel) + def activeWallets(self): + return self._loaded_wallets + + @pyqtProperty(QEWallet,notify=wallet_loaded) + def currentWallet(self): + return self._current_wallet diff --git a/electrum/gui/qml/qeqr.py b/electrum/gui/qml/qeqr.py new file mode 100644 index 000000000..0df046869 --- /dev/null +++ b/electrum/gui/qml/qeqr.py @@ -0,0 +1,58 @@ +from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl + +from electrum.logging import get_logger + +from PIL import Image +from ctypes import * + +class QEQR(QObject): + def __init__(self, text=None, parent=None): + super().__init__(parent) + self._text = text + + _logger = get_logger(__name__) + scan_ready_changed = pyqtSignal() + + _ready = True + + @pyqtSlot('QImage') + def scanImage(self, image=None): + if not self._ready: + self._logger.warning("Already processing an image. Check 'ready' property before calling scanImage") + return + self._ready = False + self.scan_ready_changed.emit() + + pilimage = self.convertToPILImage(image) + parseQR(pilimage) + + self._ready = True + + def logImageStats(self, image): + self._logger.info('width: ' + str(image.width())) + self._logger.info('height: ' + str(image.height())) + self._logger.info('depth: ' + str(image.depth())) + self._logger.info('format: ' + str(image.format())) + + def convertToPILImage(self, image) -> Image: + self.logImageStats(image) + + rawimage = image.constBits() + # assumption: pixels are 32 bits ARGB + numbytes = image.width() * image.height() * 4 + + self._logger.info(type(rawimage)) + buf = bytearray(numbytes) + c_buf = (c_byte * numbytes).from_buffer(buf) + memmove(c_buf, c_void_p(rawimage.__int__()), numbytes) + buf2 = bytes(buf) + + return Image.frombytes('RGBA', (image.width(), image.height()), buf2, 'raw') + + def parseQR(self, image): + pass + + @pyqtProperty(bool, notify=scan_ready_changed) + def ready(self): + return self._ready + diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py new file mode 100644 index 000000000..58db1b60d --- /dev/null +++ b/electrum/gui/qml/qewallet.py @@ -0,0 +1,43 @@ +from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl +from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex + +from electrum.util import register_callback +from electrum.logging import get_logger +from electrum.wallet import Wallet, Abstract_Wallet + +class QETransactionsListModel(QAbstractListModel): + def __init__(self, parent=None): + super().__init__(parent) + self.tx_history = [] + + def rowCount(self, index): + return len(self.tx_history) + + def data(self, index, role): + if role == Qt.DisplayRole: + return str(self.tx_history[index.row()]['bc_value']) + + def set_history(self, history): + self.beginInsertRows(QModelIndex(), 0, len(history) - 1) + self.tx_history = history + self.endInsertRows() + +class QEWallet(QObject): + def __init__(self, wallet, parent=None): + super().__init__(parent) + self.wallet = wallet + self.get_history() + + _logger = get_logger(__name__) + + _historyModel = QETransactionsListModel() + + @pyqtProperty(QETransactionsListModel) + def historyModel(self): + return self._historyModel + + def get_history(self): + history = self.wallet.get_detailed_history(show_addresses = True) + txs = history['transactions'] + self._historyModel.set_history(txs) +