1
0
Files
electrum/electrum/gui/qml/components/Preferences.qml
f321x f8c44886f4 qml: Preferences: add "Security" section
Adds a separate "Security" section to the qml preferences which contains
security related toggles so the Preferences are less mixed up.
2026-01-21 10:19:29 +01:00

513 lines
21 KiB
QML

import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Controls.Material
import org.electrum 1.0
import "controls"
Pane {
id: preferences
objectName: 'Properties'
property string title: qsTr("Preferences")
padding: 0
property var _baseunits: ['BTC','mBTC','bits','sat']
ColumnLayout {
anchors.fill: parent
Flickable {
Layout.fillHeight: true
Layout.fillWidth: true
contentHeight: prefsPane.height
interactive: height < contentHeight
clip: true
Pane {
id: prefsPane
width: parent.width
GridLayout {
columns: 2
width: parent.width
PrefsHeading {
Layout.columnSpan: 2
text: qsTr('User Interface')
}
Label {
text: qsTr('Language')
}
ElComboBox {
id: language
textRole: 'text'
valueRole: 'value'
model: Config.languagesAvailable
onCurrentValueChanged: {
if (activeFocus) {
if (Config.language != currentValue) {
Config.language = currentValue
var dialog = app.messageDialog.createObject(app, {
text: qsTr('Please restart Electrum to activate the new GUI settings')
})
dialog.open()
}
}
}
}
Label {
text: qsTr('Base unit')
}
ElComboBox {
id: baseUnit
model: _baseunits
onCurrentValueChanged: {
if (activeFocus)
Config.baseUnit = currentValue
}
}
RowLayout {
Layout.columnSpan: 2
Layout.fillWidth: true
spacing: 0
Switch {
id: thousands
onCheckedChanged: {
if (activeFocus)
Config.thousandsSeparator = checked
}
}
Label {
Layout.fillWidth: true
text: qsTr('Add thousands separators to bitcoin amounts')
wrapMode: Text.Wrap
}
}
RowLayout {
spacing: 0
Switch {
id: fiatEnable
onCheckedChanged: {
if (activeFocus)
Daemon.fx.enabled = checked
}
}
Label {
Layout.fillWidth: true
text: qsTr('Fiat Currency')
wrapMode: Text.Wrap
}
}
ElComboBox {
id: currencies
model: Daemon.fx.currencies
enabled: Daemon.fx.enabled
onCurrentValueChanged: {
if (activeFocus)
Daemon.fx.fiatCurrency = currentValue
}
}
RowLayout {
Layout.columnSpan: 2
Layout.fillWidth: true
spacing: 0
Switch {
id: historicRates
enabled: Daemon.fx.enabled
onCheckedChanged: {
if (activeFocus)
Daemon.fx.historicRates = checked
}
}
Label {
Layout.fillWidth: true
text: qsTr('Historic rates')
wrapMode: Text.Wrap
}
}
Label {
text: qsTr('Exchange rate provider')
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
}
}
RowLayout {
Layout.columnSpan: 2
Layout.fillWidth: true
spacing: 0
Switch {
id: syncLabels
onCheckedChanged: {
if (activeFocus)
AppController.setPluginEnabled('labels', checked)
}
}
Label {
Layout.fillWidth: true
text: qsTr('Synchronize labels')
wrapMode: Text.Wrap
}
}
RowLayout {
Layout.columnSpan: 2
Layout.fillWidth: true
spacing: 0
Switch {
id: psbtNostr
onCheckedChanged: {
if (activeFocus)
AppController.setPluginEnabled('psbt_nostr', checked)
}
}
Label {
Layout.fillWidth: true
text: qsTr('Nostr Cosigner')
wrapMode: Text.Wrap
}
}
RowLayout {
Layout.columnSpan: 2
Layout.fillWidth: true
spacing: 0
Switch {
id: setMaxBrightnessOnQrDisplay
onCheckedChanged: {
if (activeFocus)
Config.setMaxBrightnessOnQrDisplay = checked
}
}
Label {
Layout.fillWidth: true
text: qsTr('Set display to max brightness when displaying QR codes')
wrapMode: Text.Wrap
}
}
PrefsHeading {
Layout.columnSpan: 2
text: qsTr('Security')
}
RowLayout {
Layout.columnSpan: 2
Layout.fillWidth: true
spacing: 0
property bool noWalletPassword: Daemon.currentWallet ? Daemon.currentWallet.verifyPassword('') : true
enabled: Daemon.currentWallet && !noWalletPassword
Switch {
id: paymentAuthentication
// showing the toggle as checked even if the wallet has no password would be misleading
checked: Config.paymentAuthentication && !(Daemon.currentWallet && parent.noWalletPassword)
onCheckedChanged: {
if (activeFocus) {
// will request authentication when checked = false
console.log('paymentAuthentication: ' + checked)
Config.paymentAuthentication = checked;
}
}
}
Label {
Layout.fillWidth: true
text: qsTr('Request authentication for payments')
wrapMode: Text.Wrap
}
}
RowLayout {
Layout.columnSpan: 2
Layout.fillWidth: true
spacing: 0
// isAvailable checks phone support and if a fingerprint is enrolled on the system
enabled: Biometrics.isAvailable && Daemon.currentWallet
Connections {
target: Biometrics
function onEnablingFailed(error) {
if (error === 'CANCELLED') {
return // don't show error popup
}
var err = app.messageDialog.createObject(app, {
text: qsTr('Failed to enable biometric authentication: ') + error
})
err.open()
}
}
Switch {
id: useBiometrics
checked: Biometrics.isEnabled
onCheckedChanged: {
if (activeFocus) {
useBiometrics.focus = false
if (checked) {
if (Daemon.singlePasswordEnabled) {
Biometrics.enable(Daemon.singlePassword)
} else {
useBiometrics.checked = false
var err = app.messageDialog.createObject(app, {
title: qsTr('Unavailable'),
text: [
qsTr("Cannot activate biometric authentication because you have wallets with different passwords."),
qsTr("To use biometric authentication you first need to change all wallet passwords to the same password.")
].join("\n")
})
err.open()
}
} else {
Biometrics.disableProtected()
}
}
}
}
Label {
Layout.fillWidth: true
text: qsTr('Biometric authentication')
wrapMode: Text.Wrap
}
}
RowLayout {
Layout.columnSpan: 2
Layout.fillWidth: true
spacing: 0
Switch {
id: disableScreenshots
onCheckedChanged: {
if (activeFocus)
Config.alwaysAllowScreenshots = !checked
}
}
Label {
Layout.fillWidth: true
text: qsTr('Protect secrets from screenshots')
wrapMode: Text.Wrap
}
}
PrefsHeading {
Layout.columnSpan: 2
text: qsTr('Wallet behavior')
}
RowLayout {
Layout.columnSpan: 2
spacing: 0
Switch {
id: spendUnconfirmed
onCheckedChanged: {
if (activeFocus)
Config.spendUnconfirmed = checked
}
}
Label {
Layout.fillWidth: true
text: qsTr('Spend unconfirmed')
wrapMode: Text.Wrap
}
}
RowLayout {
Layout.columnSpan: 2
spacing: 0
Switch {
id: freezeReusedAddressUtxos
onCheckedChanged: {
if (activeFocus)
Config.freezeReusedAddressUtxos = checked
}
}
Label {
Layout.fillWidth: true
text: Config.shortDescFor('WALLET_FREEZE_REUSED_ADDRESS_UTXOS')
wrapMode: Text.Wrap
}
}
PrefsHeading {
Layout.columnSpan: 2
text: qsTr('Lightning')
}
Label {
Layout.fillWidth: true
text: Config.shortDescFor('LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS')
wrapMode: Text.Wrap
}
Label {
Layout.fillWidth: true
text: qsTr('<b>%1%</b> of payment').arg(maxfeeslider._fees[maxfeeslider.value]/10000)
wrapMode: Text.Wrap
}
Slider {
id: maxfeeslider
Layout.columnSpan: 2
Layout.fillWidth: true
Layout.leftMargin: constants.paddingXLarge
Layout.rightMargin: constants.paddingXLarge
property var _fees: [500, 1000, 3000, 5000, 10000, 20000, 30000, 50000]
snapMode: Slider.SnapOnRelease
stepSize: 1
from: 0
to: _fees.length - 1
onValueChanged: {
if (activeFocus)
Config.lightningPaymentFeeMaxMillionths = _fees[value]
}
Component.onCompleted: {
value = _fees.indexOf(Config.lightningPaymentFeeMaxMillionths)
}
}
RowLayout {
Layout.columnSpan: 2
Layout.fillWidth: true
spacing: 0
Switch {
id: useTrampolineRouting
onCheckedChanged: {
if (activeFocus) {
if (!checked) {
var dialog = app.messageDialog.createObject(app, {
title: qsTr('Are you sure?'),
text: qsTr('Electrum will have to download the Lightning Network graph, which is not recommended on mobile.'),
yesno: true
})
dialog.accepted.connect(function() {
Config.useGossip = true
})
dialog.rejected.connect(function() {
checked = true // revert
})
dialog.open()
} else {
Config.useGossip = !checked
}
}
}
}
Label {
Layout.fillWidth: true
text: qsTr('Trampoline routing')
wrapMode: Text.Wrap
}
}
RowLayout {
Layout.columnSpan: 2
Layout.fillWidth: true
spacing: 0
Switch {
id: useRecoverableChannels
onCheckedChanged: {
if (activeFocus) {
if (!checked) {
var dialog = app.messageDialog.createObject(app, {
title: qsTr('Are you sure?'),
text: qsTr('This option allows you to recover your lightning funds if you lose your device, or if you uninstall this app while lightning channels are active. Do not disable it unless you know how to recover channels from backups.'),
yesno: true
})
dialog.accepted.connect(function() {
Config.useRecoverableChannels = false
})
dialog.rejected.connect(function() {
checked = true // revert
})
dialog.open()
} else {
Config.useRecoverableChannels = checked
}
}
}
}
Label {
Layout.fillWidth: true
text: qsTr('Create recoverable channels')
wrapMode: Text.Wrap
}
}
PrefsHeading {
Layout.columnSpan: 2
text: qsTr('Advanced')
}
RowLayout {
Layout.columnSpan: 2
Layout.fillWidth: true
spacing: 0
Switch {
id: enableDebugLogs
onCheckedChanged: {
if (activeFocus)
Config.enableDebugLogs = checked
}
enabled: Config.canToggleDebugLogs
}
Label {
Layout.fillWidth: true
text: qsTr('Enable debug logs (for developers)')
wrapMode: Text.Wrap
}
}
}
}
}
}
Component.onCompleted: {
language.currentIndex = language.indexOfValue(Config.language)
baseUnit.currentIndex = _baseunits.indexOf(Config.baseUnit)
thousands.checked = Config.thousandsSeparator
currencies.currentIndex = currencies.indexOfValue(Daemon.fx.fiatCurrency)
historicRates.checked = Daemon.fx.historicRates
rateSources.currentIndex = rateSources.indexOfValue(Daemon.fx.rateSource)
fiatEnable.checked = Daemon.fx.enabled
spendUnconfirmed.checked = Config.spendUnconfirmed
freezeReusedAddressUtxos.checked = Config.freezeReusedAddressUtxos
useTrampolineRouting.checked = !Config.useGossip
enableDebugLogs.checked = Config.enableDebugLogs
disableScreenshots.checked = !Config.alwaysAllowScreenshots
setMaxBrightnessOnQrDisplay.checked = Config.setMaxBrightnessOnQrDisplay
useRecoverableChannels.checked = Config.useRecoverableChannels
syncLabels.checked = AppController.isPluginEnabled('labels')
psbtNostr.checked = AppController.isPluginEnabled('psbt_nostr')
}
}