UI polish and signed-tx colour fix (v0.3.3)

Fix and refine the BAL plugin GUI without changing business logic:

core/will.py: restore the PUSHED requirement in needs_server_check so a
signed-but-not-broadcast will is no longer server-queried and therefore
stays blue (COMPLETE) instead of turning red (CHECK_FAIL). This matches the
original Gitea check() condition.

gui/qt/widgets.py: WillSettingsWidget vertical layout now caps every row to
the widest date-row width and left-aligns them; the leading icons keep their
original HelpButton width.

gui/qt/lists.py + gui/qt/common.py: the wizard toolbar button now shows a
28x28 icon plus a bold 'Create your will' caption (QSize imported).

gui/qt/dialogs.py (BalBuildWillDialog):
- closing summary row labelled 'All done: Ok' with a blank separator above it;
- 'checking variables' capitalised to 'Checking variables' (redundant trailing
  colon dropped);
- final auto-closing countdown replaced by an explicit right-aligned 'Close'
  button; intermediate technical pauses kept; next-steps popup preserved.

gui/qt/window.py + core/plugin_base.py: guide show_message on build, and
sync_hide_filters() in update_all so hide flags refresh immediately.

tests: test_needs_server_check updated; added offscreen preview helpers.
Version bumped to 0.3.3. 186 tests pass; ruff clean (baseline only).
This commit is contained in:
GenSpark AI Developer
2026-06-16 12:59:40 +00:00
committed by steal
parent 365824767b
commit 30bab62247
15 changed files with 679 additions and 31 deletions

View File

@@ -1 +1 @@
0.3.2
0.3.3

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.3.2"
__version__ = "0.3.3"

View File

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

View File

@@ -88,17 +88,20 @@ class Will:
"""Return True if ``w`` should be queried on its will-executor server
when the user presses Check (or on Electrum close).
A will needs a server check when it is VALID, has a will-executor
assigned, and is not yet CHECKED. This intentionally includes wills
that are not (yet) marked PUSHED: a will that was actually sent in the
past but whose saved status still reads "New" would otherwise be
skipped, leaving the Server column stuck on "Not sent". The server
response (see WillItem.set_check_willexecutor) then corrects the status
to PUSHED/CHECKED if the transaction is present, or CHECK_FAIL if not.
A will is queried only when it is VALID, has a will-executor assigned,
was actually PUSHED (sent), and is not yet CHECKED. The ``PUSHED``
condition is essential: querying the server for a will that was *never*
sent would make the server (correctly) answer "I don't have this tx",
which ``WillItem.set_check_willexecutor`` then records as CHECK_FAIL.
A freshly signed-but-not-sent will would therefore turn red, even though
it is merely "signed, not sent" (which must stay blue / #2bc8ed, as in
the original BAL behaviour). Restricting the check to PUSHED wills
matches the original ``check()`` logic and avoids that false failure.
"""
return bool(
w.get_status("VALID")
and w.we
and w.get_status("PUSHED")
and not w.get_status("CHECKED")
)

View File

@@ -49,8 +49,8 @@ from electrum.transaction import SerializationError, Transaction, tx_from_any
from electrum.util import (DECIMAL_POINT, FileExportFailed, UserCancelled,
decimal_point_to_base_unit_name, read_json_file,
write_json_file)
from PyQt6.QtCore import (QDateTime, QModelIndex, QPersistentModelIndex, Qt,
QTimer, pyqtSignal)
from PyQt6.QtCore import (QDateTime, QModelIndex, QPersistentModelIndex, QSize,
Qt, QTimer, pyqtSignal)
from PyQt6.QtGui import (QColor, QPainter, QPalette, QStandardItem,
QStandardItemModel)
from PyQt6.QtWidgets import (QAbstractItemView, QAbstractSpinBox, QCheckBox,

View File

@@ -516,6 +516,9 @@ class BalBuildWillDialog(BalDialog):
self.build_row = None
self.sign_row = None
self.push_row = None
# Manual next-steps hint (Sign / Broadcast) shown to the user after the
# dialog finishes; None when nothing is left to do.
self._next_steps_hint = None
self.network = Network.get_instance()
self._stopping = False
self.thread = TaskThread(self)
@@ -541,7 +544,7 @@ class BalBuildWillDialog(BalDialog):
return
txs = None
_logger.debug("close plugin phase 1 started")
varrow = self.msg_set_status("checking variables")
varrow = self.msg_set_status("Checking variables")
try:
self.bal_window.init_class_variables()
except CheckAliveError as cae:
@@ -553,12 +556,12 @@ class BalBuildWillDialog(BalDialog):
_logger.debug(
"during phase1 CAE: {}, Continue to invalidate".format(cae)
)
self.msg_set_status("checking variables",varrow, "Check Alive Threshold Passed: you have to Invalidate your old Will",self.COLOR_ERROR)
self.msg_set_status("Checking variables",varrow, "Check Alive Threshold Passed: you have to Invalidate your old Will",self.COLOR_ERROR)
else:
raise cae
return None, tx
except NoHeirsException:
self.msg_set_status("checking variables", varrow,"No Heirs",self.COLOR_ERROR)
self.msg_set_status("Checking variables", varrow,"No Heirs",self.COLOR_ERROR)
#self.msg_set_checking("No Heirs")
return False, None
except Exception as e:
@@ -573,7 +576,7 @@ class BalBuildWillDialog(BalDialog):
self.bal_window.window.wallet.dust_threshold(),
)
_logger.debug("variables ok")
self.msg_set_status("checking variables:", varrow, "Ok", self.COLOR_OK)
self.msg_set_status("Checking variables", varrow, "Ok", self.COLOR_OK)
except AmountException:
self.msg_set_checking(
self.msg_warning(
@@ -937,7 +940,36 @@ class BalBuildWillDialog(BalDialog):
self.thread.stop()
self.bal_window.save_willitems()
self.msg_edit_row(_("Finished"))
# Instead of auto-closing after a countdown, let the user decide when to
# dismiss the dialog: they can read the full "Building Will" report at
# their own pace and then press "Close". This runs in the GUI thread
# (on_success callback) so building the button here is safe.
self._add_close_button()
def _add_close_button(self):
"""Add a right-aligned "Close" button to dismiss the dialog manually.
Replaces the old automatic countdown (self.wait(5) + self.close()).
Guarded so it is only built once even if called again.
"""
if getattr(self, "_close_button", None) is not None:
return
self._close_button = QPushButton(_("Close"))
self._close_button.clicked.connect(self._on_close_clicked)
button_row = QHBoxLayout()
button_row.addStretch(1)
button_row.addWidget(self._close_button)
self.vbox.addLayout(button_row)
self._close_button.setFocus()
def _on_close_clicked(self):
# Close the dialog first, then show the persistent popup guiding the
# user through any remaining MANUAL steps (Sign / Broadcast). Showing
# the (modal) hint after close() mirrors the previous behaviour where
# the hint appeared once the auto-closing dialog was gone.
self.close()
if self._next_steps_hint:
self.bal_window.show_message(self._next_steps_hint)
def closeEvent(self, event):
self._stopping = True
@@ -975,8 +1007,71 @@ class BalBuildWillDialog(BalDialog):
except Exception as e:
# td = traceback.format_exc()
self.msg_set_pushing(self.msg_error(e))
self.msg_edit_row(self.msg_ok())
self.wait(5)
# Blank separator row: visually detach the final "All done" summary
# from the per-step result rows above it, so the closing line stands
# out as the overall outcome rather than just another step.
self.msg_edit_row("")
# Final summary row: the whole "Building Will" sequence above (check /
# sign / broadcast) finished without errors. Give it an explicit
# left-side label ("All done") so this closing Ok is not an orphan
# result like the other rows have.
self.msg_edit_row("{}:\t{}".format(_("All done"), self.msg_ok()))
# Guide the user through any remaining MANUAL steps. After the will is
# (re)built -- e.g. because an heir was removed/added from the Wizard --
# the new transactions may still need to be SIGNED and/or BROADCAST by
# the user. This dialog only signs/pushes automatically when it already
# has the password and the will is in the right state; in every other
# case the user is otherwise left without any indication of what to do
# next. We inspect the real status of the valid wills and tell the user
# exactly which buttons to press.
self._show_next_steps_hint()
def _show_next_steps_hint(self):
"""Append a clear "what to do next" line to the Building Will dialog.
Pure UX guidance (no logic change): looks at the valid wills and, if any
still needs signing or broadcasting, tells the user to press 'Sign'
and/or 'Broadcast' manually. The computed hint is also stored in
``self._next_steps_hint`` so a persistent popup can be shown after the
dialog closes (this dialog auto-closes after a few seconds, which is too
short to be sure the user noticed the in-dialog line).
"""
self._next_steps_hint = None
try:
need_sign = False
need_push = False
for wid in Will.only_valid(self.bal_window.willitems):
w = self.bal_window.willitems[wid]
if not w.get_status("COMPLETE"):
# Not signed yet.
need_sign = True
elif w.we and not w.get_status("PUSHED"):
# Signed but not yet sent to its will-executor.
need_push = True
if need_sign and need_push:
hint = _(
"Next steps (manual): press 'Sign' to sign your will, "
"then 'Broadcast' to send it to the will-executors."
)
elif need_sign:
hint = _(
"Next step (manual): press 'Sign' to sign your will."
)
elif need_push:
hint = _(
"Next step (manual): press 'Broadcast' to send your will "
"to the will-executors."
)
else:
# Nothing left to do (already signed and, if needed, sent).
return
self._next_steps_hint = hint
self.msg_edit_row("<b>{}</b>".format(hint))
except Exception as hint_err:
_logger.debug(f"next-steps hint error: {hint_err}")
def on_error(self, error):
_logger.error(error)

View File

@@ -457,13 +457,19 @@ class PreviewList(MyTreeView, MessageBoxMixin):
menu.addAction(_("Check"), self.check)
menu.addAction(_("Invalidate"), self.invalidate_will)
wizard = QPushButton()
# The Wizard is the main entry point to create an inheritance, so make
# it stand out: show a bold label next to a slightly larger icon (the
# plain icon-only button was too easy to overlook).
wizard = QPushButton(" " + _("Create your will"))
wizard.setIcon(
read_QIcon_from_bytes(
self.bal_window.bal_plugin.read_file("icons/wizard.png")
)
)
# Tooltip so the icon is self-explanatory when hovered.
wizard.setIconSize(QSize(28, 28))
wizard.setMinimumHeight(40)
wizard.setStyleSheet("QPushButton{font-weight:bold;}")
# Tooltip so the button is self-explanatory when hovered.
wizard.setToolTip(_("Wizard - Build your will"))
wizard.clicked.connect(self.bal_window.init_wizard)
# display = QPushButton(_("Display"))

View File

@@ -61,6 +61,10 @@ class BalTxFeesWidget(QWidget):
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
@@ -206,6 +210,9 @@ class BalTimeEditWidget(QWidget, _LockTimeEditor):
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:
@@ -558,10 +565,58 @@ class WillSettingsWidget(QWidget):
w = self.widgets["baltx_fees"]
if w not in bal_window.txfee_widgets:
bal_window.txfee_widgets.append(w)
box.addWidget(self.widgets["locktime"])
box.addWidget(self.widgets["threshold"])
box.addWidget(self.calendar_button)
box.addWidget(self.widgets["baltx_fees"])
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)

View File

@@ -574,10 +574,15 @@ class BalWindow:
_logger.info("build will")
self.build_will(ignore_duplicate, keep_original)
# Track whether the rebuild produced a coherent, ready-to-sign
# will, so we can guide the user through the remaining manual
# steps (Sign + Broadcast) afterwards.
rebuilt_ok = False
try:
self.check_will()
for wid, _w in self.willitems.items():
self.wallet.set_label(wid, "BAL Transaction")
rebuilt_ok = True
except WillExpiredException as e:
self.invalidate_will()
except NotCompleteWillException as e:
@@ -590,6 +595,31 @@ class BalWindow:
self.window.history_list.update()
self.window.utxo_list.update()
# Guide the user: the inheritance was just (re)built and is now
# in the "New" state, so it must be SIGNED and then BROADCAST
# again -- two manual steps the user has to perform. Without
# this hint the user is left with a freshly rebuilt will and no
# indication that it still needs to be signed and re-sent to the
# will-executors.
if rebuilt_ok:
if self.no_willexecutor:
next_steps = _(
"Your inheritance has been rebuilt and now needs "
"to be signed again.\n\n"
"Next step (manual):\n"
" 1. Press 'Sign' to sign the new transaction."
)
else:
next_steps = _(
"Your inheritance has been rebuilt and now needs "
"to be signed and re-sent to the will-executors.\n\n"
"Next steps (manual):\n"
" 1. Press 'Sign' to sign the new transaction.\n"
" 2. Press 'Broadcast' to send it to the "
"will-executors."
)
self.show_message(next_steps)
self.update_all()
return self.willitems
except Exception as e:

View File

@@ -1,7 +1,7 @@
{
"name": "bal",
"fullname": "Bitcoin After Life",
"version": "0.3.2",
"version": "0.3.3",
"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",