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