v0.4.8: seven UX fixes for the BAL inheritance plugin

- #04: Raw/Date selector now reappears on the WILL/HEIR tabs when switching to
  ADVANCED (and when a raw value is pushed from the wizard), keeping the current
  value/editor. New BalTimeEditWidget.apply_user_type_visibility() wired into
  WillSettingsWidget for both the locktime and check-alive boxes.
- #3: Building Will report window minimum height 500 -> 450 px.
- #5: 'User Type' setting moved to the bottom, above 'Rebroadcast transactions'.
- #6: enabling ADVANCED now requires typing 'at My Risk' (case-insensitive);
  wrong phrase or cancel reverts to BASIC. QInputDialog added to common import.
- #7a: on CHECK with an emptied wallet, an extra reassuring line is shown on
  'Checking your will': 'Inheritance already executed (on blockchain)' in GREEN
  (CONFIRMED) or 'Inheritance in mempool (waiting confirmation)' in ORANGE
  (MEMPOOL). New _executed_inheritance_status() helper.
- #7b: 'Balance is too low, or CheckAlive is in the past. Skipped' (space added)
  recoloured to ORANGE instead of red.
- Reset button renamed 'Reset setting' -> 'Reset to Default Setting'.
- Added tests/test_group_h_v048.py (8 tests). Version 0.4.7 -> 0.4.8.
  Full suite: 266 passed; ruff clean; CHANGELOG #24; memory updated (#04 done).
This commit is contained in:
2026-06-28 23:02:40 -04:00
parent 0f2b3a4146
commit 33e8a7533d
11 changed files with 425 additions and 38 deletions

View File

@@ -1 +1 @@
0.4.7
0.4.8

View File

@@ -34,4 +34,4 @@ The plugin targets Electrum 4.7.2 (the last stable release exposing
``json_db.register_dict``) and PyQt6.
"""
__version__ = "0.4.7"
__version__ = "0.4.8"

View File

@@ -91,7 +91,7 @@ class BalPlugin(BasePlugin):
"""
_version = None
__version__ = "0.4.7" # AUTOMATICALLY GENERATED DO NOT EDIT
__version__ = "0.4.8" # AUTOMATICALLY GENERATED DO NOT EDIT
# Command used to open an .ics calendar file, per operating system.
default_app = {

View File

@@ -55,9 +55,9 @@ from PyQt6.QtGui import (QColor, QPainter, QPalette, QStandardItem,
QStandardItemModel)
from PyQt6.QtWidgets import (QAbstractItemView, QAbstractSpinBox, QCheckBox,
QComboBox, QDateTimeEdit, QGridLayout, QHBoxLayout,
QLabel, QLineEdit, QTextEdit, QMenu, QMenuBar,
QPushButton, QScrollArea, QSizePolicy, QSpinBox,
QStackedWidget, QStyle, QStyleOptionFrame,
QInputDialog, QLabel, QLineEdit, QTextEdit, QMenu,
QMenuBar, QPushButton, QScrollArea, QSizePolicy,
QSpinBox, QStackedWidget, QStyle, QStyleOptionFrame,
QVBoxLayout, QWidget, QDialog)
# --- Core (GUI-free) logic layer ---

View File

@@ -555,11 +555,12 @@ class BalBuildWillDialog(BalDialog):
self.scroll_area = QScrollArea(self)
self.scroll_area.setWidgetResizable(True)
self.scroll_area.setWidget(self.message_label)
# Open the report area already ~500px tall (owner request, allegato1:
# the previous ~140px area was far too short). The dialog may still grow
# up to 700px to fit a few more lines; beyond that the vertical
# scrollbar takes over and the bottom buttons stay reachable.
self.scroll_area.setMinimumHeight(500)
# Open the report area ~450px tall (owner request: 500px left too much
# empty space below the short report; 450px lines up with the desired
# window height). The dialog may still grow up to 700px to fit a few more
# lines; beyond that the vertical scrollbar takes over and the bottom
# buttons stay reachable.
self.scroll_area.setMinimumHeight(450)
self.scroll_area.setMaximumHeight(700)
self.vbox.addWidget(self.scroll_area, 1)
@@ -701,6 +702,38 @@ class BalBuildWillDialog(BalDialog):
_logger.debug(f"not complete {e} true")
message = False
have_to_build = True
# Task #7a: if the will was already executed, the wallet is empty and
# this exception is expected. Before showing the alarming "Found
# CHANGES ... a NEW WILL must be prepared" message, add a clear,
# reassuring note on the "Checking your will" row telling the user
# the inheritance is already on its way (mempool) or done (on-chain).
# The original message is still shown afterwards (owner request: the
# red/"changes" message stays, this is only an extra, more precise
# informative line).
# NOTE: we add the informative note as its OWN extra row (not via
# msg_set_checking, which reuses self.check_row and would be
# overwritten by the "Found CHANGES" line set below). Passing row=None
# to msg_set_status appends a new line, so the executed/mempool note
# and the original message are BOTH visible.
executed_status = self._executed_inheritance_status()
if executed_status == "CONFIRMED":
# Green: the inheritance transaction is confirmed on the
# blockchain, so it has been executed correctly.
self.msg_set_status(
_("Checking your will"),
None,
_("Inheritance already executed (on blockchain)"),
self.COLOR_OK,
)
elif executed_status == "MEMPOOL":
# Orange (warning colour): the transaction is in the mempool,
# waiting to be confirmed. Not an error, just "in progress".
self.msg_set_status(
_("Checking your will"),
None,
_("Inheritance in mempool (waiting confirmation)"),
self.COLOR_WARNING,
)
if isinstance(e, HeirChangeException):
message = _("Heirs changed:")
elif isinstance(e, WillExecutorNotPresent):
@@ -741,8 +774,15 @@ class BalBuildWillDialog(BalDialog):
txs = self.bal_window.build_will()
if not txs:
self.msg_set_building(
_("Balance is too low, or CheckAlive is in the past.Skipped"),
color = self.COLOR_ERROR,
_(
"Balance is too low, or CheckAlive is in the "
"past. Skipped"
),
# Orange (warning) instead of red (error): an empty
# wallet after the inheritance was executed is a NORMAL
# situation, not a failure, so the colour should not
# alarm the user (owner request).
color=self.COLOR_WARNING,
)
return False, None
@@ -1546,6 +1586,42 @@ class BalBuildWillDialog(BalDialog):
self.msg_edit_row(self.msg_error(f"Error: {b}"))
_logger.error(f"error phase2: {b}")
def _executed_inheritance_status(self):
"""Return the on-chain status of an already-executed inheritance.
Task #7a (owner request). After an inheritance is executed the wallet is
fully emptied (the plugin always empties the wallet). Pressing CHECK on
such an empty wallet makes ``check_will`` raise NotCompleteWillException
(the heirs no longer match the empty wallet), which previously showed the
alarming "Found CHANGES ... a NEW WILL must be prepared" message even
though the inheritance was, in fact, correctly executed.
To reassure the user we look at the will items, whose status is already
set to CONFIRMED / MEMPOOL by ``Will.check_will`` (see core/will.py), and
return a short code describing the real situation:
Returns:
"CONFIRMED" if at least one will transaction is confirmed on-chain
(the inheritance is already executed);
"MEMPOOL" if none is confirmed but at least one is in the mempool
(waiting for confirmation);
None if neither (the change really has to be rebuilt).
CONFIRMED takes precedence over MEMPOOL, since a confirmed transaction is
the strongest evidence that the inheritance has gone through.
"""
has_mempool = False
try:
for _wid, witem in self.bal_window.willitems.items():
if witem.get_status("CONFIRMED"):
return "CONFIRMED"
if witem.get_status("MEMPOOL"):
has_mempool = True
except Exception as _err:
# Never let this purely informational check break the flow.
_logger.debug(f"_executed_inheritance_status error: {_err}")
return "MEMPOOL" if has_mempool else None
def msg_set_checking(self, status="Waiting", row=None):
row = self.check_row if row is None else row
self.check_row = self.msg_set_status(_("Checking your will"), row, status)

View File

@@ -438,6 +438,29 @@ class Plugin(BalPlugin):
def on_user_type_change(idx):
# Persist "basic"/"advanced" and refresh the open windows so the
# advanced controls appear/disappear right away.
#
# SAFETY GATE (owner request): enabling ADVANCED exposes powerful,
# easy-to-misuse controls. Before switching to ADVANCED the user must
# type a confirmation phrase ("at My Risk", case-insensitive). If the
# phrase is wrong or the dialog is cancelled, we revert the combo to
# BASIC and do NOT enable ADVANCED. Switching back to BASIC needs no
# confirmation.
if idx == 1:
text, ok = QInputDialog.getText(
d,
_("Enable ADVANCED mode"),
_("Type 'at My Risk' to enable ADVANCED mode"),
)
# Accept any capitalisation (e.g. "at my risk", "AT MY RISK").
if not ok or text.strip().lower() != "at my risk":
# Revert to BASIC. Block the signal while we reset the combo
# index so this handler is not called again recursively.
user_type_combo.blockSignals(True)
user_type_combo.setCurrentIndex(0)
user_type_combo.blockSignals(False)
self.USER_TYPE.set("basic")
self.update_all()
return
self.USER_TYPE.set("advanced" if idx == 1 else "basic")
self.update_all()
@@ -473,40 +496,29 @@ class Plugin(BalPlugin):
# Reset/support button row (bottom). Assigning ``QGridLayout(d)`` would
# have made the grid the dialog's only layout, leaving no room for them.
grid = QGridLayout()
add_widget(
grid,
"User Type",
user_type_combo,
0,
(
"Choose how much detail the plugin shows.\n\n"
"BASIC (default): a simpler interface. It hides the advanced "
"controls (the Raw/Date selector and the 'Check Alive' field) "
"and turns off the 'check alive' postpone behaviour, so you "
"only set the delivery date.\n\n"
"ADVANCED: shows every control, including the 'Check Alive' "
"field and the Raw/Date selector."
),
)
# NOTE: the "User Type" row used to be the first row (row 0). It was moved
# to the BOTTOM of the grid (just above "Rebroadcast transactions") at the
# owner's request; see the add_widget call further below. The remaining
# rows were renumbered up by one accordingly.
add_widget(
grid,
"Hide Replaced",
heir_hide_replaced,
1,
0,
"Hide replaced transactions from will detail and list",
)
add_widget(
grid,
"Hide Invalidated",
heir_hide_invalidated,
2,
1,
"Hide invalidated transactions from will detail and list",
)
add_widget(
grid,
"Auto-sign on Check",
heir_auto_sign,
3,
2,
(
"When checking, automatically sign and broadcast the will "
"transactions to their will-executors.\n"
@@ -518,7 +530,7 @@ class Plugin(BalPlugin):
grid,
"Panel editable Date and Fee",
heir_editable_dates,
4,
3,
(
"When enabled, the delivery-time and check-alive date fields "
"can be edited everywhere (toolbar / Heirs tab), not only in "
@@ -540,7 +552,7 @@ class Plugin(BalPlugin):
grid,
"Add transaction without willexecutor",
heir_no_willexecutor,
5,
4,
(
"Create a will that does not require a Will-executor; it can be "
"saved, for example, on a USB stick, and a copy can be given to "
@@ -551,7 +563,7 @@ class Plugin(BalPlugin):
grid,
"Number of reminders",
heir_num_reminders,
6,
5,
(
"How many reminder alarms the exported calendar (.ics) event "
"contains.\n\n"
@@ -568,7 +580,7 @@ class Plugin(BalPlugin):
grid,
"Event summary",
edit_event_summary,
7,
6,
(
"Default message to be used in event summary\n"
"Variables:\n"
@@ -581,7 +593,7 @@ class Plugin(BalPlugin):
grid,
"Event description",
edit_event_description,
8,
7,
(
"Default message to be used in event description\n"
"Variables:\n"
@@ -607,6 +619,24 @@ class Plugin(BalPlugin):
# "Ask before to ping willexecutor",
# )
# add_widget(grid,"Enable Multiverse(EXPERIMENTAL/BROKEN)",heir_enable_multiverse,6,"enable multiple locktimes, will import.... ")
# "User Type" placed here (row 8), at the BOTTOM of the settings just
# above "Rebroadcast transactions" (owner request). It used to be the very
# first row; the other rows were renumbered up by one when it was moved.
add_widget(
grid,
"User Type",
user_type_combo,
8,
(
"Choose how much detail the plugin shows.\n\n"
"BASIC (default): a simpler interface. It hides the advanced "
"controls (the Raw/Date selector and the 'Check Alive' field) "
"and turns off the 'check alive' postpone behaviour, so you "
"only set the delivery date.\n\n"
"ADVANCED: shows every control, including the 'Check Alive' "
"field and the Raw/Date selector."
),
)
grid.addWidget(heir_repush, 9, 0)
grid.addWidget(
HelpButton(
@@ -668,7 +698,7 @@ class Plugin(BalPlugin):
# reflects the reset values.
self.update_all()
btn_reset = QPushButton(_("Reset setting"))
btn_reset = QPushButton(_("Reset to Default Setting"))
btn_reset.setToolTip(_("Reset these settings to their default values"))
btn_reset.clicked.connect(on_reset_defaults)

View File

@@ -388,6 +388,43 @@ class BalTimeEditWidget(QWidget, _LockTimeEditor):
self.combo.setCurrentIndex(index)
#self.on_current_index_changed(index, force)
def apply_user_type_visibility(self):
"""Re-apply the BASIC/ADVANCED visibility of the Raw/Date selector.
WHY this is needed (bug reported by the owner, task #04): the Raw/Date
combo is hidden in BASIC mode and shown in ADVANCED mode. That visibility
used to be decided ONLY in ``__init__`` (see the ``self._basic_mode``
block there). The WILL and HEIR tab toolbars are created once and then
REUSED for the whole session (they are not rebuilt when the user switches
USER TYPE), so after switching from BASIC to ADVANCED the combo stayed
hidden there and the user could only use the "Date" editor. The same
happened when a RAW value was pushed from the wizard: the raw number
showed (the active editor was updated) but the selector box stayed hidden
because nothing ever re-showed it. The wizard worked only because it is
recreated every time it is opened.
This method re-reads ``is_basic_mode()`` and shows/hides ONLY the combo.
It deliberately does NOT change the current value or the active editor
(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.
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.
"""
try:
basic = self.bal_window.bal_plugin.is_basic_mode()
except Exception:
# 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 set_value(
self,
x: Any,
@@ -918,6 +955,18 @@ class WillSettingsWidget(QWidget):
# Hidden in BASIC, visible in ADVANCED.
threshold.setVisible(not basic)
# Task #04: also re-apply the Raw/Date selector visibility on BOTH the
# delivery-time (locktime) and the check-alive (threshold) boxes. The
# combo was hidden once at construction (BASIC) and the reused WILL/HEIR
# toolbars never re-showed it after switching to ADVANCED. Each composite
# decides its own combo visibility from is_basic_mode() WITHOUT touching
# its current value or editor.
for field in ("locktime", "threshold"):
widget = self.widgets.get(field)
apply_combo = getattr(widget, "apply_user_type_visibility", None)
if callable(apply_combo):
apply_combo()
def open_or_save_calendar(self):
"""Build and save an .ics calendar file with SEPARATE reminder events.

View File

@@ -1,7 +1,7 @@
{
"name": "bal",
"fullname": "Bitcoin After Life",
"version": "0.4.7",
"version": "0.4.8",
"description": "Provides free and decentralized Bitcoin inheritance support. Build time-locked 'will' transactions that transfer funds to your heirs if you stop refreshing them (dead-man's switch), optionally relayed by will-executor servers.",
"author": "Svatantrya",
"licence": "MIT",