forked from bitcoinafterlife/bal-electrum-plugin
922 lines
35 KiB
Python
922 lines
35 KiB
Python
"""
|
||
bal.gui.qt.widgets
|
||
==================
|
||
|
||
Reusable, self-contained Qt widgets used to build the BAL tabs and dialogs.
|
||
|
||
These are "leaf" widgets: they receive the :class:`BalWindow` controller (and
|
||
any data they need) as constructor arguments at runtime, so this module does
|
||
not import ``window``/``dialogs`` and therefore introduces no import cycles.
|
||
|
||
Contents:
|
||
* ClickableLabel, BalLineEdit, BalTextEdit, BalCheckBox - thin Qt wrappers
|
||
* BalTxFeesWidget - fee-rate editor
|
||
* _LockTimeEditor + BalTimeEditWidget + raw/date editors - locktime editing
|
||
* ThresholdTimeWidget / LockTimeWidget - threshold & locktime
|
||
* WillSettingsWidget - the settings panel
|
||
* PercAmountEdit - amount-or-percentage editor
|
||
* WillWidget - single will-tx box
|
||
"""
|
||
|
||
from .common import *
|
||
from .common import _, _logger, Will
|
||
from .calendar import BalCalendar
|
||
|
||
|
||
class ClickableLabel(QLabel):
|
||
doubleClicked = pyqtSignal()
|
||
|
||
def mouseDoubleClickEvent(self, event):
|
||
self.doubleClicked.emit()
|
||
super().mouseDoubleClickEvent(event)
|
||
|
||
|
||
|
||
class BalTxFeesWidget(QWidget):
|
||
valueChanged = pyqtSignal()
|
||
current_value = None
|
||
|
||
def __init__(self, bal_window, parent, value=None):
|
||
super().__init__(parent)
|
||
self.bal_window = bal_window
|
||
layout = QHBoxLayout(self)
|
||
self.txfee_widget = QSpinBox(self)
|
||
self.txfee_widget.setMinimum(1)
|
||
self.txfee_widget.setMaximum(10000)
|
||
value = (
|
||
value
|
||
if value
|
||
else self.bal_window.bal_plugin.WILL_SETTINGS.get()["baltx_fees"]
|
||
)
|
||
self.set_value(value)
|
||
self.default_value = self.bal_window.bal_plugin.default_will_settings()[
|
||
"baltx_fees"
|
||
]
|
||
self.txfee_widget.valueChanged.connect(self.on_heir_tx_fees)
|
||
#label = ClickableLabel("$")
|
||
#label.doubleClicked.connect(self.doubleclick)
|
||
#layout.addWidget(label)
|
||
button = HelpButton(_("mining fees expressed in sats/vbyte to be used in the Bitcoin transaction.\nHigher value ensure your transaction will be confirmed"))
|
||
button.setText("丰")
|
||
button.setStyleSheet("font-size: 16px;")
|
||
layout.addWidget(button)
|
||
layout.addWidget(self.txfee_widget)
|
||
# Expose the leading icon (prefix) and the editable field so the parent
|
||
# WillSettingsWidget can align them on a grid (see its vertical layout).
|
||
self.prefix_widget = button
|
||
self.field_widget = self.txfee_widget
|
||
|
||
def doubleclick(self, event=None):
|
||
pass
|
||
|
||
def set_read_only(self, read_only=True):
|
||
# Show the fee but make it non-editable (no spin arrows, no keyboard),
|
||
# so it can only be changed from the "Build your will" wizard.
|
||
self.txfee_widget.setReadOnly(read_only)
|
||
self.txfee_widget.setButtonSymbols(
|
||
QAbstractSpinBox.ButtonSymbols.NoButtons
|
||
if read_only
|
||
else QAbstractSpinBox.ButtonSymbols.UpDownArrows
|
||
)
|
||
# Light-grey background when locked, so the read-only state is visible
|
||
# (same look as the date fields); empty stylesheet restores the
|
||
# editable appearance used inside the wizard.
|
||
self.txfee_widget.setStyleSheet(
|
||
"QSpinBox{background-color:#f0f0f0;}" if read_only else ""
|
||
)
|
||
|
||
def get_value(self):
|
||
return self.txfee_widget.value()
|
||
|
||
def set_value(self, value, emit=True):
|
||
value = int(value) if value is not None else 20
|
||
if getattr(self, "_updating", False):
|
||
return
|
||
|
||
self._updating = True
|
||
try:
|
||
self.current_value = value
|
||
spin = self.txfee_widget
|
||
spin.blockSignals(True)
|
||
spin.setValue(value)
|
||
spin.blockSignals(False)
|
||
|
||
finally:
|
||
self._updating = False
|
||
|
||
if emit:
|
||
spin.valueChanged.emit(value)
|
||
|
||
def on_heir_tx_fees(self, value=None, update_all=True):
|
||
if value != self.current_value:
|
||
try:
|
||
self.set_value(value)
|
||
if update_all:
|
||
self.bal_window.update_setting_widgets(
|
||
self.get_value(), "baltx_fees", True
|
||
)
|
||
except Exception as e:
|
||
_logger.error(f"error while trying to update txfees{e}")
|
||
log_error(e)
|
||
else:
|
||
pass
|
||
|
||
|
||
|
||
class _LockTimeEditor:
|
||
min_allowed_value = NLOCKTIME_MIN
|
||
max_allowed_value = NLOCKTIME_MAX
|
||
alarm = None
|
||
|
||
def get_value(self) -> Optional[int]:
|
||
raise NotImplementedError()
|
||
|
||
def set_value(self, x: Any, force=True) -> None:
|
||
raise NotImplementedError()
|
||
|
||
@classmethod
|
||
def is_acceptable_locktime(cls, x: Any) -> bool:
|
||
if not x: # e.g. empty string
|
||
return True
|
||
try:
|
||
x = int(x)
|
||
except Exception as _e:
|
||
return False
|
||
return cls.min_allowed_value <= x <= cls.max_allowed_value
|
||
|
||
@staticmethod
|
||
def get_max_allowed_timestamp() -> int:
|
||
ts = NLOCKTIME_MAX
|
||
# Test if this value is within the valid timestamp limits (which is platform-dependent).
|
||
# see #6170
|
||
try:
|
||
datetime.fromtimestamp(ts)
|
||
except (OSError, OverflowError):
|
||
ts = 2**31 - 1 # INT32_MAX
|
||
datetime.fromtimestamp(ts) # test if raises
|
||
return ts
|
||
|
||
|
||
|
||
class BalTimeEditWidget(QWidget, _LockTimeEditor):
|
||
valueEdited = pyqtSignal()
|
||
_setting_locktime = False
|
||
current_value = None
|
||
current_index = None
|
||
default_value = None
|
||
|
||
help_text = (
|
||
"if you choose Raw, you can insert various options based on suffix:\n"
|
||
+ " - d: number of days after current day(ex: 1d means tomorrow)\n"
|
||
+ " - y: number of years after currrent day(ex: 1y means one year from today)\n"
|
||
)
|
||
label_text = None
|
||
tooltip_text = None
|
||
base_field = None
|
||
|
||
def __init__(self, bal_window, parent, default_locktime=None):
|
||
super().__init__(parent)
|
||
self.bal_window = bal_window
|
||
|
||
hbox = QHBoxLayout()
|
||
self.setLayout(hbox)
|
||
hbox.setContentsMargins(0, 0, 0, 0)
|
||
hbox.setSpacing(0)
|
||
self.setMinimumWidth(40 * char_width_in_lineedit())
|
||
self.locktime_raw_e = TimeRawEditWidget(self, time_edit=self)
|
||
self.locktime_date_e = LockTimeDateEdit(self, time_edit=self)
|
||
self.editors = [self.locktime_raw_e, self.locktime_date_e]
|
||
self.combo = QComboBox()
|
||
options = [_("Raw"), _("Date")]
|
||
self.option_index_to_editor_map = {
|
||
0: self.locktime_raw_e,
|
||
1: self.locktime_date_e,
|
||
}
|
||
self.combo.addItems(options)
|
||
default_index = 0
|
||
if not default_locktime:
|
||
default_locktime = self.bal_window.bal_plugin.WILL_SETTINGS.get()[self.base_field]
|
||
try:
|
||
int(default_locktime)
|
||
default_index = 1
|
||
except Exception:
|
||
default_index = 0
|
||
#hbox.addWidget(QLabel(self.label_text))
|
||
help_button=HelpButton(self.help_text)
|
||
help_button.setText(self.label_text)
|
||
# Show a short label (e.g. "Delivery time" / "Check Alive") when the
|
||
# user hovers the icon, so the emoji button is self-explanatory.
|
||
if self.tooltip_text:
|
||
help_button.setToolTip(_(self.tooltip_text))
|
||
#help_button.setStyleSheet("font-size: 155555);
|
||
hbox.addWidget(help_button)
|
||
# Expose the leading icon (prefix) so the parent WillSettingsWidget can
|
||
# align all rows on a common left edge (see its vertical layout).
|
||
self.prefix_widget = help_button
|
||
self.combo.currentIndexChanged.connect(self.on_current_index_changed)
|
||
|
||
for w in self.editors:
|
||
w.setVisible(False)
|
||
w.setEnabled(False)
|
||
|
||
self.editor = self.option_index_to_editor_map[default_index]
|
||
self.editor.setVisible(True)
|
||
self.editor.setEnabled(True)
|
||
self.set_index(default_index)
|
||
#self.on_current_index_changed(default_index)
|
||
self.set_value(default_locktime)
|
||
self.current_value=default_locktime
|
||
hbox.addWidget(self.combo)
|
||
for w in self.editors:
|
||
hbox.addWidget(w)
|
||
|
||
hbox.addStretch(1)
|
||
# spssscer_widget = QWidget()
|
||
# spacer_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||
# hbox.addWidget(spacer_widget)
|
||
self.valueEdited.connect(lambda: self.update_will_settings(True))
|
||
self.locktime_raw_e.editingFinished.connect(self.valueEdited.emit)
|
||
self.locktime_date_e.dateTimeChanged.connect(self.valueEdited.emit)
|
||
#self.combo.currentIndexChanged.connect(self.valueEdited.emit)
|
||
|
||
def update_will_settings(
|
||
self,
|
||
update_all=False,
|
||
update_will_dialog=False,
|
||
update_heirs_dialog=False,
|
||
):
|
||
self.bal_window.update_setting_widgets(
|
||
self.get_value(),
|
||
self.base_field,
|
||
update_all,
|
||
update_will_dialog,
|
||
update_heirs_dialog,
|
||
)
|
||
|
||
def on_current_index_changed(self, i):
|
||
self.current_index = i
|
||
for w in self.editors:
|
||
w.setVisible(False)
|
||
w.setEnabled(False)
|
||
# prev_locktime = self.editor.get_value()
|
||
self.editor = self.option_index_to_editor_map[i]
|
||
if i==0:
|
||
self.editor.set_value(self.bal_window.bal_plugin.default_will_settings_relative()[self.base_field])
|
||
else:
|
||
self.editor.set_value(self.bal_window.bal_plugin.default_will_settings_absolute()[self.base_field])
|
||
self.valueEdited.emit()
|
||
# if self.editor.is_acceptable_locktime(prev_locktime):
|
||
# self.editor.set_value(prev_locktime, force=False)
|
||
self.editor.setVisible(True)
|
||
self.editor.setEnabled(True)
|
||
self.bal_window.update_combo_setting_widgets(i, self.base_field,True)
|
||
|
||
def get_value(self) -> Optional[str]:
|
||
val = self.editor.get_value()
|
||
#return self.current_value
|
||
return val
|
||
|
||
def set_index(self, index):
|
||
if self.current_index != index:
|
||
self.combo.setCurrentIndex(index)
|
||
#self.on_current_index_changed(index, force)
|
||
|
||
def set_value(
|
||
self,
|
||
x: Any,
|
||
force=None,
|
||
update_all=False,
|
||
update_will_dialog=False,
|
||
update_heirs_dialog=False,
|
||
) -> None:
|
||
if not x:
|
||
if self.current_index == 0:
|
||
x = self.bal_window.bal_plugin.default_will_settings_relative()[self.base_field]
|
||
elif self.current_index == 1:
|
||
x = self.bal_window.bal_plugin.default_will_settings_absolute()[self.base_field]
|
||
if x != self.get_value():
|
||
self.editor.set_value(x)
|
||
self.current_value = x
|
||
self.bal_window.update_setting_widgets(x, self.base_field)
|
||
|
||
def set_read_only(self, read_only=True):
|
||
"""Show the value but make it non-editable.
|
||
|
||
Used everywhere except the "Build your will" wizard, where the date is
|
||
the only place the user is allowed to change it. The Raw/Date combo is
|
||
disabled and both editors become read-only with no spin buttons.
|
||
"""
|
||
self.combo.setEnabled(not read_only)
|
||
for w in self.editors:
|
||
w.set_read_only(read_only)
|
||
|
||
|
||
|
||
class TimeRawEditWidget(QWidget):
|
||
editingFinished = pyqtSignal()
|
||
|
||
def is_acceptable_locktime(self, value):
|
||
return True
|
||
|
||
def __init__(self, parent, time_edit=None):
|
||
super().__init__(parent)
|
||
self.editor = LockTimeRawEdit(parent, time_edit)
|
||
self.label = QLabel("")
|
||
self.label.setFixedWidth(10 * char_width_in_lineedit())
|
||
self.layout = QHBoxLayout(self)
|
||
self.layout.addWidget(self.editor)
|
||
self.layout.addWidget(self.label)
|
||
self.editor.editingFinished.connect(self.editingFinished.emit)
|
||
self.get_value = self.editor.get_value
|
||
self.set_value = self.editor.set_value
|
||
|
||
def set_read_only(self, read_only=True):
|
||
self.editor.setReadOnly(read_only)
|
||
# Match the Date editor: grey background when locked so the read-only
|
||
# state is visible; empty stylesheet restores the editable look.
|
||
self.editor.setStyleSheet(
|
||
"QLineEdit{background-color:#f0f0f0;}" if read_only else ""
|
||
)
|
||
|
||
|
||
|
||
class LockTimeRawEdit(QLineEdit, _LockTimeEditor):
|
||
def __init__(self, parent=None, time_edit=None):
|
||
QLineEdit.__init__(self, parent)
|
||
self.setFixedWidth(12 * char_width_in_lineedit())
|
||
self.textChanged.connect(self.numbify)
|
||
self.isdays = False
|
||
self.isyears = False
|
||
self.time_edit = time_edit
|
||
|
||
@staticmethod
|
||
def replace_str(text):
|
||
return str(text).replace("d", "").replace("y", "")
|
||
|
||
def checkbdy(self, s, pos, appendix):
|
||
try:
|
||
charpos = pos - 1
|
||
charpos = max(0, charpos)
|
||
charpos = min(len(s) - 1, charpos)
|
||
if appendix == s[charpos]:
|
||
s = self.replace_str(s) + appendix
|
||
pos = charpos
|
||
except Exception:
|
||
pass
|
||
return pos, s
|
||
|
||
def numbify(self):
|
||
text = self.text().strip()
|
||
chars = "0123456789dy"
|
||
pos = self.cursorPosition()
|
||
pos = len("".join([i for i in text[:pos] if i in chars]))
|
||
s = "".join([i for i in text if i in chars])
|
||
self.isdays = False
|
||
self.isyears = False
|
||
self.isblocks = False
|
||
|
||
pos, s = self.checkbdy(s, pos, "d")
|
||
pos, s = self.checkbdy(s, pos, "y")
|
||
|
||
if "d" in s:
|
||
self.isdays = True
|
||
if "y" in s:
|
||
self.isyears = True
|
||
|
||
if self.isdays:
|
||
s = self.replace_str(s) + "d"
|
||
if self.isyears:
|
||
s = self.replace_str(s) + "y"
|
||
self.blockSignals(True)
|
||
self.setText(s)
|
||
self.blockSignals(False)
|
||
self.current_value = s
|
||
self.setModified(self.hasFocus())
|
||
self.setCursorPosition(pos)
|
||
|
||
def get_value(self) -> Optional[str]:
|
||
try:
|
||
return str(self.text())
|
||
except Exception:
|
||
return None
|
||
|
||
def set_value(self, x: Any, force=True) -> None:
|
||
if x != self.get_value():
|
||
self.blockSignals(True)
|
||
self.setText(str(x))
|
||
self.blockSignals(False)
|
||
self.numbify()
|
||
|
||
|
||
|
||
class LockTimeDateEdit(QDateTimeEdit, _LockTimeEditor):
|
||
min_allowed_value = 0
|
||
max_allowed_value = _LockTimeEditor.get_max_allowed_timestamp()
|
||
|
||
def __init__(self, parent=None, time_edit=None):
|
||
QDateTimeEdit.__init__(self, parent)
|
||
self.setMinimumDateTime(datetime.fromtimestamp(self.min_allowed_value))
|
||
self.setMaximumDateTime(datetime.fromtimestamp(self.max_allowed_value))
|
||
#self.setDateTime(QDateTime.currentDateTime())
|
||
self.time_edit = time_edit
|
||
|
||
def set_read_only(self, read_only=True):
|
||
# Read-only display: keyboard editing disabled and the up/down spin
|
||
# arrows removed, so the date can only be changed from the wizard.
|
||
self.setReadOnly(read_only)
|
||
self.setButtonSymbols(
|
||
QAbstractSpinBox.ButtonSymbols.NoButtons
|
||
if read_only
|
||
else QAbstractSpinBox.ButtonSymbols.UpDownArrows
|
||
)
|
||
# A read-only QDateTimeEdit keeps a white background by default, which
|
||
# does not visually signal that it is locked. Paint it light grey (like
|
||
# the disabled combo/fee fields next to it) so the user sees at a glance
|
||
# that the date is not editable here; an empty stylesheet restores the
|
||
# default look when the field is made editable again (in the wizard).
|
||
self.setStyleSheet(
|
||
"QDateTimeEdit{background-color:#f0f0f0;}" if read_only else ""
|
||
)
|
||
|
||
def get_value(self) -> Optional[int]:
|
||
#dt = self.dateTime().toPyDateTime()
|
||
#locktime = int(time.mktime(dt.timetuple()))
|
||
#p#
|
||
#dt = dt_edit.dateTime()
|
||
## QDateTimets = dt.toSecsSinceEpoch()
|
||
|
||
dt = self.dateTime()
|
||
_ts = dt.toSecsSinceEpoch()
|
||
|
||
return _ts
|
||
|
||
|
||
def set_value(self, x: Any, force=False) -> None:
|
||
if not self.is_acceptable_locktime(x):
|
||
self.setDateTime(QDateTime.currentDateTime())
|
||
return
|
||
try:
|
||
x = int(x)
|
||
except Exception as e:
|
||
x = QDateTime.currentDateTime().timestamp()
|
||
finally:
|
||
# Use the overflow-safe converter: on Windows datetime.fromtimestamp
|
||
# raises OverflowError for timestamps past 2038 (e.g. NLOCKTIME_MAX).
|
||
_dt = BalTimestamp._safe_fromtimestamp(x)
|
||
#if self.alarm != dt:
|
||
self.setDateTime(_dt)
|
||
self.alarm = _dt
|
||
|
||
|
||
|
||
class ThresholdTimeWidget(BalTimeEditWidget):
|
||
# rich_text=True is used by the HelpButton, so HTML tags (<b>, <br>) render.
|
||
help_text = (
|
||
"<b>CHECK ALIVE</b><br><br>"
|
||
"Check to ask for invalidation.<br><br>"
|
||
"When less then this time is missing, ask to invalidate.<br>"
|
||
"If you fail to invalidate during this time, your transactions will be delivered to your heirs.<br><br>"
|
||
"if you choose Raw, you can insert various options based on suffix:<br>"
|
||
" - d: number of days after current day(ex: 1d means tomorrow)<br>"
|
||
" - y: number of years after currrent day(ex: 1y means one year from today)<br>"
|
||
)
|
||
label_text = "🚨"
|
||
#label_text = "Check Alive"
|
||
tooltip_text = "Check Alive"
|
||
base_field = "threshold"
|
||
|
||
def __init__(self, bal_window, parent, init_value=None):
|
||
if init_value is None:
|
||
init_value = bal_window.bal_plugin.WILL_SETTINGS.get()["threshold"]
|
||
super().__init__(bal_window, parent, init_value)
|
||
self.default_value = self.bal_window.bal_plugin.default_will_settings()[
|
||
"threshold"
|
||
]
|
||
|
||
|
||
|
||
class LockTimeWidget(BalTimeEditWidget):
|
||
# rich_text=True is used by the HelpButton, so HTML tags (<b>, <br>) render.
|
||
help_text = (
|
||
"<b>DELIVERY TIME</b><br><br>"
|
||
"Set Locktime for transactions.<br>"
|
||
"Any time is needed transaction will be anticipated by 1day<br><br>"
|
||
"if you choose Raw, you can insert various options based on suffix:<br>"
|
||
" - d: number of days after current day(ex: 1d means tomorrow)<br>"
|
||
" - y: number of years after currrent day(ex: 1y means one year from today)<br>"
|
||
)
|
||
label_text = "🚛"
|
||
#label_text = "Locktime"
|
||
tooltip_text = "Delivery time"
|
||
base_field = "locktime"
|
||
|
||
def __init__(self, bal_window, parent, init_value=None):
|
||
if init_value is None:
|
||
init_value = bal_window.bal_plugin.WILL_SETTINGS.get()["locktime"]
|
||
super().__init__(bal_window, parent, init_value)
|
||
self.default_value = self.bal_window.bal_plugin.default_will_settings()[
|
||
"locktime"
|
||
]
|
||
|
||
|
||
|
||
class WillSettingsWidget(QWidget):
|
||
|
||
def __init__(self, bal_window: "BalWindow", parent, layout_type="h",
|
||
read_only=True):
|
||
self.widgets = {}
|
||
QWidget.__init__(self, parent)
|
||
self.bal_window = bal_window
|
||
# When read_only=True (toolbars, Heirs tab) the delivery time, check
|
||
# alive and fee fields are display-only; they can only be edited from
|
||
# the "Build your will" wizard, which passes read_only=False.
|
||
self.read_only = read_only
|
||
box = QHBoxLayout(self) if layout_type == "h" else QVBoxLayout(self)
|
||
|
||
self.calendar_button = QPushButton()
|
||
self.calendar_button.setIcon(
|
||
read_QIcon_from_bytes(
|
||
self.bal_window.bal_plugin.read_file("icons/calendar.png")
|
||
)
|
||
)
|
||
# Tooltip so the icon is self-explanatory when hovered.
|
||
self.calendar_button.setToolTip(_("Calendar"))
|
||
self.calendar_button.clicked.connect(self.open_or_save_calendar)
|
||
self.widgets["locktime"] = LockTimeWidget(bal_window, self)
|
||
self.widgets["threshold"] = ThresholdTimeWidget(bal_window, self)
|
||
self.widgets["locktime"].valueEdited.connect(self.on_locktime_change)
|
||
self.widgets["threshold"].valueEdited.connect(self.on_locktime_change)
|
||
# self.widgets['baltx_fees'].valueChange.connect(self.bal_window.update_setting_widgets)
|
||
self.on_locktime_change()
|
||
self.widgets["baltx_fees"] = BalTxFeesWidget(bal_window, self)
|
||
if not hasattr(bal_window, "txfee_widgets"):
|
||
bal_window.txfee_widgets = []
|
||
|
||
w = self.widgets["baltx_fees"]
|
||
if w not in bal_window.txfee_widgets:
|
||
bal_window.txfee_widgets.append(w)
|
||
if layout_type == "h":
|
||
box.addWidget(self.widgets["locktime"])
|
||
box.addWidget(self.widgets["threshold"])
|
||
box.addWidget(self.calendar_button)
|
||
box.addWidget(self.widgets["baltx_fees"])
|
||
else:
|
||
# Vertical layout (the "Build your will" wizard): make every row the
|
||
# same width and left aligned so they all fit in one tidy block,
|
||
# instead of letting the calendar button and the fee field stretch to
|
||
# the dialog's right edge (which made them far wider than the date
|
||
# rows above).
|
||
#
|
||
# IMPORTANT: the leading icons keep their ORIGINAL size. The icons
|
||
# are HelpButtons, which already pin themselves to a fixed width
|
||
# (2.2 * char_width_in_lineedit()); we must NOT widen them, otherwise
|
||
# they look oversized compared with the original toolbar layout. We
|
||
# only need to (1) align the calendar row's left edge with the icons'
|
||
# original width and (2) cap every row to the date-row width.
|
||
locktime_w = self.widgets["locktime"]
|
||
threshold_w = self.widgets["threshold"]
|
||
fees_w = self.widgets["baltx_fees"]
|
||
|
||
# Original icon width (HelpButton's own fixed width); used only to
|
||
# offset the calendar button so its field starts under the others.
|
||
icon_w = locktime_w.prefix_widget.sizeHint().width()
|
||
|
||
# Common row width = natural width of the date rows (the reference).
|
||
row_w = max(
|
||
locktime_w.sizeHint().width(),
|
||
threshold_w.sizeHint().width(),
|
||
)
|
||
for w in (locktime_w, threshold_w, fees_w):
|
||
w.setFixedWidth(row_w)
|
||
|
||
# The calendar row has no prefix icon: wrap it so it starts with an
|
||
# empty spacer of the icon width (calendar field aligned with the
|
||
# date/fee fields) and cap it to the same total width as the rows
|
||
# above, so it no longer stretches to the dialog's right edge.
|
||
calendar_row = QWidget(self)
|
||
calendar_box = QHBoxLayout(calendar_row)
|
||
calendar_box.setContentsMargins(0, 0, 0, 0)
|
||
calendar_box.setSpacing(0)
|
||
calendar_spacer = QWidget()
|
||
calendar_spacer.setFixedWidth(icon_w)
|
||
calendar_box.addWidget(calendar_spacer)
|
||
calendar_box.addWidget(self.calendar_button)
|
||
calendar_row.setFixedWidth(row_w)
|
||
|
||
box.addWidget(locktime_w, alignment=Qt.AlignmentFlag.AlignLeft)
|
||
box.addWidget(threshold_w, alignment=Qt.AlignmentFlag.AlignLeft)
|
||
box.addWidget(calendar_row, alignment=Qt.AlignmentFlag.AlignLeft)
|
||
box.addWidget(fees_w, alignment=Qt.AlignmentFlag.AlignLeft)
|
||
|
||
if self.read_only:
|
||
self.widgets["locktime"].set_read_only(True)
|
||
self.widgets["threshold"].set_read_only(True)
|
||
self.widgets["baltx_fees"].set_read_only(True)
|
||
|
||
def create_alarms(self, alarm_start, alarm_end):
|
||
"""Delegate to the pure :meth:`BalCalendar.build_alarms` helper.
|
||
|
||
The reminder count comes from the plugin's ``ALARM_NUMBER`` setting; the
|
||
actual VALARM generation lives in ``BalCalendar`` so it can be unit
|
||
tested without a Qt widget.
|
||
"""
|
||
num_alarms = self.bal_window.bal_plugin.ALARM_NUMBER.get()
|
||
return BalCalendar.build_alarms(num_alarms, alarm_start, alarm_end)
|
||
|
||
def open_or_save_calendar(self):
|
||
now = BalCalendar.format_time(datetime.now())
|
||
|
||
locktime = self.widgets["locktime"].alarm
|
||
threshold = self.widgets["threshold"].alarm
|
||
# Use the larger of current time and threshold as alarm start
|
||
alarm_start_dt = max(datetime.now(), threshold)
|
||
alarm_end_dt = locktime
|
||
alarm_end = BalCalendar.format_time(alarm_end_dt)
|
||
|
||
heirs_details = "\r\n".join(f" {heir} - {self.bal_window.heirs[heir][0]}, {self.bal_window.heirs[heir][1]}" for heir in self.bal_window.heirs)
|
||
event_description = BalCalendar.ical_escape(
|
||
f"{self.bal_window.bal_plugin.EVENT_DESCRIPTION.get()}".replace("$wallet_name",str(self.bal_window.wallet)).replace("$heirs_complete",heirs_details)
|
||
)
|
||
uid = f"bal-{str(self.bal_window.wallet)}"
|
||
summary = BalCalendar.ical_escape(
|
||
f"{self.bal_window.bal_plugin.EVENT_SUMMARY.get()}".replace("$wallet_name",str(self.bal_window.wallet))
|
||
)
|
||
# The event date/time is the lower value between the will-settings
|
||
# locktime (here represented by ``alarm_end_dt``) and the valid will
|
||
# transaction with the lowest locktime.
|
||
min_tx_locktime = Will.get_min_locktime(
|
||
self.bal_window.willitems, None
|
||
)
|
||
event_ts = BalCalendar.compute_event_timestamp(
|
||
alarm_end_dt.timestamp(), min_tx_locktime
|
||
)
|
||
event_dt = datetime.fromtimestamp(event_ts)
|
||
event_time = BalCalendar.format_time(event_dt)
|
||
|
||
lines = [
|
||
"BEGIN:VCALENDAR",
|
||
"VERSION:2.0",
|
||
f"PRODID:-//Bitcoin After Life//Electrum Plugin/{BalPlugin.__version__}",
|
||
"BEGIN:VEVENT",
|
||
f"UID:{uid}",
|
||
f"DTSTAMP:{now}",
|
||
f"DTSTART:{event_time}",
|
||
f"DTEND:{event_time}",
|
||
f"SUMMARY:{summary}",
|
||
f"DESCRIPTION:{event_description}",
|
||
]
|
||
lines.extend(self.create_alarms(alarm_start_dt, alarm_end_dt))
|
||
lines.extend([
|
||
"END:VEVENT",
|
||
"END:VCALENDAR",
|
||
])
|
||
|
||
lines = [s.rstrip("\r\n") for s in lines]
|
||
ics_content = "\r\n".join(lines) + "\r\n"
|
||
self.temp_path = BalCalendar.write_temp_ics(ics_content)
|
||
opened = BalCalendar.open_with_default_app(
|
||
self.bal_window.bal_plugin.CALENDAR_APP.get(), self.temp_path
|
||
)
|
||
if opened:
|
||
_logger.info(f"File opened with default app: {self.temp_path}")
|
||
else:
|
||
export_meta_gui(
|
||
self.bal_window.window, f"will_event.ics",self.save_to_cwd
|
||
|
||
)
|
||
|
||
|
||
def save_to_cwd(self, filename="event.ics"):
|
||
"""Copy the temporary .ics file to ``filename`` in the current dir.
|
||
|
||
Overwrites the destination file if it already exists. Returns the
|
||
absolute path of the written file.
|
||
"""
|
||
target = os.path.abspath(filename)
|
||
_logger.debug(f"save_to_cwd {self.temp_path},{filename}")
|
||
with open(self.temp_path, "rb") as src, open(target, "wb") as dst:
|
||
dst.write(src.read())
|
||
return target
|
||
|
||
def on_locktime_change(self):
|
||
locktime = self.widgets["locktime"].get_value()
|
||
threshold = self.widgets["threshold"].get_value()
|
||
locktime = BalTimestamp(locktime)
|
||
threshold = BalTimestamp(threshold)
|
||
|
||
min_locktime = min(
|
||
Will.get_min_locktime(self.bal_window.willitems, NLOCKTIME_MAX),
|
||
locktime.to_timestamp(),
|
||
)
|
||
td = threshold.to_date(min_locktime, True)
|
||
self.widgets["threshold"].alarm=td
|
||
self.bal_window.will_settings["real_threshold"]=td.timestamp()
|
||
try:
|
||
self.widgets["threshold"].editor.label.setText(td.strftime("%Y-%m-%d"))
|
||
except Exception as _e:
|
||
pass
|
||
|
||
td = locktime.to_date()
|
||
alarm = BalTimestamp(min_locktime).to_date()
|
||
self.widgets["locktime"].alarm=alarm
|
||
self.bal_window.will_settings["real_locktime"]=td.timestamp()
|
||
try:
|
||
self.widgets["locktime"].editor.label.setText(td.strftime("%Y-%m-%d"))
|
||
except Exception as _e:
|
||
pass
|
||
|
||
|
||
|
||
class PercAmountEdit(BTCAmountEdit):
|
||
def __init__(self, decimal_point, is_int=False, parent=None, *, max_amount=None):
|
||
super().__init__(decimal_point, is_int, parent, max_amount=max_amount)
|
||
|
||
def numbify(self):
|
||
text = self.text().strip()
|
||
if text == "!":
|
||
self.shortcut.emit()
|
||
return
|
||
pos = self.cursorPosition()
|
||
chars = "0123456789%"
|
||
chars += DECIMAL_POINT
|
||
|
||
s = "".join([i for i in text if i in chars])
|
||
|
||
if "%" in s:
|
||
self.is_perc = True
|
||
s = s.replace("%", "")
|
||
else:
|
||
self.is_perc = False
|
||
|
||
if DECIMAL_POINT in s:
|
||
p = s.find(DECIMAL_POINT)
|
||
s = s.replace(DECIMAL_POINT, "")
|
||
s = s[:p] + DECIMAL_POINT + s[p : p + 8]
|
||
if self.is_perc:
|
||
s += "%"
|
||
|
||
self.setText(s)
|
||
self.setModified(self.hasFocus())
|
||
self.setCursorPosition(pos)
|
||
|
||
def _get_amount_from_text(self, text: str) -> Union[None, Decimal, int]:
|
||
try:
|
||
text = text.replace(DECIMAL_POINT, ".")
|
||
text = text.replace("%", "")
|
||
return (Decimal)(text)
|
||
except Exception:
|
||
return None
|
||
|
||
def _get_text_from_amount(self, amount):
|
||
out = super()._get_text_from_amount(amount)
|
||
if self.is_perc:
|
||
out += "%"
|
||
return out
|
||
|
||
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())
|
||
if len(self.text()) == 0:
|
||
painter.drawText(
|
||
textRect,
|
||
int(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter),
|
||
self.base_unit() + " or perc value",
|
||
)
|
||
|
||
|
||
|
||
class BalLineEdit(QLineEdit):
|
||
def __init__(self,variable):
|
||
QLineEdit.__init__(self)
|
||
self.setText(variable.get())
|
||
def on_edit():
|
||
variable.set(self.text())
|
||
self.editingFinished.connect(on_edit)
|
||
|
||
|
||
class BalTextEdit(QTextEdit):
|
||
def __init__(self,variable):
|
||
QTextEdit.__init__(self)
|
||
self.setPlainText(variable.get())
|
||
def on_edit():
|
||
variable.set(self.toPlainText())
|
||
self.textChanged.connect(on_edit)
|
||
|
||
|
||
class BalCheckBox(QCheckBox):
|
||
def __init__(self, variable, on_click=None):
|
||
QCheckBox.__init__(self)
|
||
self.setChecked(variable.get())
|
||
self.on_click = on_click
|
||
|
||
def on_check(v):
|
||
variable.set(v == 2)
|
||
#variable.get()
|
||
if self.on_click:
|
||
self.on_click()
|
||
|
||
self.stateChanged.connect(on_check)
|
||
|
||
|
||
|
||
class WillWidget(QWidget):
|
||
def __init__(self, father=None, parent=None):
|
||
super().__init__()
|
||
vlayout = QVBoxLayout()
|
||
self.setLayout(vlayout)
|
||
self.will = parent.bal_window.willitems
|
||
self._bal_parent = parent
|
||
for w in self.will:
|
||
if (
|
||
self.will[w].get_status("REPLACED")
|
||
and self._bal_parent.bal_window.bal_plugin._hide_replaced
|
||
):
|
||
continue
|
||
if (
|
||
self.will[w].get_status("INVALIDATED")
|
||
and self._bal_parent.bal_window.bal_plugin._hide_invalidated
|
||
):
|
||
continue
|
||
f = self.will[w].father
|
||
if father == f:
|
||
qwidget = QWidget()
|
||
# childWidget = QWidget()
|
||
hlayout = QHBoxLayout(qwidget)
|
||
qwidget.setLayout(hlayout)
|
||
vlayout.addWidget(qwidget)
|
||
detailw = QWidget()
|
||
detaillayout = QVBoxLayout()
|
||
detailw.setLayout(detaillayout)
|
||
|
||
willpushbutton = QPushButton(w)
|
||
|
||
willpushbutton.clicked.connect(
|
||
partial(self._bal_parent.bal_window.show_transaction, txid=w)
|
||
)
|
||
detaillayout.addWidget(willpushbutton)
|
||
locktime = str(BalTimestamp(self.will[w].tx.locktime))
|
||
creation = str(BalTimestamp(self.will[w].time))
|
||
|
||
def qlabel(title, value):
|
||
label = "<b>" + _(str(title)) + f":</b>\t{str(value)}"
|
||
return QLabel(label)
|
||
|
||
detaillayout.addWidget(qlabel("Locktime", locktime))
|
||
detaillayout.addWidget(qlabel("Creation Time", creation))
|
||
try:
|
||
total_fees = (
|
||
self.will[w].tx.input_value() - self.will[w].tx.output_value()
|
||
)
|
||
except Exception:
|
||
total_fees = -1
|
||
decoded_fees = total_fees
|
||
fee_per_byte = round(total_fees / self.will[w].tx.estimated_size(), 3)
|
||
fees_str = str(decoded_fees) + " (" + str(fee_per_byte) + " sats/vbyte)"
|
||
detaillayout.addWidget(qlabel("Transaction fees:", fees_str))
|
||
detaillayout.addWidget(qlabel("Status:", self.will[w].status))
|
||
detaillayout.addWidget(QLabel(""))
|
||
detaillayout.addWidget(QLabel("<b>Heirs:</b>"))
|
||
for heir in self.will[w].heirs:
|
||
if 'w!ll3x3c"' not in heir:
|
||
decoded_amount = Util.decode_amount(
|
||
self.will[w].heirs[heir][3], self._bal_parent.decimal_point
|
||
)
|
||
detaillayout.addWidget(
|
||
qlabel(
|
||
heir, f"{decoded_amount} {self._bal_parent.base_unit_name}"
|
||
)
|
||
)
|
||
if self.will[w].we:
|
||
detaillayout.addWidget(QLabel(""))
|
||
detaillayout.addWidget(QLabel(_("<b>Willexecutor:</b:")))
|
||
decoded_amount = Util.decode_amount(
|
||
self.will[w].we["base_fee"], self._bal_parent.decimal_point
|
||
)
|
||
|
||
detaillayout.addWidget(
|
||
qlabel(
|
||
self.will[w].we["url"],
|
||
f"{decoded_amount} {self._bal_parent.base_unit_name}",
|
||
)
|
||
)
|
||
detaillayout.addStretch()
|
||
pal = QPalette()
|
||
pal.setColor(
|
||
QPalette.ColorRole.Window, QColor(status_color(self.will[w]))
|
||
)
|
||
detailw.setAutoFillBackground(True)
|
||
detailw.setPalette(pal)
|
||
|
||
hlayout.addWidget(detailw)
|
||
hlayout.addWidget(WillWidget(w, parent=parent))
|
||
|
||
|