Files
pallectrum/electrum/gui/qml/qeconfig.py
SomberNight 0dae17339d qml: add config setting to allow screenshots
On Android, we disallow screenshots on screens where the seed is visible.
(The seed is extremely sensitive data that should not be stored digitally without
significant precautions but it's also cumbersome to write down or memorise, so
some people instinctively just try to take a screenshot of it when creating a wallet.)
We do this by using the built-in OS mechanism of setting FLAG_SECURE on the window.
However, on some devices with custom ROMs (one report from LineageOS, one from /e/OS),
unsetting FLAG_SECURE crashes the application for some reason.

As a workaround, this commit adds a config setting into the Preferences,
to disable this mechanism and just always allow screenshots.
(note that you can get into the qml Preferences before creating/opening any wallet)

ref https://github.com/spesmilo/electrum/issues/8522
2023-12-27 07:28:39 +00:00

312 lines
12 KiB
Python

import copy
from decimal import Decimal
from typing import TYPE_CHECKING
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QRegularExpression
from electrum.bitcoin import TOTAL_COIN_SUPPLY_LIMIT_IN_BTC
from electrum.i18n import set_language, languages
from electrum.logging import get_logger
from electrum.util import base_unit_name_to_decimal_point
from .qetypes import QEAmount
from .auth import AuthMixin, auth_protect
if TYPE_CHECKING:
from electrum.simple_config import SimpleConfig
class QEConfig(AuthMixin, QObject):
_logger = get_logger(__name__)
def __init__(self, config: 'SimpleConfig', parent=None):
super().__init__(parent)
self.config = config
languageChanged = pyqtSignal()
@pyqtProperty(str, notify=languageChanged)
def language(self):
return self.config.LOCALIZATION_LANGUAGE
@language.setter
def language(self, language):
if language not in languages:
return
if self.config.LOCALIZATION_LANGUAGE != language:
self.config.LOCALIZATION_LANGUAGE = language
set_language(language)
self.languageChanged.emit()
languagesChanged = pyqtSignal()
@pyqtProperty('QVariantList', notify=languagesChanged)
def languagesAvailable(self):
# sort on translated languages, then re-add Default on top
langs = copy.deepcopy(languages)
default = langs.pop('')
langs_sorted = sorted(list(map(lambda x: {'value': x[0], 'text': x[1]}, langs.items())), key=lambda x: x['text'])
langs_sorted.insert(0, {'value': '', 'text': default})
return langs_sorted
autoConnectChanged = pyqtSignal()
@pyqtProperty(bool, notify=autoConnectChanged)
def autoConnect(self):
return self.config.NETWORK_AUTO_CONNECT
@autoConnect.setter
def autoConnect(self, auto_connect):
self.config.NETWORK_AUTO_CONNECT = auto_connect
self.autoConnectChanged.emit()
# auto_connect is actually a tri-state, expose the undefined case
@pyqtProperty(bool, notify=autoConnectChanged)
def autoConnectDefined(self):
return self.config.cv.NETWORK_AUTO_CONNECT.is_set()
baseUnitChanged = pyqtSignal()
@pyqtProperty(str, notify=baseUnitChanged)
def baseUnit(self):
return self.config.get_base_unit()
@baseUnit.setter
def baseUnit(self, unit):
self.config.set_base_unit(unit)
self.baseUnitChanged.emit()
@pyqtProperty('QRegularExpression', notify=baseUnitChanged)
def btcAmountRegex(self):
decimal_point = base_unit_name_to_decimal_point(self.config.get_base_unit())
max_digits_before_dp = (
len(str(TOTAL_COIN_SUPPLY_LIMIT_IN_BTC))
+ (base_unit_name_to_decimal_point("BTC") - decimal_point))
exp = '[0-9]{0,%d}' % max_digits_before_dp
if decimal_point > 0:
exp += '\\.'
exp += '[0-9]{0,%d}' % decimal_point
return QRegularExpression(exp)
thousandsSeparatorChanged = pyqtSignal()
@pyqtProperty(bool, notify=thousandsSeparatorChanged)
def thousandsSeparator(self):
return self.config.BTC_AMOUNTS_ADD_THOUSANDS_SEP
@thousandsSeparator.setter
def thousandsSeparator(self, checked):
self.config.BTC_AMOUNTS_ADD_THOUSANDS_SEP = checked
self.config.amt_add_thousands_sep = checked
self.thousandsSeparatorChanged.emit()
spendUnconfirmedChanged = pyqtSignal()
@pyqtProperty(bool, notify=spendUnconfirmedChanged)
def spendUnconfirmed(self):
return not self.config.WALLET_SPEND_CONFIRMED_ONLY
@spendUnconfirmed.setter
def spendUnconfirmed(self, checked):
self.config.WALLET_SPEND_CONFIRMED_ONLY = not checked
self.spendUnconfirmedChanged.emit()
requestExpiryChanged = pyqtSignal()
@pyqtProperty(int, notify=requestExpiryChanged)
def requestExpiry(self):
return self.config.WALLET_PAYREQ_EXPIRY_SECONDS
@requestExpiry.setter
def requestExpiry(self, expiry):
self.config.WALLET_PAYREQ_EXPIRY_SECONDS = expiry
self.requestExpiryChanged.emit()
pinCodeChanged = pyqtSignal()
@pyqtProperty(str, notify=pinCodeChanged)
def pinCode(self):
return self.config.CONFIG_PIN_CODE or ""
@pinCode.setter
def pinCode(self, pin_code):
if pin_code == '':
self.pinCodeRemoveAuth()
else:
self.config.CONFIG_PIN_CODE = pin_code
self.pinCodeChanged.emit()
@auth_protect(method='wallet')
def pinCodeRemoveAuth(self):
self.config.CONFIG_PIN_CODE = ""
self.pinCodeChanged.emit()
useGossipChanged = pyqtSignal()
@pyqtProperty(bool, notify=useGossipChanged)
def useGossip(self):
return self.config.LIGHTNING_USE_GOSSIP
@useGossip.setter
def useGossip(self, gossip):
self.config.LIGHTNING_USE_GOSSIP = gossip
self.useGossipChanged.emit()
useFallbackAddressChanged = pyqtSignal()
@pyqtProperty(bool, notify=useFallbackAddressChanged)
def useFallbackAddress(self):
return self.config.WALLET_BOLT11_FALLBACK
@useFallbackAddress.setter
def useFallbackAddress(self, use_fallback):
self.config.WALLET_BOLT11_FALLBACK = use_fallback
self.useFallbackAddressChanged.emit()
enableDebugLogsChanged = pyqtSignal()
@pyqtProperty(bool, notify=enableDebugLogsChanged)
def enableDebugLogs(self):
gui_setting = self.config.GUI_ENABLE_DEBUG_LOGS
return gui_setting or bool(self.config.get('verbosity'))
@pyqtProperty(bool, notify=enableDebugLogsChanged)
def canToggleDebugLogs(self):
gui_setting = self.config.GUI_ENABLE_DEBUG_LOGS
return not self.config.get('verbosity') or gui_setting
@enableDebugLogs.setter
def enableDebugLogs(self, enable):
self.config.GUI_ENABLE_DEBUG_LOGS = enable
self.enableDebugLogsChanged.emit()
alwaysAllowScreenshotsChanged = pyqtSignal()
@pyqtProperty(bool, notify=alwaysAllowScreenshotsChanged)
def alwaysAllowScreenshots(self):
return self.config.GUI_QML_ALWAYS_ALLOW_SCREENSHOTS
@alwaysAllowScreenshots.setter
def alwaysAllowScreenshots(self, enable):
self.config.GUI_QML_ALWAYS_ALLOW_SCREENSHOTS = enable
self.alwaysAllowScreenshotsChanged.emit()
useRecoverableChannelsChanged = pyqtSignal()
@pyqtProperty(bool, notify=useRecoverableChannelsChanged)
def useRecoverableChannels(self):
return self.config.LIGHTNING_USE_RECOVERABLE_CHANNELS
@useRecoverableChannels.setter
def useRecoverableChannels(self, useRecoverableChannels):
self.config.LIGHTNING_USE_RECOVERABLE_CHANNELS = useRecoverableChannels
self.useRecoverableChannelsChanged.emit()
trustedcoinPrepayChanged = pyqtSignal()
@pyqtProperty(int, notify=trustedcoinPrepayChanged)
def trustedcoinPrepay(self):
return self.config.PLUGIN_TRUSTEDCOIN_NUM_PREPAY
@trustedcoinPrepay.setter
def trustedcoinPrepay(self, num_prepay):
if num_prepay != self.config.PLUGIN_TRUSTEDCOIN_NUM_PREPAY:
self.config.PLUGIN_TRUSTEDCOIN_NUM_PREPAY = num_prepay
self.trustedcoinPrepayChanged.emit()
preferredRequestTypeChanged = pyqtSignal()
@pyqtProperty(str, notify=preferredRequestTypeChanged)
def preferredRequestType(self):
return self.config.GUI_QML_PREFERRED_REQUEST_TYPE
@preferredRequestType.setter
def preferredRequestType(self, preferred_request_type):
if preferred_request_type != self.config.GUI_QML_PREFERRED_REQUEST_TYPE:
self.config.GUI_QML_PREFERRED_REQUEST_TYPE = preferred_request_type
self.preferredRequestTypeChanged.emit()
userKnowsPressAndHoldChanged = pyqtSignal()
@pyqtProperty(bool, notify=userKnowsPressAndHoldChanged)
def userKnowsPressAndHold(self):
return self.config.GUI_QML_USER_KNOWS_PRESS_AND_HOLD
@userKnowsPressAndHold.setter
def userKnowsPressAndHold(self, userKnowsPressAndHold):
if userKnowsPressAndHold != self.config.GUI_QML_USER_KNOWS_PRESS_AND_HOLD:
self.config.GUI_QML_USER_KNOWS_PRESS_AND_HOLD = userKnowsPressAndHold
self.userKnowsPressAndHoldChanged.emit()
addresslistShowTypeChanged = pyqtSignal()
@pyqtProperty(int, notify=addresslistShowTypeChanged)
def addresslistShowType(self):
return self.config.GUI_QML_ADDRESS_LIST_SHOW_TYPE
@addresslistShowType.setter
def addresslistShowType(self, addresslistShowType):
if addresslistShowType != self.config.GUI_QML_ADDRESS_LIST_SHOW_TYPE:
self.config.GUI_QML_ADDRESS_LIST_SHOW_TYPE = addresslistShowType
self.addresslistShowTypeChanged.emit()
addresslistShowUsedChanged = pyqtSignal()
@pyqtProperty(bool, notify=addresslistShowUsedChanged)
def addresslistShowUsed(self):
return self.config.GUI_QML_ADDRESS_LIST_SHOW_USED
@addresslistShowUsed.setter
def addresslistShowUsed(self, addresslistShowUsed):
if addresslistShowUsed != self.config.GUI_QML_ADDRESS_LIST_SHOW_USED:
self.config.GUI_QML_ADDRESS_LIST_SHOW_USED = addresslistShowUsed
self.addresslistShowUsedChanged.emit()
@pyqtSlot('qint64', result=str)
@pyqtSlot(QEAmount, result=str)
def formatSatsForEditing(self, satoshis):
if isinstance(satoshis, QEAmount):
satoshis = satoshis.satsInt
return self.config.format_amount(
satoshis,
add_thousands_sep=False,
)
@pyqtSlot('qint64', result=str)
@pyqtSlot('qint64', bool, result=str)
@pyqtSlot(QEAmount, result=str)
@pyqtSlot(QEAmount, bool, result=str)
def formatSats(self, satoshis, with_unit=False):
if isinstance(satoshis, QEAmount):
satoshis = satoshis.satsInt
if with_unit:
return self.config.format_amount_and_units(satoshis)
else:
return self.config.format_amount(satoshis)
@pyqtSlot(QEAmount, result=str)
@pyqtSlot(QEAmount, bool, result=str)
def formatMilliSats(self, amount, with_unit=False):
if isinstance(amount, QEAmount):
msats = amount.msatsInt
else:
return '---'
precision = 3 # config.amt_precision_post_satoshi is not exposed in preferences
if with_unit:
return self.config.format_amount_and_units(msats/1000, precision=precision)
else:
return self.config.format_amount(msats/1000, precision=precision)
# TODO delegate all this to config.py/util.py
def decimal_point(self):
return self.config.BTC_AMOUNTS_DECIMAL_POINT
def max_precision(self):
return self.decimal_point() + 0 # self.extra_precision
@pyqtSlot(str, result=QEAmount)
def unitsToSats(self, unitAmount):
self._amount = QEAmount()
try:
x = Decimal(unitAmount)
except Exception:
return self._amount
# scale it to max allowed precision, make it an int
max_prec_amount = int(pow(10, self.max_precision()) * x)
# if the max precision is simply what unit conversion allows, just return
if self.max_precision() == self.decimal_point():
self._amount = QEAmount(amount_sat=max_prec_amount)
return self._amount
self._logger.debug('fallthrough')
# otherwise, scale it back to the expected unit
#amount = Decimal(max_prec_amount) / Decimal(pow(10, self.max_precision()-self.decimal_point()))
#return int(amount) #Decimal(amount) if not self.is_int else int(amount)
return self._amount
@pyqtSlot('quint64', result=float)
def satsToUnits(self, satoshis):
return satoshis / pow(10, self.config.decimal_point)