diff --git a/bal/gui/qt/calendar.py b/bal/gui/qt/calendar.py
index 8f8bf91..82fe601 100644
--- a/bal/gui/qt/calendar.py
+++ b/bal/gui/qt/calendar.py
@@ -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
diff --git a/bal/gui/qt/dialogs.py b/bal/gui/qt/dialogs.py
index 62bd906..3196111 100644
--- a/bal/gui/qt/dialogs.py
+++ b/bal/gui/qt/dialogs.py
@@ -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(
diff --git a/bal/gui/qt/plugin.py b/bal/gui/qt/plugin.py
index 9a18490..294c86e 100644
--- a/bal/gui/qt/plugin.py
+++ b/bal/gui/qt/plugin.py
@@ -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)
diff --git a/bal/gui/qt/widgets.py b/bal/gui/qt/widgets.py
index 9e135c1..ddb8514 100644
--- a/bal/gui/qt/widgets.py
+++ b/bal/gui/qt/widgets.py
@@ -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:
"
" - d: number of days after current day(ex: 1d means tomorrow)
"
" - y: number of years after current day(ex: 1y means one year from today)
"
+ "
Note: the date/time is expressed in your computer's "
+ "Local time (not UTC time).
"
)
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:
"
" - d: number of days after current day(ex: 1d means tomorrow)
"
" - y: number of years after currrent day(ex: 1y means one year from today)
"
+ "
Note: the date/time is expressed in your computer's "
+ "Local time (not UTC time).
"
)
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):
diff --git a/bal/gui/qt/window.py b/bal/gui/qt/window.py
index eb89178..4408376 100644
--- a/bal/gui/qt/window.py
+++ b/bal/gui/qt/window.py
@@ -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