diff --git a/electrum/gui/qml/__init__.py b/electrum/gui/qml/__init__.py index 3ca01d4dc..42a4ee40c 100644 --- a/electrum/gui/qml/__init__.py +++ b/electrum/gui/qml/__init__.py @@ -15,7 +15,7 @@ try: 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, pyqtSlot, QObject, QUrl, QLocale, QTimer +from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl, QLocale, QTimer, qInstallMessageHandler from PyQt5.QtGui import QGuiApplication from PyQt5.QtQml import qmlRegisterType, QQmlComponent, QQmlApplicationEngine from PyQt5.QtQuick import QQuickView @@ -36,39 +36,36 @@ if TYPE_CHECKING: from electrum.simple_config import SimpleConfig from electrum.plugin import Plugins +from .qeconfig import QEConfig from .qedaemon import QEDaemon, QEWalletListModel from .qenetwork import QENetwork from .qewallet import QEWallet from .qeqr import QEQR class ElectrumQmlApplication(QGuiApplication): - def __init__(self, args, daemon): + def __init__(self, args, config, daemon): super().__init__(args) - self.logger = get_logger(__name__) + self.logger = get_logger(__name__ + '.engine') - qmlRegisterType(QEWalletListModel, 'Electrum', 1, 0, 'WalletListModel') - qmlRegisterType(QEWallet, 'Electrum', 1, 0, 'Wallet') + qmlRegisterType(QEWalletListModel, 'org.electrum', 1, 0, 'WalletListModel') + qmlRegisterType(QEWallet, 'org.electrum', 1, 0, 'Wallet') self.engine = QQmlApplicationEngine(parent=self) self.engine.addImportPath('./qml') - self.logger.info('importPathList() :') - for i in self.engine.importPathList(): - self.logger.info(i) - - self.logger.info('pluginPathList() :') - for i in self.engine.pluginPathList(): - self.logger.info(i) - self.context = self.engine.rootContext() + self._singletons['config'] = QEConfig(config) self._singletons['network'] = QENetwork(daemon.network) self._singletons['daemon'] = QEDaemon(daemon) self._singletons['qr'] = QEQR() + self.context.setContextProperty('Config', self._singletons['config']) self.context.setContextProperty('Network', self._singletons['network']) self.context.setContextProperty('Daemon', self._singletons['daemon']) self.context.setContextProperty('QR', self._singletons['qr']) + qInstallMessageHandler(self.message_handler) + # get notified whether root QML document loads or not self.engine.objectCreated.connect(self.objectCreated) @@ -82,14 +79,17 @@ class ElectrumQmlApplication(QGuiApplication): self._valid = False self.engine.objectCreated.disconnect(self.objectCreated) + def message_handler(self, line, funct, file): + self.logger.warning(file) + class ElectrumGui(Logger): @profiler def __init__(self, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins'): set_language(config.get('language', self.get_default_language())) Logger.__init__(self) - os.environ['QML_IMPORT_TRACE'] = '1' - os.environ['QT_DEBUG_PLUGINS'] = '1' + #os.environ['QML_IMPORT_TRACE'] = '1' + #os.environ['QT_DEBUG_PLUGINS'] = '1' self.logger.info(f"Qml GUI starting up... Qt={QtCore.QT_VERSION_STR}, PyQt={QtCore.PYQT_VERSION_STR}") self.logger.info("CWD=%s" % os.getcwd()) @@ -112,7 +112,7 @@ class ElectrumGui(Logger): self.config = config self.daemon = daemon self.plugins = plugins - self.app = ElectrumQmlApplication(sys.argv, self.daemon) + self.app = ElectrumQmlApplication(sys.argv, self.config, self.daemon) # timer self.timer = QTimer(self.app) self.timer.setSingleShot(False) @@ -124,14 +124,6 @@ class ElectrumGui(Logger): self.app.engine.load('electrum/gui/qml/components/main.qml') 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): diff --git a/electrum/gui/qml/components/History.qml b/electrum/gui/qml/components/History.qml index 4579603df..625b13c48 100644 --- a/electrum/gui/qml/components/History.qml +++ b/electrum/gui/qml/components/History.qml @@ -2,7 +2,7 @@ import QtQuick 2.6 import QtQuick.Layouts 1.0 import QtQuick.Controls 2.0 -import Electrum 1.0 +import org.electrum 1.0 Pane { id: rootItem diff --git a/electrum/gui/qml/components/ServerConnectWizard.qml b/electrum/gui/qml/components/ServerConnectWizard.qml new file mode 100644 index 000000000..5562b56e8 --- /dev/null +++ b/electrum/gui/qml/components/ServerConnectWizard.qml @@ -0,0 +1,223 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.0 +import QtQuick.Controls 2.15 +import QtQuick.Controls.Material 2.15 + +Wizard { + id: serverconnectwizard + + title: qsTr('How do you want to connect to a server?') + + enter: null // disable transition + + onAccepted: { + var proxy = wizard_data['proxy'] + if (proxy && proxy['enabled'] == true) { + Network.proxy = proxy + } else { + Network.proxy = {'enabled': false} + } + Config.autoConnect = wizard_data['autoconnect'] + if (!wizard_data['autoconnect']) { + Network.server = wizard_data['server'] + } + } + + Component.onCompleted: { + var start = _loadNextComponent(autoconnect) + start.next.connect(function() {autoconnectDone()}) + } + + function autoconnectDone() { + var page = _loadNextComponent(proxyconfig, wizard_data) + page.next.connect(function() {proxyconfigDone()}) + } + + function proxyconfigDone() { + var page = _loadNextComponent(serverconfig, wizard_data) + } + + property Component autoconnect: Component { + WizardComponent { + valid: true + last: serverconnectgroup.checkedButton.connecttype === 'auto' + + onAccept: { + wizard_data['autoconnect'] = serverconnectgroup.checkedButton.connecttype === 'auto' + } + + ColumnLayout { + anchors.fill: parent + + Text { + text: qsTr('Electrum communicates with remote servers to get information about your transactions and addresses. The servers all fulfill the same purpose only differing in hardware. In most cases you simply want to let Electrum pick one at random. However if you prefer feel free to select a server manually.') + wrapMode: Text.Wrap + Layout.fillWidth: true + color: Material.primaryTextColor + } + + ButtonGroup { + id: serverconnectgroup + } + + RadioButton { + ButtonGroup.group: serverconnectgroup + property string connecttype: 'auto' + text: qsTr('Auto connect') + } + RadioButton { + ButtonGroup.group: serverconnectgroup + property string connecttype: 'manual' + checked: true + text: qsTr('Select servers manually') + } + + } + + } + } + + property Component proxyconfig: Component { + WizardComponent { + valid: true + last: false + + onAccept: { + var p = {} + p['enabled'] = proxy_enabled.checked + if (proxy_enabled.checked) { + var type = proxytype.currentValue.toLowerCase() + if (type == 'tor') + type = 'socks5' + p['mode'] = type + p['host'] = address.text + p['port'] = port.text + p['user'] = username.text + p['password'] = password.text + } + wizard_data['proxy'] = p + } + + ColumnLayout { + anchors.fill: parent + + Text { + text: qsTr('Proxy settings') + wrapMode: Text.Wrap + Layout.fillWidth: true + color: Material.primaryTextColor + } + + CheckBox { + id: proxy_enabled + text: qsTr('Enable Proxy') + } + + ComboBox { + id: proxytype + enabled: proxy_enabled.checked + model: ['TOR', 'SOCKS5', 'SOCKS4'] + onCurrentIndexChanged: { + if (currentIndex == 0) { + address.text = "127.0.0.1" + port.text = "9050" + } + } + } + + GridLayout { + columns: 4 + Layout.fillWidth: true + + Label { + text: qsTr("Address") + enabled: address.enabled + } + + TextField { + id: address + enabled: proxytype.enabled && proxytype.currentIndex > 0 + } + + Label { + text: qsTr("Port") + enabled: port.enabled + } + + TextField { + id: port + enabled: proxytype.enabled && proxytype.currentIndex > 0 + } + + Label { + text: qsTr("Username") + enabled: username.enabled + } + + TextField { + id: username + enabled: proxytype.enabled && proxytype.currentIndex > 0 + } + + Label { + text: qsTr("Password") + enabled: password.enabled + } + + TextField { + id: password + enabled: proxytype.enabled && proxytype.currentIndex > 0 + echoMode: TextInput.Password + } + } + } + + } + } + + property Component serverconfig: Component { + WizardComponent { + valid: true + last: true + + onAccept: { + wizard_data['oneserver'] = !auto_server.checked + wizard_data['server'] = address.text + } + + ColumnLayout { + anchors.fill: parent + + Text { + text: qsTr('Server settings') + wrapMode: Text.Wrap + Layout.fillWidth: true + color: Material.primaryTextColor + } + + CheckBox { + id: auto_server + text: qsTr('Select server automatically') + checked: true + } + + GridLayout { + columns: 2 + Layout.fillWidth: true + + Label { + text: qsTr("Server") + enabled: address.enabled + } + + TextField { + id: address + enabled: !auto_server.checked + } + } + } + + } + } + +} diff --git a/electrum/gui/qml/components/landing.qml b/electrum/gui/qml/components/WalletMainView.qml similarity index 100% rename from electrum/gui/qml/components/landing.qml rename to electrum/gui/qml/components/WalletMainView.qml diff --git a/electrum/gui/qml/components/WizardComponents.qml b/electrum/gui/qml/components/WizardComponents.qml index cccc1fbad..6e22726fd 100644 --- a/electrum/gui/qml/components/WizardComponents.qml +++ b/electrum/gui/qml/components/WizardComponents.qml @@ -6,7 +6,7 @@ Item { property Component walletname: Component { WizardComponent { valid: wallet_name.text.length > 0 - //property alias wallet_name: wallet_name.text + onAccept: { wizard_data['wallet_name'] = wallet_name.text } diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index b23cd8b26..0d8f1ecb9 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -1,5 +1,5 @@ import QtQuick 2.6 -import QtQuick.Controls 2.3 +import QtQuick.Controls 2.15 import QtQuick.Layouts 1.0 import QtQuick.Controls.Material 2.0 @@ -10,6 +10,8 @@ ApplicationWindow { id: app visible: true + + // dimensions ignored on android width: 480 height: 800 @@ -55,7 +57,7 @@ ApplicationWindow Label { id: networkNameLabel text: Network.networkName - color: Material.accentColor //'orange' + color: Material.accentColor font.pointSize: 5 } } @@ -85,7 +87,7 @@ ApplicationWindow id: mainStackView anchors.fill: parent - initialItem: Qt.resolvedUrl('landing.qml') + initialItem: Qt.resolvedUrl('WalletMainView.qml') } Timer { @@ -124,6 +126,22 @@ ApplicationWindow } } + property alias serverConnectWizard: _serverConnectWizard + Component { + id: _serverConnectWizard + ServerConnectWizard { + parent: Overlay.overlay + x: 12 + y: 12 + width: parent.width - 24 + height: parent.height - 24 + + Overlay.modal: Rectangle { + color: "#aa000000" + } + } + } + property alias messageDialog: _messageDialog Component { id: _messageDialog @@ -144,8 +162,17 @@ ApplicationWindow } Component.onCompleted: { - Daemon.load_wallet() + //Daemon.load_wallet() splashTimer.start() + if (!Config.autoConnectDefined) { + var dialog = serverConnectWizard.createObject(app) + // without completed serverConnectWizard we can't start + dialog.rejected.connect(function() { + app.visible = false + Qt.callLater(Qt.quit) + }) + dialog.open() + } } onClosing: { diff --git a/electrum/gui/qml/qeconfig.py b/electrum/gui/qml/qeconfig.py new file mode 100644 index 000000000..d65e22819 --- /dev/null +++ b/electrum/gui/qml/qeconfig.py @@ -0,0 +1,47 @@ +from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject + +from electrum.logging import get_logger + +class QEConfig(QObject): + def __init__(self, config, parent=None): + super().__init__(parent) + self.config = config + + _logger = get_logger(__name__) + + autoConnectChanged = pyqtSignal() + serverStringChanged = pyqtSignal() + manualServerChanged = pyqtSignal() + + @pyqtProperty(bool, notify=autoConnectChanged) + def autoConnect(self): + return self.config.get('auto_connect') + + @autoConnect.setter + def autoConnect(self, auto_connect): + self.config.set_key('auto_connect', auto_connect, True) + self.autoConnectChanged.emit() + + # auto_connect is actually a tri-state, expose the undefined case + @pyqtProperty(bool, notify=autoConnectChanged) + def autoConnectDefined(self): + return self.config.get('auto_connect') is not None + + @pyqtProperty('QString', notify=serverStringChanged) + def serverString(self): + return self.config.get('server') + + @serverString.setter + def serverString(self, server): + self.config.set_key('server', server, True) + self.serverStringChanged.emit() + + @pyqtProperty(bool, notify=manualServerChanged) + def manualServer(self): + return self.config.get('oneserver') + + @manualServer.setter + def manualServer(self, oneserver): + self.config.set_key('oneserver', oneserver, True) + self.manualServerChanged.emit() + diff --git a/electrum/gui/qml/qenetwork.py b/electrum/gui/qml/qenetwork.py index a857fdf1a..11c696c1d 100644 --- a/electrum/gui/qml/qenetwork.py +++ b/electrum/gui/qml/qenetwork.py @@ -3,6 +3,7 @@ from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject from electrum.util import register_callback from electrum.logging import get_logger from electrum import constants +from electrum.interface import ServerAddr class QENetwork(QObject): def __init__(self, network, parent=None): @@ -20,6 +21,7 @@ class QENetwork(QObject): blockchainUpdated = pyqtSignal() defaultServerChanged = pyqtSignal() proxySet = pyqtSignal() + proxyChanged = pyqtSignal() statusUpdated = pyqtSignal() dataChanged = pyqtSignal() # dummy to silence warnings @@ -64,6 +66,17 @@ class QENetwork(QObject): def server(self): return self._server + @server.setter + def server(self, server): + net_params = self.network.get_parameters() + try: + server = ServerAddr.from_str_with_inference(server) + if not server: raise Exception("failed to parse") + except Exception: + return + net_params = net_params._replace(server=server) + self.network.run_from_another_thread(self.network.set_parameters(net_params)) + @pyqtProperty('QString',notify=statusUpdated) def status(self): return self._status @@ -76,3 +89,16 @@ class QENetwork(QObject): def networkName(self): return constants.net.__name__.replace('Bitcoin','') + @pyqtProperty('QVariantMap', notify=proxyChanged) + def proxy(self): + net_params = self.network.get_parameters() + return net_params + + @proxy.setter + def proxy(self, proxy_settings): + net_params = self.network.get_parameters() + if not proxy_settings['enabled']: + proxy_settings = None + net_params = net_params._replace(proxy=proxy_settings) + self.network.run_from_another_thread(self.network.set_parameters(net_params)) + self.proxyChanged.emit()