v0.5.10: Check Alive/BASIC fixes, UX improvements, Raw-Date handling.

This commit is contained in:
2026-07-05 09:03:57 +00:00
parent 4c81290954
commit 527ae95c75
5 changed files with 229 additions and 88 deletions

View File

@@ -47,13 +47,24 @@ class BalCalendarButton(QToolButton):
# ------------------------------------------------------------------ #
def _ensure_ics(self):
"""Generate the .ics content and cache the temp file path."""
"""Generate the .ics content and cache the temp file path.
If the provider returns no content because there are no reminder events
to save (the delivery date is too close or already passed), warn the
user and do NOT create a file (ToDo #2).
"""
try:
content = self._ics_provider()
if content:
self._calendar_temp_path = BalCalendar.write_temp_ics(content)
else:
self._calendar_temp_path = None
self._bal_window.show_warning(
_(
"No reminders were saved: the delivery date is too "
"close (or already passed)"
)
)
except Exception as e:
_logger.error(f"failed to generate .ics: {e}")
self._calendar_temp_path = None

View File

@@ -723,7 +723,7 @@ class BalBuildWillDialog(BalDialog):
self.msg_set_status(
_("Checking your will"),
None,
_("Inheritance already executed (on blockchain)"),
_("An inheritance of this wallet is already executed (on blockchain)"),
self.COLOR_OK,
)
elif executed_status == "MEMPOOL":
@@ -776,8 +776,14 @@ class BalBuildWillDialog(BalDialog):
if not txs:
self.msg_set_building(
_(
"Balance is too low, or CheckAlive is in the "
"past. Skipped"
"Could not build the will ! Possible reasons:\n"
"1- the Balance of wallet is too low to cover the "
"fees for miners and will executors,\n"
"2- the Heirs' shares are below the minimum (Dust "
"UTXO, less than 546 Satoshi),\n"
"3- the Check Alive Date/Time is later than the "
"delivery time (it must be earlier),\n"
"Skipped"
),
# Orange (warning) instead of red (error): an empty
# wallet after the inheritance was executed is a NORMAL
@@ -1531,6 +1537,12 @@ class BalBuildWillDialog(BalDialog):
]
total = len(offsets)
# ToDo #2: if no reminder falls in the future (the delivery date is
# too close or already passed), there are no events to write.
# Return None so the caller shows a clear warning instead of
# producing an empty, seemingly-broken .ics file.
if total == 0:
return None
for idx, offset in enumerate(offsets, start=1):
event_dt = BalCalendar.format_time(locktime - timedelta(days=offset))
summary = BalCalendar.ical_escape(

View File

@@ -486,6 +486,7 @@ class Plugin(BalPlugin):
user_type_combo.setCurrentIndex(0)
user_type_combo.blockSignals(False)
self.USER_TYPE.set("basic")
self._apply_editor_default_on_toolbars()
self.update_all()
return
self.USER_TYPE.set("advanced" if idx == 1 else "basic")
@@ -500,6 +501,11 @@ class Plugin(BalPlugin):
reset_btn_6, reset_btn_7, reset_btn_8, reset_btn_9, reset_btn_10,
reset_btn_auto_sign):
w.setVisible(not basic)
# Opzione 2: apply the per-mode Raw/Date editor default ONLY here, on
# a real USER TYPE change (not inside update_all/CHECK), so pressing
# CHECK never resets a manual Date/RAW choice. update_all() below
# still refreshes the transaction list and the combo visibility.
self._apply_editor_default_on_toolbars()
self.update_all()
user_type_combo.currentIndexChanged.connect(on_user_type_change)
@@ -833,6 +839,28 @@ class Plugin(BalPlugin):
for _k, w in self.bal_windows.items():
w.update_all()
def _apply_editor_default_on_toolbars(self):
"""Apply the per-mode Raw/Date editor default to the WILL/HEIR toolbars.
Called ONLY on a real USER TYPE (BASIC<->ADVANCED) change - never from
update_all()/CHECK - so pressing CHECK does not reset a manual Date/RAW
choice (Opzione 2). Guarded so a missing widget never breaks the switch.
"""
for _k, w in self.bal_windows.items():
for _list in (getattr(w, "heir_list_widget", None),
getattr(w, "will_list_widget", None)):
settings_widget = getattr(_list, "will_settings_widget", None)
apply_default = getattr(
settings_widget, "apply_user_type_editor_default", None
)
if callable(apply_default):
try:
apply_default()
except Exception as _e:
_logger.debug(
f"apply_user_type_editor_default error: {_e}"
)
def get_window_title(self, title):
return _("BAL - ") + _(title)

View File

@@ -291,18 +291,39 @@ class BalTimeEditWidget(QWidget, _LockTimeEditor):
# combo is hidden. We keep a flag so the rest of __init__ can force the
# Date editor regardless of the stored value's format.
self._basic_mode = self.bal_window.bal_plugin.is_basic_mode()
# Tracks whether the user PICKED an editor by hand from the Raw/Date
# selector (set only on the combo's `activated` signal, i.e. real user
# interaction). It lets the runtime BASIC<->ADVANCED switch respect a
# manual "Date" choice in ADVANCED instead of forcing RAW back. It stays
# False for programmatic changes (defaults, switches).
self._user_picked_editor = False
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
# Force the calendar ("Date") editor in BASIC mode so the user always
# picks a date and never sees the RAW input ("30d"/"1y" style).
# Default editor per mode (owner request):
# * BASIC -> Date editor (index 1); the Raw/Date selector is hidden,
# so the user always picks a date and never sees RAW.
# * ADVANCED -> RAW editor (index 0) by default; the selector stays
# visible so the user can switch to Date manually.
if self._basic_mode:
default_index = 1
else:
default_index = 0
# In ADVANCED we default to RAW. If the stored value is an absolute
# timestamp (a bare number), showing it in RAW would display that
# raw number; substitute the relative default ("1y"/"30d") so the
# RAW editor opens with a human-readable duration instead.
try:
int(default_locktime)
is_absolute = True
except Exception:
is_absolute = False
if is_absolute:
default_locktime = (
self.bal_window.bal_plugin.default_will_settings_relative()[
self.base_field
]
)
#hbox.addWidget(QLabel(self.label_text))
help_button=HelpButton(self.help_text)
help_button.setText(self.label_text)
@@ -316,6 +337,11 @@ class BalTimeEditWidget(QWidget, _LockTimeEditor):
# 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)
# `activated` fires ONLY on real user interaction with the selector (not
# on programmatic setCurrentIndex), so we use it to remember that the
# user picked the editor by hand. This is what lets the runtime
# BASIC<->ADVANCED switch respect a manual "Date" choice in ADVANCED.
self.combo.activated.connect(self._on_user_picked_editor)
for w in self.editors:
w.setVisible(False)
@@ -360,6 +386,16 @@ class BalTimeEditWidget(QWidget, _LockTimeEditor):
update_heirs_dialog,
)
def _on_user_picked_editor(self, i):
"""Record that the user chose the Raw/Date editor by hand.
Connected to the combo's ``activated`` signal, which only fires on real
user interaction (not on programmatic ``setCurrentIndex``). Used by the
runtime BASIC<->ADVANCED switch to respect a manual "Date" choice in
ADVANCED instead of forcing RAW back.
"""
self._user_picked_editor = True
def on_current_index_changed(self, i):
self.current_index = i
for w in self.editors:
@@ -408,9 +444,11 @@ class BalTimeEditWidget(QWidget, _LockTimeEditor):
(owner request: keep the current value to avoid confusing the delivery
date with the inheritance). It is called from
``WillSettingsWidget.apply_user_type_visibility()`` (triggered by
``BalWindow.update_all()`` on a BASIC<->ADVANCED switch), so toggling the
mode takes effect immediately on the already-existing WILL/HEIR toolbars
without restarting Electrum.
``BalWindow.update_all()``, which also runs on every CHECK/refresh), so
it must be side-effect free on the editor - otherwise pressing CHECK
would reset a manual Date/RAW choice back to the default. The actual
per-mode editor default lives in ``apply_user_type_editor_default()``,
which is called ONLY on a real USER TYPE change.
It is safe to call repeatedly and on either layout (horizontal toolbar or
vertical wizard): it only flips the visibility of the Raw/Date combo.
@@ -421,10 +459,37 @@ class BalTimeEditWidget(QWidget, _LockTimeEditor):
# If the mode cannot be read, show the combo (the safe, most
# capable default for an existing widget).
basic = False
# Hidden in BASIC, visible in ADVANCED. The current value/editor is left
# untouched on purpose (see the docstring).
self.combo.setVisible(not basic)
def apply_user_type_editor_default(self):
"""Set the ACTIVE Raw/Date editor to the per-mode default.
Split out from ``apply_user_type_visibility`` (Opzione 2) so it runs
ONLY on a real BASIC<->ADVANCED USER TYPE change, not on every
``update_all()``/CHECK refresh. That is what stopped pressing CHECK from
resetting a manual Date choice back to RAW.
Behaviour:
* BASIC -> always the Date editor (index 1); the selector is hidden,
so the field must not stay on RAW.
* ADVANCED -> RAW by default (index 0), UNLESS the user previously
picked an editor by hand (``_user_picked_editor``), in
which case their choice is left untouched.
"""
try:
basic = self.bal_window.bal_plugin.is_basic_mode()
except Exception:
basic = False
try:
if basic:
self.set_index(1)
else:
if not self._user_picked_editor:
self.set_index(0)
except Exception:
# Never let a cosmetic editor switch break mode toggling.
pass
def set_value(
self,
x: Any,
@@ -653,6 +718,8 @@ class ThresholdTimeWidget(BalTimeEditWidget):
"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 current day(ex: 1y means one year from today)<br>"
"<br><b>Note:</b> the date/time is expressed in your computer's "
"<b>Local time</b> (not UTC time).<br>"
)
label_text = "🚨"
#label_text = "Check Alive"
@@ -682,6 +749,8 @@ class LockTimeWidget(BalTimeEditWidget):
"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>"
"<br><b>Note:</b> the date/time is expressed in your computer's "
"<b>Local time</b> (not UTC time).<br>"
)
label_text = "🚛"
#label_text = "Locktime"
@@ -711,6 +780,12 @@ class WillSettingsWidget(QWidget):
# 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
# Remember which layout this widget was built with, so the per-update
# hooks (apply_editable_dates / apply_user_type_visibility) can tell the
# main-window toolbar ("h") apart from the wizard (vertical) - needed
# for the BASIC-mode Check-Alive "visible but read-only in the main
# window only" behaviour.
self.layout_type = layout_type
box = QHBoxLayout(self) if layout_type == "h" else QVBoxLayout(self)
self.calendar_button = BalCalendarButton(self.bal_window, self._ics_provider)
@@ -726,13 +801,13 @@ class WillSettingsWidget(QWidget):
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)
# SIMPLE / ADVANCED: in BASIC mode hide the whole "Check Alive"
# (threshold) row, including its leading icon. The widget is still
# created and kept in self.widgets so the rest of the code (and the
# saved settings) keep working; it is only hidden from view. The
# Delivery time (locktime) row stays visible. We hide it after creation
# so both the horizontal (toolbar/Heirs) and vertical (wizard) layouts
# below add an already-hidden widget.
# SIMPLE / ADVANCED: in BASIC mode the "Check Alive" (threshold) row is
# hidden EVERYWHERE (both the main-window toolbar and the wizard). The
# user must never see or touch the Check Alive in BASIC. The widget is
# still created and kept in self.widgets so the rest of the code and the
# saved settings keep working; it is only hidden from view. (This
# reverts the v0.5.2 experiment that had shown it read-only in the main
# window - owner asked to hide it again.)
if bal_window.bal_plugin.is_basic_mode():
self.widgets["threshold"].setVisible(False)
# self.widgets['baltx_fees'].valueChange.connect(self.bal_window.update_setting_widgets)
@@ -954,7 +1029,9 @@ class WillSettingsWidget(QWidget):
basic = False
threshold = self.widgets.get("threshold")
if threshold is not None:
# Hidden in BASIC, visible in ADVANCED.
# Hidden in BASIC, visible in ADVANCED - in BOTH layouts (main
# window toolbar and wizard). Reverts the v0.5.2 experiment that
# kept it visible in the main window in BASIC.
threshold.setVisible(not basic)
# Task #04: also re-apply the Raw/Date selector visibility on BOTH the
@@ -969,6 +1046,19 @@ class WillSettingsWidget(QWidget):
if callable(apply_combo):
apply_combo()
def apply_user_type_editor_default(self):
"""Apply the per-mode Raw/Date editor default to both time fields.
Called ONLY on a real BASIC<->ADVANCED USER TYPE change (not on every
refresh/CHECK), so a manual Date/RAW choice is preserved when the user
just presses CHECK. See BalTimeEditWidget.apply_user_type_editor_default.
"""
for field in ("locktime", "threshold"):
widget = self.widgets.get(field)
apply_default = getattr(widget, "apply_user_type_editor_default", None)
if callable(apply_default):
apply_default()
def open_or_save_calendar(self):
"""Build and save an .ics calendar file with SEPARATE reminder events.
@@ -1248,6 +1338,43 @@ class WillSettingsWidget(QWidget):
except Exception as _e:
pass
# VISUAL WARNING (ToDo #5): the Check Alive must always be EARLIER than
# the delivery time. If the user sets a Check Alive that is later than
# (or equal to) the delivery time, tint its box with a soft red so the
# problem is obvious immediately, instead of only surfacing later as a
# build error. Applied only in ADVANCED mode (in BASIC the Check Alive
# is hidden and not editable, so there is nothing to warn about). The
# tint is cleared again as soon as the relationship becomes valid.
try:
SOFT_RED = "#FCE4E4"
threshold_box = self.widgets["threshold"]
if not self.bal_window.bal_plugin.is_basic_mode():
if threshold.to_timestamp() >= locktime.to_timestamp():
# Only set a background colour, and DO NOT touch the border:
# styling the border of a QDateTimeEdit/QAbstractSpinBox via
# stylesheet makes Qt stop drawing its native up/down spin
# arrows. Scoping the rule to the inner editor widget types
# (rather than the whole container) keeps the tint on the
# value box while leaving the arrows intact.
threshold_box.setStyleSheet(
"QDateTimeEdit, QLineEdit, QAbstractSpinBox "
"{ background-color: %s; }" % SOFT_RED
)
threshold_box.setToolTip(
_(
"The Check Alive date must be earlier than the "
"delivery time."
)
)
else:
threshold_box.setStyleSheet("")
threshold_box.setToolTip("")
else:
# BASIC: never show the warning styling (field is hidden).
threshold_box.setStyleSheet("")
except Exception as _e:
pass
class PercAmountEdit(BTCAmountEdit):

View File

@@ -325,6 +325,10 @@ class BalWindow:
raise NoWillExecutorNotPresent(
"No Will-Executor or backup transaction selected"
)
# date_to_check already carries the correct reference timestamp for
# the current mode (the Check Alive in ADVANCED, or "now" in BASIC -
# see init_class_variables). So build the will directly against it;
# no per-mode branch is needed here anymore.
txs = self.heirs.get_transactions(
self.bal_plugin,
self.window.wallet,
@@ -451,74 +455,33 @@ class BalWindow:
for heir, value in updates.items():
self.heirs[heir] = value
# How long BEFORE the delivery time (locktime) the auto-computed Check
# Alive threshold is placed in BASIC mode (see compute_date_to_check).
# Owner-approved value: 2 hours. Kept as a named constant so the offset
# is documented in one place and easy to tune later if needed.
BASIC_MODE_CHECK_ALIVE_OFFSET_SECONDS = 2 * 60 * 60
@staticmethod
def compute_date_to_check(is_basic_mode, locktime_setting, threshold_setting):
"""Resolve the "Check Alive" reference timestamp (``date_to_check``).
SIMPLE / ADVANCED: the "Check Alive" (threshold) field is hidden in
BASIC mode, so the user can never keep it in sync with the delivery
time (locktime) when they anticipate/postpone it from the wizard.
Left untouched, the threshold stays at its old/default value (e.g.
"today + 11 months") and, if the user later sets a delivery date
EARLIER than that, two checks elsewhere (the "locktime is lower than
threshold" guard in ``build_inheritance_transaction``, and
``Will.check_will_expired`` via ``date_to_check``) misfire and
block/expire the will - even though the postpone/expiry check itself
is meant to be inert in BASIC mode (see the ``is_basic_mode()`` guard
in ``init_class_variables``).
FIX: in BASIC mode only, ignore the stored threshold and instead
derive ``date_to_check`` LIVE from the current delivery time
(``locktime_setting``), placed a small, fixed offset BEFORE it
(``BASIC_MODE_CHECK_ALIVE_OFFSET_SECONDS`` = 2 hours). This keeps
``date_to_check`` always < locktime by construction, so anticipating
or postponing the delivery date in BASIC mode never triggers a
spurious "expired" / "threshold" error. In ADVANCED mode nothing
changes: the stored threshold is used as-is, exactly like before.
Args:
is_basic_mode: ``BalPlugin.is_basic_mode()`` result.
locktime_setting: the current ``will_settings["locktime"]`` value
(relative string like ``"1y"`` or an absolute timestamp).
threshold_setting: the current ``will_settings["threshold"]``
value, used as-is in ADVANCED mode and as a fallback if the
locktime cannot be parsed.
Returns:
float: the resolved ``date_to_check`` UNIX timestamp.
"""
if is_basic_mode:
try:
locktime_ts = Util.parse_locktime_string(locktime_setting)
# Util.parse_locktime_string never raises: on anything it
# cannot parse it silently returns 0 (see bal/core/util.py).
# Treat that sentinel the same as a parse error, otherwise we
# would compute a nonsensical date_to_check near the UNIX
# epoch instead of falling back to the stored threshold.
if locktime_ts:
return (
locktime_ts
- BalWindow.BASIC_MODE_CHECK_ALIVE_OFFSET_SECONDS
)
except Exception:
pass
return BalTimestamp(threshold_setting).to_timestamp()
def init_class_variables(self):
if not self.heirs:
raise NoHeirsException(_("Heirs are not defined"))
try:
self.date_to_check = BalWindow.compute_date_to_check(
self.bal_plugin.is_basic_mode(),
self.will_settings["locktime"],
self.will_settings["threshold"],
)
# SIMPLE / ADVANCED root behaviour of the "Check Alive" (threshold).
#
# self.date_to_check is the single reference timestamp that EVERY
# downstream validity check reads: the build filter
# (get_locktimes/get_transactions), the heir-count in
# check_willexecutors_and_heirs ("No Heirs"), check_will_expired,
# check_amounts, and the "locktime is lower than threshold" guard.
#
# In BASIC mode the Check Alive is HIDDEN and NOT editable by the
# user, so it must never govern any of those checks. Setting
# date_to_check to the Check Alive there caused the will to be
# wrongly blocked whenever the (fixed) Check Alive ended up later
# than the delivery time (e.g. "No Heirs" even with heirs present,
# or a will that refused to (re)build). We therefore set
# date_to_check to "now" in BASIC: every check is then evaluated
# against the current moment, i.e. the Check Alive effectively does
# not exist, while the delivery time (locktime) is still fully
# enforced. ADVANCED mode keeps the user-controlled Check Alive
# exactly as before.
if self.bal_plugin.is_basic_mode():
self.date_to_check = datetime.now().timestamp()
else:
self.date_to_check = BalTimestamp(self.will_settings['threshold']).to_timestamp()
# found = False
# NOTE: block-height tracking removed (A1) - locktimes are always
# UNIX timestamps now, so we no longer read the current block height