Browse Source

qml: initial plugin support, with labelsync mostly implemented

patch-4
Sander van Grieken 2 years ago
parent
commit
32a81d8ee7
  1. 2
      electrum/gui/qml/__init__.py
  2. 358
      electrum/gui/qml/components/Preferences.qml
  3. 25
      electrum/gui/qml/qeapp.py
  4. 2
      electrum/gui/qml/qedaemon.py
  5. 1
      electrum/gui/qml/qewallet.py
  6. 45
      electrum/plugins/labels/Labels.qml
  7. 2
      electrum/plugins/labels/__init__.py
  8. 138
      electrum/plugins/labels/qml.py
  9. 4
      electrum/plugins/qml_test/qml.py

2
electrum/gui/qml/__init__.py

@ -63,7 +63,7 @@ class ElectrumGui(Logger):
self.gui_thread = threading.current_thread()
self.plugins = plugins
self.app = ElectrumQmlApplication(sys.argv, config, daemon)
self.app = ElectrumQmlApplication(sys.argv, config, daemon, plugins)
# timer
self.timer = QTimer(self.app)
self.timer.setSingleShot(False)

358
electrum/gui/qml/components/Preferences.qml

@ -1,6 +1,6 @@
import QtQuick 2.6
import QtQuick.Layouts 1.0
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.3
import QtQuick.Controls.Material 2.0
import org.electrum 1.0
@ -12,175 +12,212 @@ Pane {
property string title: qsTr("Preferences")
padding: 0
property var _baseunits: ['BTC','mBTC','bits','sat']
Flickable {
ColumnLayout {
anchors.fill: parent
contentHeight: rootLayout.height
interactive: height < contentHeight
GridLayout {
id: rootLayout
columns: 2
width: parent.width
Label {
text: qsTr('Language')
TabBar {
id: tabbar
Layout.fillWidth: true
currentIndex: swipeview.currentIndex
TabButton {
text: qsTr('Preferences')
font.pixelSize: constants.fontSizeLarge
}
ElComboBox {
id: language
enabled: false
TabButton {
text: qsTr('Plugins')
font.pixelSize: constants.fontSizeLarge
}
}
Label {
text: qsTr('Base unit')
}
SwipeView {
id: swipeview
ElComboBox {
id: baseUnit
model: _baseunits
onCurrentValueChanged: {
if (activeFocus)
Config.baseUnit = currentValue
}
}
Layout.fillHeight: true
Layout.fillWidth: true
currentIndex: tabbar.currentIndex
Switch {
id: thousands
Layout.columnSpan: 2
text: qsTr('Add thousands separators to bitcoin amounts')
onCheckedChanged: {
if (activeFocus)
Config.thousandsSeparator = checked
}
}
Flickable {
contentHeight: prefsPane.height
interactive: height < contentHeight
clip: true
Switch {
id: checkSoftware
Layout.columnSpan: 2
text: qsTr('Automatically check for software updates')
enabled: false
}
Pane {
id: prefsPane
GridLayout {
columns: 2
width: parent.width
Switch {
id: fiatEnable
text: qsTr('Fiat Currency')
onCheckedChanged: {
if (activeFocus)
Daemon.fx.enabled = checked
}
}
Label {
text: qsTr('Language')
}
ElComboBox {
id: currencies
model: Daemon.fx.currencies
enabled: Daemon.fx.enabled
onCurrentValueChanged: {
if (activeFocus)
Daemon.fx.fiatCurrency = currentValue
}
}
ElComboBox {
id: language
enabled: false
}
Switch {
id: historicRates
text: qsTr('Historic rates')
enabled: Daemon.fx.enabled
Layout.columnSpan: 2
onCheckedChanged: {
if (activeFocus)
Daemon.fx.historicRates = checked
}
}
Label {
text: qsTr('Base unit')
}
Label {
text: qsTr('Source')
enabled: Daemon.fx.enabled
}
ElComboBox {
id: baseUnit
model: _baseunits
onCurrentValueChanged: {
if (activeFocus)
Config.baseUnit = currentValue
}
}
ElComboBox {
id: rateSources
enabled: Daemon.fx.enabled
model: Daemon.fx.rateSources
onModelChanged: {
currentIndex = rateSources.indexOfValue(Daemon.fx.rateSource)
}
onCurrentValueChanged: {
if (activeFocus)
Daemon.fx.rateSource = currentValue
}
}
Switch {
id: thousands
Layout.columnSpan: 2
text: qsTr('Add thousands separators to bitcoin amounts')
onCheckedChanged: {
if (activeFocus)
Config.thousandsSeparator = checked
}
}
Switch {
id: spendUnconfirmed
text: qsTr('Spend unconfirmed')
Layout.columnSpan: 2
onCheckedChanged: {
if (activeFocus)
Config.spendUnconfirmed = checked
}
}
Switch {
id: checkSoftware
Layout.columnSpan: 2
text: qsTr('Automatically check for software updates')
enabled: false
}
Label {
text: qsTr('PIN')
}
Switch {
id: fiatEnable
text: qsTr('Fiat Currency')
onCheckedChanged: {
if (activeFocus)
Daemon.fx.enabled = checked
}
}
RowLayout {
Label {
text: Config.pinCode == '' ? qsTr('Off'): qsTr('On')
color: Material.accentColor
Layout.rightMargin: constants.paddingMedium
}
Button {
text: qsTr('Enable')
visible: Config.pinCode == ''
onClicked: {
var dialog = pinSetup.createObject(preferences, {mode: 'enter'})
dialog.accepted.connect(function() {
Config.pinCode = dialog.pincode
dialog.close()
})
dialog.open()
}
}
Button {
text: qsTr('Change')
visible: Config.pinCode != ''
onClicked: {
var dialog = pinSetup.createObject(preferences, {mode: 'change', pincode: Config.pinCode})
dialog.accepted.connect(function() {
Config.pinCode = dialog.pincode
dialog.close()
})
dialog.open()
}
}
Button {
text: qsTr('Remove')
visible: Config.pinCode != ''
onClicked: {
Config.pinCode = ''
}
}
}
ElComboBox {
id: currencies
model: Daemon.fx.currencies
enabled: Daemon.fx.enabled
onCurrentValueChanged: {
if (activeFocus)
Daemon.fx.fiatCurrency = currentValue
}
}
Label {
text: qsTr('Lightning Routing')
}
Switch {
id: historicRates
text: qsTr('Historic rates')
enabled: Daemon.fx.enabled
Layout.columnSpan: 2
onCheckedChanged: {
if (activeFocus)
Daemon.fx.historicRates = checked
}
}
Label {
text: qsTr('Source')
enabled: Daemon.fx.enabled
}
ElComboBox {
id: rateSources
enabled: Daemon.fx.enabled
model: Daemon.fx.rateSources
onModelChanged: {
currentIndex = rateSources.indexOfValue(Daemon.fx.rateSource)
}
onCurrentValueChanged: {
if (activeFocus)
Daemon.fx.rateSource = currentValue
}
}
Switch {
id: spendUnconfirmed
text: qsTr('Spend unconfirmed')
Layout.columnSpan: 2
onCheckedChanged: {
if (activeFocus)
Config.spendUnconfirmed = checked
}
}
Label {
text: qsTr('PIN')
}
RowLayout {
Label {
text: Config.pinCode == '' ? qsTr('Off'): qsTr('On')
color: Material.accentColor
Layout.rightMargin: constants.paddingMedium
}
Button {
text: qsTr('Enable')
visible: Config.pinCode == ''
onClicked: {
var dialog = pinSetup.createObject(preferences, {mode: 'enter'})
dialog.accepted.connect(function() {
Config.pinCode = dialog.pincode
dialog.close()
})
dialog.open()
}
}
Button {
text: qsTr('Change')
visible: Config.pinCode != ''
onClicked: {
var dialog = pinSetup.createObject(preferences, {mode: 'change', pincode: Config.pinCode})
dialog.accepted.connect(function() {
Config.pinCode = dialog.pincode
dialog.close()
})
dialog.open()
}
}
Button {
text: qsTr('Remove')
visible: Config.pinCode != ''
onClicked: {
Config.pinCode = ''
}
}
}
Label {
text: qsTr('Lightning Routing')
}
ElComboBox {
id: lnRoutingType
enabled: Daemon.currentWallet && Daemon.currentWallet.isLightning
ElComboBox {
id: lnRoutingType
enabled: Daemon.currentWallet && Daemon.currentWallet.isLightning
valueRole: 'key'
textRole: 'label'
model: ListModel {
ListElement { key: 'gossip'; label: qsTr('Gossip') }
ListElement { key: 'trampoline'; label: qsTr('Trampoline') }
}
onCurrentValueChanged: {
if (activeFocus)
Config.useGossip = currentValue == 'gossip'
}
}
}
valueRole: 'key'
textRole: 'label'
model: ListModel {
ListElement { key: 'gossip'; label: qsTr('Gossip') }
ListElement { key: 'trampoline'; label: qsTr('Trampoline') }
}
onCurrentValueChanged: {
if (activeFocus)
Config.useGossip = currentValue == 'gossip'
}
Pane {
ColumnLayout {
id: pluginsRootLayout
}
}
}
@ -192,6 +229,19 @@ Pane {
Pin {}
}
Component {
id: pluginHeader
RowLayout {
property QtObject plugin
Switch {
checked: plugin.pluginEnabled
}
Label {
text: plugin.name
}
}
}
Component.onCompleted: {
baseUnit.currentIndex = _baseunits.indexOf(Config.baseUnit)
thousands.checked = Config.thousandsSeparator
@ -201,5 +251,15 @@ Pane {
fiatEnable.checked = Daemon.fx.enabled
spendUnconfirmed.checked = Config.spendUnconfirmed
lnRoutingType.currentIndex = Config.useGossip ? 0 : 1
var labelsPlugin = AppController.plugin('labels')
if (labelsPlugin) {
pluginHeader.createObject(pluginsRootLayout, { plugin: labelsPlugin })
// console.log(Qt.resolvedUrl(labelsPlugin.settingsComponent()))
if (labelsPlugin.settingsComponent()) {
var component = Qt.createComponent(Qt.resolvedUrl(labelsPlugin.settingsComponent()))
component.createObject(pluginsRootLayout, {plugin: labelsPlugin})
}
}
}
}

25
electrum/gui/qml/qeapp.py

@ -34,11 +34,12 @@ notification = None
class QEAppController(QObject):
userNotify = pyqtSignal(str)
def __init__(self, qedaemon):
def __init__(self, qedaemon, plugins):
super().__init__()
self.logger = get_logger(__name__)
self._qedaemon = qedaemon
self._plugins = plugins
# set up notification queue and notification_timer
self.user_notification_queue = queue.Queue()
@ -131,11 +132,22 @@ class QEAppController(QObject):
def clipboardToText(self):
return QGuiApplication.clipboard().text()
@pyqtSlot(str, result=QObject)
def plugin(self, plugin_name):
self.logger.warning(f'now {self._plugins.count()} plugins loaded')
plugin = self._plugins.get(plugin_name)
self.logger.debug(f'plugin with name {plugin_name} is {str(type(plugin))}')
if plugin:
return plugin.so
else:
self.logger.debug('None!')
return None
class ElectrumQmlApplication(QGuiApplication):
_valid = True
def __init__(self, args, config, daemon):
def __init__(self, args, config, daemon, plugins):
super().__init__(args)
self.logger = get_logger(__name__)
@ -162,7 +174,6 @@ class ElectrumQmlApplication(QGuiApplication):
qmlRegisterUncreatableType(QEAmount, 'org.electrum', 1, 0, 'Amount', 'Amount can only be used as property')
self.engine = QQmlApplicationEngine(parent=self)
self.engine.addImportPath('./qml')
screensize = self.primaryScreen().size()
@ -181,13 +192,13 @@ class ElectrumQmlApplication(QGuiApplication):
self.context = self.engine.rootContext()
self._qeconfig = QEConfig(config)
self._qenetwork = QENetwork(daemon.network, self._qeconfig)
self._qedaemon = QEDaemon(daemon)
self._appController = QEAppController(self._qedaemon)
self.daemon = QEDaemon(daemon)
self.appController = QEAppController(self.daemon, plugins)
self._maxAmount = QEAmount(is_max=True)
self.context.setContextProperty('AppController', self._appController)
self.context.setContextProperty('AppController', self.appController)
self.context.setContextProperty('Config', self._qeconfig)
self.context.setContextProperty('Network', self._qenetwork)
self.context.setContextProperty('Daemon', self._qedaemon)
self.context.setContextProperty('Daemon', self.daemon)
self.context.setContextProperty('FixedFont', self.fixedFont)
self.context.setContextProperty('MAX', self._maxAmount)
self.context.setContextProperty('QRIP', self.qr_ip_h)

2
electrum/gui/qml/qedaemon.py

@ -7,6 +7,7 @@ from electrum.i18n import _
from electrum.logging import get_logger
from electrum.util import WalletFileException, standardize_path
from electrum.wallet import Abstract_Wallet
from electrum.plugin import run_hook
from electrum.lnchannel import ChannelState
from .auth import AuthMixin, auth_protect
@ -179,6 +180,7 @@ class QEDaemon(AuthMixin, QObject):
self._logger.info('use single password disabled by config')
self.daemon.config.save_last_wallet(wallet)
run_hook('load_wallet', wallet)
else:
self._logger.info('could not open wallet')
self.walletOpenError.emit('could not open wallet')

1
electrum/gui/qml/qewallet.py

@ -62,6 +62,7 @@ class QEWallet(AuthMixin, QObject, QtEventListener):
transactionSigned = pyqtSignal([str], arguments=['txid'])
#broadcastSucceeded = pyqtSignal([str], arguments=['txid'])
broadcastFailed = pyqtSignal([str,str,str], arguments=['txid','code','reason'])
labelsUpdated = pyqtSignal()
_network_signal = pyqtSignal(str, object)

45
electrum/plugins/labels/Labels.qml

@ -0,0 +1,45 @@
import QtQuick 2.6
import QtQuick.Layouts 1.0
import QtQuick.Controls 2.14
import QtQuick.Controls.Material 2.0
import org.electrum 1.0
//import "controls"
Item {
width: parent.width
height: rootLayout.height
property QtObject plugin
RowLayout {
id: rootLayout
Button {
text: 'Force upload'
enabled: !plugin.busy
onClicked: plugin.upload()
}
Button {
text: 'Force download'
enabled: !plugin.busy
onClicked: plugin.download()
}
}
Connections {
target: plugin
function onUploadSuccess() {
console.log('upload success')
}
function onUploadFailed() {
console.log('upload failed')
}
function onDownloadSuccess() {
console.log('download success')
}
function onDownloadFailed() {
console.log('download failed')
}
}
}

2
electrum/plugins/labels/__init__.py

@ -5,5 +5,5 @@ description = ' '.join([
_("Save your wallet labels on a remote server, and synchronize them across multiple devices where you use Electrum."),
_("Labels, transactions IDs and addresses are encrypted before they are sent to the remote server.")
])
available_for = ['qt', 'kivy', 'cmdline']
available_for = ['qt', 'qml', 'kivy', 'cmdline']

138
electrum/plugins/labels/qml.py

@ -0,0 +1,138 @@
import threading
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
from electrum.i18n import _
from electrum.plugin import hook
from electrum.gui.qml.qewallet import QEWallet
from .labels import LabelsPlugin
class Plugin(LabelsPlugin):
class QSignalObject(QObject):
pluginChanged = pyqtSignal()
pluginEnabledChanged = pyqtSignal()
labelsChanged = pyqtSignal()
busyChanged = pyqtSignal()
uploadSuccess = pyqtSignal()
uploadFailed = pyqtSignal()
downloadSuccess = pyqtSignal()
downloadFailed = pyqtSignal()
_busy = False
def __init__(self, plugin, parent = None):
super().__init__(parent)
self.plugin = plugin
@pyqtProperty(str, notify=pluginChanged)
def name(self): return _('Labels Plugin')
@pyqtProperty(bool, notify=busyChanged)
def busy(self): return self._busy
@pyqtProperty(bool, notify=pluginEnabledChanged)
def pluginEnabled(self): return self.plugin.is_enabled()
@pyqtSlot(result=str)
def settingsComponent(self): return '../../../plugins/labels/Labels.qml'
@pyqtSlot()
def upload(self):
assert self.plugin
self._busy = True
self.busyChanged.emit()
self.plugin.push_async()
def upload_finished(self, result):
if result:
self.uploadSuccess.emit()
else:
self.uploadFailed.emit()
self._busy = False
self.busyChanged.emit()
@pyqtSlot()
def download(self):
assert self.plugin
self._busy = True
self.busyChanged.emit()
self.plugin.pull_async()
def download_finished(self, result):
if result:
self.downloadSuccess.emit()
else:
self.downloadFailed.emit()
self._busy = False
self.busyChanged.emit()
def __init__(self, *args):
LabelsPlugin.__init__(self, *args)
@hook
def load_wallet(self, wallet):
self.logger.info(f'load_wallet hook for wallet {str(type(wallet))}')
self.start_wallet(wallet)
def push_async(self):
if not self._app.daemon.currentWallet:
self.logger.error('No current wallet')
self.so.download_finished(False)
return
wallet = self._app.daemon.currentWallet.wallet
def push_thread(wallet):
try:
self.push(wallet)
self.so.upload_finished(True)
self._app.appController.userNotify.emit(_('Labels uploaded'))
except Exception as e:
self.logger.error(repr(e))
self.so.upload_finished(False)
self._app.appController.userNotify.emit(repr(e))
threading.Thread(target=push_thread,args=[wallet]).start()
def pull_async(self):
if not self._app.daemon.currentWallet:
self.logger.error('No current wallet')
self.so.download_finished(False)
return
wallet = self._app.daemon.currentWallet.wallet
def pull_thread(wallet):
try:
self.pull(wallet, True)
self.so.download_finished(True)
self._app.appController.userNotify.emit(_('Labels downloaded'))
except Exception as e:
self.logger.error(repr(e))
self.so.download_finished(False)
self._app.appController.userNotify.emit(repr(e))
threading.Thread(target=pull_thread,args=[wallet]).start()
def on_pulled(self, wallet):
self.logger.info('on pulled')
_wallet = QEWallet.getInstanceFor(wallet)
self.logger.debug('wallet ' + ('found' if _wallet else 'not found'))
if _wallet:
_wallet.labelsUpdated.emit()
@hook
def init_qml(self, gui: 'ElectrumGui'):
self.logger.debug('init_qml hook called')
self.logger.debug(f'gui={str(type(gui))}')
self._app = gui.app
# important: QSignalObject needs to be parented, as keeping a ref
# in the plugin is not enough to avoid gc
self.so = Plugin.QSignalObject(self, self._app)

4
electrum/plugins/qml_test/qml.py

@ -10,8 +10,6 @@ class Plugin(BasePlugin):
def __init__(self, parent, config, name):
BasePlugin.__init__(self, parent, config, name)
_logger = get_logger(__name__)
@hook
def init_qml(self, gui: 'ElectrumGui'):
self._logger.debug('init_qml hook called')
self.logger.debug('init_qml hook called')

Loading…
Cancel
Save