Files
pallectrum/electrum/gui/qt/amountedit.py
Davide Grilli 6a39a1401a Replace BTC references with PLM in codebase
Update all instances of BTC currency references to PLM across multiple files including UI components, utility functions, and command descriptions to reflect the new currency denomination
2025-11-20 16:43:48 +01:00

178 lines
6.4 KiB
Python

# -*- coding: utf-8 -*-
from decimal import Decimal
from typing import Union
from PyQt6.QtCore import pyqtSignal, Qt, QSize
from PyQt6.QtGui import QPainter
from PyQt6.QtWidgets import (QLineEdit, QStyle, QStyleOptionFrame, QSizePolicy)
from .util import char_width_in_lineedit, ColorScheme
from electrum.util import (format_satoshis_plain, decimal_point_to_base_unit_name,
FEERATE_PRECISION, quantize_feerate, DECIMAL_POINT, UI_UNIT_NAME_FEERATE_SAT_PER_VBYTE)
from electrum.bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC
_NOT_GIVEN = object() # sentinel value
class FreezableLineEdit(QLineEdit):
frozen = pyqtSignal()
def setFrozen(self, b):
self.setReadOnly(b)
self.setStyleSheet(ColorScheme.LIGHTBLUE.as_stylesheet(True) if b else '')
self.frozen.emit()
def isFrozen(self):
return self.isReadOnly()
class SizedFreezableLineEdit(FreezableLineEdit):
def __init__(self, *, width: int, parent=None):
super().__init__(parent)
self._width = width
self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
self.setMaximumWidth(width)
def sizeHint(self) -> QSize:
sh = super().sizeHint()
return QSize(self._width, sh.height())
class AmountEdit(SizedFreezableLineEdit):
shortcut = pyqtSignal()
def __init__(self, base_unit, is_int=False, parent=None, *, max_amount=None):
# This seems sufficient for hundred-PLM amounts with 8 decimals
width = 16 * char_width_in_lineedit()
super().__init__(width=width, parent=parent)
self.base_unit = base_unit
self.textChanged.connect(self.numbify)
self.is_int = is_int
self.is_shortcut = False
self.extra_precision = 0
self.max_amount = max_amount
def decimal_point(self):
return 8
def max_precision(self):
return self.decimal_point() + self.extra_precision
def numbify(self):
text = self.text().strip()
if text == '!':
self.shortcut.emit()
return
pos = self.cursorPosition()
chars = '0123456789'
if not self.is_int: chars += DECIMAL_POINT
s = ''.join([i for i in text if i in chars])
if not self.is_int:
if DECIMAL_POINT in s:
p = s.find(DECIMAL_POINT)
s = s.replace(DECIMAL_POINT, '')
s = s[:p] + DECIMAL_POINT + s[p:p+self.max_precision()]
if self.max_amount:
if (amt := self._get_amount_from_text(s)) and amt >= self.max_amount:
s = self._get_text_from_amount(self.max_amount)
self.setText(s)
# setText sets Modified to False. Instead we want to remember
# if updates were because of user modification.
self.setModified(self.hasFocus())
self.setCursorPosition(pos)
def paintEvent(self, event):
QLineEdit.paintEvent(self, event)
if self.base_unit:
panel = QStyleOptionFrame()
self.initStyleOption(panel)
textRect = self.style().subElementRect(QStyle.SubElement.SE_LineEditContents, panel, self)
textRect.adjust(2, 0, -10, 0)
painter = QPainter(self)
painter.setPen(ColorScheme.GRAY.as_color())
painter.drawText(textRect, int(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter), self.base_unit())
def _get_amount_from_text(self, text: str) -> Union[None, Decimal, int]:
try:
text = text.replace(DECIMAL_POINT, '.')
return (int if self.is_int else Decimal)(text)
except Exception:
return None
def get_amount(self) -> Union[None, Decimal, int]:
amt = self._get_amount_from_text(str(self.text()))
if self.max_amount and amt and amt >= self.max_amount:
return self.max_amount
return amt
def _get_text_from_amount(self, amount) -> str:
return "%d" % amount
def setAmount(self, amount):
text = self._get_text_from_amount(amount)
self.setText(text)
class BTCAmountEdit(AmountEdit):
def __init__(self, decimal_point, is_int=False, parent=None, *, max_amount=_NOT_GIVEN):
if max_amount is _NOT_GIVEN:
max_amount = TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN
AmountEdit.__init__(self, self._base_unit, is_int, parent, max_amount=max_amount)
self.decimal_point = decimal_point
def _base_unit(self):
return decimal_point_to_base_unit_name(self.decimal_point())
def _get_amount_from_text(self, text):
# returns amt in satoshis
try:
text = text.replace(DECIMAL_POINT, '.')
x = Decimal(text)
except Exception:
return None
# scale it to max allowed precision, make it an int
power = pow(10, self.max_precision())
max_prec_amount = int(power * x)
# if the max precision is simply what unit conversion allows, just return
if self.max_precision() == self.decimal_point():
return max_prec_amount
# otherwise, scale it back to the expected unit
amount = Decimal(max_prec_amount) / pow(10, self.max_precision()-self.decimal_point())
return Decimal(amount) if not self.is_int else int(amount)
def _get_text_from_amount(self, amount_sat):
text = format_satoshis_plain(amount_sat, decimal_point=self.decimal_point())
text = text.replace('.', DECIMAL_POINT)
return text
def setAmount(self, amount_sat):
if amount_sat is None:
self.setText(" ") # Space forces repaint in case units changed
else:
text = self._get_text_from_amount(amount_sat)
self.setText(text)
self.setFrozen(self.isFrozen()) # re-apply styling, as it is nuked by setText (?)
self.repaint() # macOS hack for #6269
class FeerateEdit(BTCAmountEdit):
def __init__(self, decimal_point, is_int=False, parent=None, *, max_amount=_NOT_GIVEN):
super().__init__(decimal_point, is_int, parent, max_amount=max_amount)
self.extra_precision = FEERATE_PRECISION
def _base_unit(self):
return UI_UNIT_NAME_FEERATE_SAT_PER_VBYTE
def _get_amount_from_text(self, text):
sat_per_byte_amount = super()._get_amount_from_text(text)
return quantize_feerate(sat_per_byte_amount)
def _get_text_from_amount(self, amount):
amount = quantize_feerate(amount)
return super()._get_text_from_amount(amount)