2013-04-06 23:34:12 +02:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
2018-05-09 19:30:18 +02:00
|
|
|
from decimal import Decimal
|
2019-11-19 19:29:10 +01:00
|
|
|
from typing import Union
|
2018-05-09 19:30:18 +02:00
|
|
|
|
2024-09-05 16:20:01 +00:00
|
|
|
from PyQt6.QtCore import pyqtSignal, Qt, QSize
|
2025-01-23 12:58:28 +01:00
|
|
|
from PyQt6.QtGui import QPainter
|
2024-09-05 16:20:01 +00:00
|
|
|
from PyQt6.QtWidgets import (QLineEdit, QStyle, QStyleOptionFrame, QSizePolicy)
|
2013-04-06 23:34:12 +02:00
|
|
|
|
2020-06-28 03:50:34 +02:00
|
|
|
from .util import char_width_in_lineedit, ColorScheme
|
2019-06-29 05:27:28 +02:00
|
|
|
|
2018-06-04 21:01:47 +02:00
|
|
|
from electrum.util import (format_satoshis_plain, decimal_point_to_base_unit_name,
|
2024-02-03 05:13:09 +00:00
|
|
|
FEERATE_PRECISION, quantize_feerate, DECIMAL_POINT, UI_UNIT_NAME_FEERATE_SAT_PER_VBYTE)
|
2022-10-19 17:58:05 +00:00
|
|
|
from electrum.bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC
|
|
|
|
|
|
|
|
|
|
_NOT_GIVEN = object() # sentinel value
|
2013-04-06 23:34:12 +02:00
|
|
|
|
2017-01-22 21:25:24 +03:00
|
|
|
|
2019-11-20 18:00:45 +01:00
|
|
|
class FreezableLineEdit(QLineEdit):
|
2014-06-24 17:44:31 +02:00
|
|
|
frozen = pyqtSignal()
|
2014-06-05 14:49:32 +02:00
|
|
|
|
|
|
|
|
def setFrozen(self, b):
|
|
|
|
|
self.setReadOnly(b)
|
2023-07-07 17:05:08 +02:00
|
|
|
self.setStyleSheet(ColorScheme.LIGHTBLUE.as_stylesheet(True) if b else '')
|
2014-06-24 17:44:31 +02:00
|
|
|
self.frozen.emit()
|
2014-06-05 14:49:32 +02:00
|
|
|
|
2023-06-26 12:58:10 +02:00
|
|
|
def isFrozen(self):
|
|
|
|
|
return self.isReadOnly()
|
2021-07-05 17:26:29 +02:00
|
|
|
|
2025-01-23 12:58:28 +01:00
|
|
|
|
2021-07-05 17:26:29 +02:00
|
|
|
class SizedFreezableLineEdit(FreezableLineEdit):
|
|
|
|
|
|
|
|
|
|
def __init__(self, *, width: int, parent=None):
|
|
|
|
|
super().__init__(parent)
|
|
|
|
|
self._width = width
|
2024-09-05 16:20:01 +00:00
|
|
|
self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
|
2021-09-08 16:36:07 +02:00
|
|
|
self.setMaximumWidth(width)
|
2021-07-05 17:26:29 +02:00
|
|
|
|
|
|
|
|
def sizeHint(self) -> QSize:
|
|
|
|
|
sh = super().sizeHint()
|
|
|
|
|
return QSize(self._width, sh.height())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AmountEdit(SizedFreezableLineEdit):
|
2014-09-07 18:45:06 +02:00
|
|
|
shortcut = pyqtSignal()
|
2013-04-06 23:34:12 +02:00
|
|
|
|
2022-10-19 17:58:05 +00:00
|
|
|
def __init__(self, base_unit, is_int=False, parent=None, *, max_amount=None):
|
2015-04-29 21:41:27 +09:00
|
|
|
# This seems sufficient for hundred-BTC amounts with 8 decimals
|
2021-07-05 17:26:29 +02:00
|
|
|
width = 16 * char_width_in_lineedit()
|
|
|
|
|
super().__init__(width=width, parent=parent)
|
2014-06-11 14:44:26 +02:00
|
|
|
self.base_unit = base_unit
|
|
|
|
|
self.textChanged.connect(self.numbify)
|
|
|
|
|
self.is_int = is_int
|
|
|
|
|
self.is_shortcut = False
|
2018-05-09 19:30:18 +02:00
|
|
|
self.extra_precision = 0
|
2022-10-19 17:58:05 +00:00
|
|
|
self.max_amount = max_amount
|
2014-06-11 14:44:26 +02:00
|
|
|
|
2014-06-16 18:38:28 +02:00
|
|
|
def decimal_point(self):
|
|
|
|
|
return 8
|
|
|
|
|
|
2018-05-09 19:30:18 +02:00
|
|
|
def max_precision(self):
|
|
|
|
|
return self.decimal_point() + self.extra_precision
|
|
|
|
|
|
2014-06-11 14:44:26 +02:00
|
|
|
def numbify(self):
|
2017-01-30 12:36:56 +03:00
|
|
|
text = self.text().strip()
|
2014-06-11 14:44:26 +02:00
|
|
|
if text == '!':
|
2014-09-07 18:45:06 +02:00
|
|
|
self.shortcut.emit()
|
|
|
|
|
return
|
2014-06-11 14:44:26 +02:00
|
|
|
pos = self.cursorPosition()
|
|
|
|
|
chars = '0123456789'
|
2023-01-10 14:45:35 +00:00
|
|
|
if not self.is_int: chars += DECIMAL_POINT
|
2014-06-11 14:44:26 +02:00
|
|
|
s = ''.join([i for i in text if i in chars])
|
|
|
|
|
if not self.is_int:
|
2023-01-10 14:45:35 +00:00
|
|
|
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()]
|
2022-10-19 17:58:05 +00:00
|
|
|
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)
|
2014-06-11 14:44:26 +02:00
|
|
|
self.setText(s)
|
2015-07-03 20:14:12 +09:00
|
|
|
# setText sets Modified to False. Instead we want to remember
|
|
|
|
|
# if updates were because of user modification.
|
|
|
|
|
self.setModified(self.hasFocus())
|
2014-06-11 14:44:26 +02:00
|
|
|
self.setCursorPosition(pos)
|
|
|
|
|
|
|
|
|
|
def paintEvent(self, event):
|
|
|
|
|
QLineEdit.paintEvent(self, event)
|
|
|
|
|
if self.base_unit:
|
2017-09-23 05:54:38 +02:00
|
|
|
panel = QStyleOptionFrame()
|
2014-06-11 18:52:55 +02:00
|
|
|
self.initStyleOption(panel)
|
2024-09-05 16:20:01 +00:00
|
|
|
textRect = self.style().subElementRect(QStyle.SubElement.SE_LineEditContents, panel, self)
|
2014-06-11 18:52:55 +02:00
|
|
|
textRect.adjust(2, 0, -10, 0)
|
|
|
|
|
painter = QPainter(self)
|
2020-06-28 03:50:34 +02:00
|
|
|
painter.setPen(ColorScheme.GRAY.as_color())
|
2024-09-05 16:20:01 +00:00
|
|
|
painter.drawText(textRect, int(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter), self.base_unit())
|
2014-06-11 14:44:26 +02:00
|
|
|
|
2022-10-19 17:22:23 +00:00
|
|
|
def _get_amount_from_text(self, text: str) -> Union[None, Decimal, int]:
|
2014-07-16 15:33:59 +02:00
|
|
|
try:
|
2023-01-10 14:45:35 +00:00
|
|
|
text = text.replace(DECIMAL_POINT, '.')
|
2022-10-19 17:22:23 +00:00
|
|
|
return (int if self.is_int else Decimal)(text)
|
2023-04-23 01:33:12 +00:00
|
|
|
except Exception:
|
2014-07-16 15:33:59 +02:00
|
|
|
return None
|
2014-06-11 14:44:26 +02:00
|
|
|
|
2022-10-19 17:22:23 +00:00
|
|
|
def get_amount(self) -> Union[None, Decimal, int]:
|
2022-10-19 17:58:05 +00:00
|
|
|
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
|
2022-10-19 17:22:23 +00:00
|
|
|
|
|
|
|
|
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)
|
2017-12-01 09:58:24 +01:00
|
|
|
|
2014-06-11 14:44:26 +02:00
|
|
|
|
|
|
|
|
class BTCAmountEdit(AmountEdit):
|
|
|
|
|
|
2022-10-19 17:58:05 +00:00
|
|
|
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)
|
2014-06-05 12:40:07 +02:00
|
|
|
self.decimal_point = decimal_point
|
2013-04-06 23:34:12 +02:00
|
|
|
|
2014-06-11 18:52:55 +02:00
|
|
|
def _base_unit(self):
|
2018-05-05 12:42:17 +02:00
|
|
|
return decimal_point_to_base_unit_name(self.decimal_point())
|
2014-06-05 12:40:07 +02:00
|
|
|
|
2022-10-19 17:22:23 +00:00
|
|
|
def _get_amount_from_text(self, text):
|
2020-06-22 22:37:58 +02:00
|
|
|
# returns amt in satoshis
|
2014-06-11 18:17:27 +02:00
|
|
|
try:
|
2023-01-10 14:45:35 +00:00
|
|
|
text = text.replace(DECIMAL_POINT, '.')
|
2022-10-19 17:22:23 +00:00
|
|
|
x = Decimal(text)
|
2023-04-23 01:33:12 +00:00
|
|
|
except Exception:
|
2014-06-05 12:40:07 +02:00
|
|
|
return None
|
2018-05-09 19:30:18 +02:00
|
|
|
# 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)
|
2014-06-05 12:40:07 +02:00
|
|
|
|
2022-10-19 17:22:23 +00:00
|
|
|
def _get_text_from_amount(self, amount_sat):
|
2023-01-10 14:45:35 +00:00
|
|
|
text = format_satoshis_plain(amount_sat, decimal_point=self.decimal_point())
|
|
|
|
|
text = text.replace('.', DECIMAL_POINT)
|
|
|
|
|
return text
|
2022-10-19 17:22:23 +00:00
|
|
|
|
2020-06-22 22:37:58 +02:00
|
|
|
def setAmount(self, amount_sat):
|
|
|
|
|
if amount_sat is None:
|
|
|
|
|
self.setText(" ") # Space forces repaint in case units changed
|
2015-05-27 16:34:42 +09:00
|
|
|
else:
|
2022-10-19 17:22:23 +00:00
|
|
|
text = self._get_text_from_amount(amount_sat)
|
|
|
|
|
self.setText(text)
|
2023-06-26 12:58:10 +02:00
|
|
|
self.setFrozen(self.isFrozen()) # re-apply styling, as it is nuked by setText (?)
|
2020-06-25 22:03:40 +02:00
|
|
|
self.repaint() # macOS hack for #6269
|
2015-08-04 07:15:54 +02:00
|
|
|
|
2017-12-18 22:26:29 +01:00
|
|
|
|
|
|
|
|
class FeerateEdit(BTCAmountEdit):
|
2018-05-09 19:30:18 +02:00
|
|
|
|
2022-10-19 17:58:05 +00:00
|
|
|
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)
|
2018-05-09 19:30:18 +02:00
|
|
|
self.extra_precision = FEERATE_PRECISION
|
|
|
|
|
|
2015-08-04 07:15:54 +02:00
|
|
|
def _base_unit(self):
|
2024-02-03 05:13:09 +00:00
|
|
|
return UI_UNIT_NAME_FEERATE_SAT_PER_VBYTE
|
2017-12-18 22:26:29 +01:00
|
|
|
|
2022-10-19 17:22:23 +00:00
|
|
|
def _get_amount_from_text(self, text):
|
|
|
|
|
sat_per_byte_amount = super()._get_amount_from_text(text)
|
2018-06-04 21:17:25 +02:00
|
|
|
return quantize_feerate(sat_per_byte_amount)
|
2018-06-04 21:01:47 +02:00
|
|
|
|
2022-10-19 17:22:23 +00:00
|
|
|
def _get_text_from_amount(self, amount):
|
2018-06-04 21:01:47 +02:00
|
|
|
amount = quantize_feerate(amount)
|
2022-10-19 17:22:23 +00:00
|
|
|
return super()._get_text_from_amount(amount)
|