Files
pallectrum/electrum/gui/qt/amountedit.py
SomberNight 2a9909c252 locale amounts: consistently use "." as dec point, and " " as thou sep
Always use "." as decimal point, and " " as thousands separator.

Previously,
- for decimal point, we were using
  - "." in some places (e.g. AmountEdit, most fiat amounts), and
  - `locale.localeconv()['decimal_point']` in others.
- for thousands separator, we were using
  - "," in some places (most fiat amounts), and
  - " " in others (format_satoshis)

I think it is better to be consistent even if whatever we pick differs from the locale.
Using whitespace for thousands separator (vs comma) is probably less confusing for people
whose locale would user "." for ts and "," for dp (as in e.g. German).

The alternative option would be to always use the locale. Even if we decide to do that later,
this refactoring should be useful.

closes https://github.com/spesmilo/electrum/issues/2629
2023-01-10 14:45:35 +00:00

175 lines
6.1 KiB
Python

# -*- coding: utf-8 -*-
from decimal import Decimal
from typing import Union
from PyQt5.QtCore import pyqtSignal, Qt, QSize
from PyQt5.QtGui import QPalette, QPainter
from PyQt5.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)
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.setFrame(not b)
self.frozen.emit()
class SizedFreezableLineEdit(FreezableLineEdit):
def __init__(self, *, width: int, parent=None):
super().__init__(parent)
self._width = width
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.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-BTC 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.SE_LineEditContents, panel, self)
textRect.adjust(2, 0, -10, 0)
painter = QPainter(self)
painter.setPen(ColorScheme.GRAY.as_color())
painter.drawText(textRect, int(Qt.AlignRight | Qt.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:
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:
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.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 'sat/byte'
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)