diff --git a/CHANGELOG_REFACTOR.md b/CHANGELOG_REFACTOR.md
index f4e545b..87fbe19 100644
--- a/CHANGELOG_REFACTOR.md
+++ b/CHANGELOG_REFACTOR.md
@@ -609,3 +609,62 @@ Modifica centralizzata negli helper `msg_ok`, `msg_error`, `msg_warning`,
Confermato dall'utente sui dati reali: dopo Sign -> Broadcast -> Check le
transazioni gia inviate sono tornate verdi ("confirmed on server"); la lista
torna pulita; il grassetto e l'aggiornamento delle hide-flag funzionano.
+
+## 17. UI polish and bug fix — signed-tx colour, wizard, Building Will dialog (v0.3.3)
+
+This session groups several behaviour-invariant UI refinements plus one colour
+bug fix. All comments and code remain in English; only the chat with the
+author was in Italian.
+
+### FIX — Signed-but-not-sent transaction shown RED instead of blue
+`core/will.py` (`needs_server_check`): a previous-session change (section 16,
+"FIX 2") had removed the `PUSHED` requirement from `needs_server_check`, so a
+will that was *signed but never broadcast* was still server-queried. The query
+returned CHECK_FAIL, and because `status_color()` checks CHECK_FAIL (red,
+`#e83845`) before COMPLETE (blue, `#2bc8ed`), the row turned red. Restored the
+original Gitea `check()` condition by adding back `and w.get_status("PUSHED")`,
+so only already-broadcast wills are server-checked. A signed-but-not-sent will
+now stays blue (COMPLETE) as in the original.
+- `tests/test_core_will.py` (`test_needs_server_check`): a freshly-built item
+ (VALID, not PUSHED) now correctly expects `False`.
+
+### Wizard "Will Settings" — equal-width, left-aligned rows
+`gui/qt/widgets.py` (`WillSettingsWidget`, vertical layout): the calendar button
+and the fee field used to stretch to the dialog's right edge, far wider than the
+date rows. Now every row is capped to the widest date-row width (`row_w`) and
+left-aligned, so they form a tidy column. The leading icons keep their original
+`HelpButton` width (`icon_w` is used only as a spacer in front of the calendar,
+never to widen the icons themselves).
+
+### Wizard button — icon + text
+`gui/qt/lists.py` (`create_toolbar`): the "build your will" toolbar button is now
+more inviting: a 28×28 wizard icon plus a bold `"Create your will"` caption,
+`setMinimumHeight(40)`. `gui/qt/common.py` gained `QSize` in the QtCore import.
+
+### Building Will dialog — clearer final report + manual Close
+`gui/qt/dialogs.py` (`BalBuildWillDialog`):
+- The closing summary line is no longer a bare "Ok": it now has an explicit
+ left-side label, `"All done: Ok"`, like the other result rows.
+- A blank separator row is inserted above "All done" so the overall outcome is
+ visually detached from the per-step rows.
+- The four `"checking variables"` status strings are capitalised to
+ `"Checking variables"` to match the rows below; the redundant trailing colon
+ on the final one was dropped (`msg_set_status` already adds `":\t"`).
+- The final auto-closing countdown (`self.wait(5)`) was replaced by an explicit
+ right-aligned **"Close"** button (`_add_close_button` / `_on_close_clicked`).
+ The dialog now stays open until the user dismisses it, so the full report can
+ be read at leisure. The intermediate technical pauses (`wait(10)`, `wait(5)`,
+ `wait(3)`) are kept. Closing still shows the persistent "next steps"
+ (Sign / Broadcast) popup when `self._next_steps_hint` is set.
+
+### Preview helpers (dev-only, not shipped logic)
+`tests/preview_wizard_settings_align.py`, `tests/preview_wizard_button.py`,
+`tests/preview_building_will_close_btn.py`: small offscreen scripts used to
+render before/after mock-ups for visual approval.
+
+### Test
+- 186 official tests pass; smoke test, external-zip test OK.
+- `ruff` reports only the pre-existing baseline false positives (F401/F403/F405
+ star-import re-exports, one F841, one F541) — no new issues.
+- Version bumped to **0.3.3** (`bal/VERSION`, `bal/__init__.py`,
+ `bal/manifest.json`).
diff --git a/bal/VERSION b/bal/VERSION
index 9fc80f9..87a0871 100644
--- a/bal/VERSION
+++ b/bal/VERSION
@@ -1 +1 @@
-0.3.2
\ No newline at end of file
+0.3.3
\ No newline at end of file
diff --git a/bal/__init__.py b/bal/__init__.py
index f309239..a814720 100644
--- a/bal/__init__.py
+++ b/bal/__init__.py
@@ -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"
diff --git a/bal/core/plugin_base.py b/bal/core/plugin_base.py
index 8b281db..aa0b7cf 100644
--- a/bal/core/plugin_base.py
+++ b/bal/core/plugin_base.py
@@ -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 = {
diff --git a/bal/core/will.py b/bal/core/will.py
index 4c1f341..85d6782 100644
--- a/bal/core/will.py
+++ b/bal/core/will.py
@@ -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")
)
diff --git a/bal/gui/qt/common.py b/bal/gui/qt/common.py
index 90dd77b..38a4710 100644
--- a/bal/gui/qt/common.py
+++ b/bal/gui/qt/common.py
@@ -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,
diff --git a/bal/gui/qt/dialogs.py b/bal/gui/qt/dialogs.py
index 48606ee..44cdb09 100644
--- a/bal/gui/qt/dialogs.py
+++ b/bal/gui/qt/dialogs.py
@@ -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("{}".format(hint))
+ except Exception as hint_err:
+ _logger.debug(f"next-steps hint error: {hint_err}")
def on_error(self, error):
_logger.error(error)
diff --git a/bal/gui/qt/lists.py b/bal/gui/qt/lists.py
index 53bd123..b853191 100644
--- a/bal/gui/qt/lists.py
+++ b/bal/gui/qt/lists.py
@@ -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"))
diff --git a/bal/gui/qt/widgets.py b/bal/gui/qt/widgets.py
index 997d8b5..487a083 100644
--- a/bal/gui/qt/widgets.py
+++ b/bal/gui/qt/widgets.py
@@ -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)
diff --git a/bal/gui/qt/window.py b/bal/gui/qt/window.py
index b3950e8..8c27d24 100644
--- a/bal/gui/qt/window.py
+++ b/bal/gui/qt/window.py
@@ -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:
diff --git a/bal/manifest.json b/bal/manifest.json
index 45e609c..fa0fe4b 100644
--- a/bal/manifest.json
+++ b/bal/manifest.json
@@ -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",
diff --git a/tests/preview_building_will_close_btn.py b/tests/preview_building_will_close_btn.py
new file mode 100644
index 0000000..939fa96
--- /dev/null
+++ b/tests/preview_building_will_close_btn.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python3
+"""Visual PREVIEW: replace the final countdown with a "Close" button.
+
+Mocks the "Building Will" dialog content (the result rows are built exactly as
+the real dialog builds them) and shows the proposed bottom "Close" button that
+replaces the "Please wait 5secs" auto-closing countdown.
+
+BEFORE: last line is the countdown "Please wait 5secs" (dialog auto-closes).
+AFTER : a real "Close" button at the bottom; the user closes when ready.
+
+Run:
+ QT_QPA_PLATFORM=offscreen python3 tests/preview_building_will_close_btn.py
+Writes preview_buildwill_before.png / preview_buildwill_after.png.
+"""
+
+import os
+
+os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
+
+from PyQt6.QtWidgets import ( # noqa: E402
+ QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
+)
+from PyQt6.QtCore import Qt # noqa: E402
+
+COLOR_OK = "#05ad05"
+
+
+def _ok(text="Ok"):
+ return "{}".format(COLOR_OK, text)
+
+
+def _rows(after: bool):
+ """The exact result rows the real dialog shows on a clean run.
+
+ BEFORE keeps the old lowercase "checking variables" and the "All done"
+ row directly under the others. AFTER applies the two text fixes:
+ capitalised "Checking variables" + a blank separator row above "All done".
+ """
+ if not after:
+ return [
+ "checking variables:\t" + _ok("Ok"),
+ "Checking your will:\t" + _ok("Ok"),
+ "Signing your will:\tNothing to do",
+ "Broadcasting your will to executors:\tNothing to do",
+ "All done:\t" + _ok("Ok"),
+ ]
+ return [
+ "Checking variables:\t" + _ok("Ok"),
+ "Checking your will:\t" + _ok("Ok"),
+ "Signing your will:\tNothing to do",
+ "Broadcasting your will to executors:\tNothing to do",
+ "", # blank separator row
+ "All done:\t" + _ok("Ok"),
+ ]
+
+
+def _build(after: bool) -> QWidget:
+ panel = QWidget()
+ panel.setMinimumWidth(600)
+ v = QVBoxLayout(panel)
+ v.addWidget(QLabel("Building Will:"))
+
+ rows = QWidget()
+ rv = QVBoxLayout(rows)
+ rv.setContentsMargins(8, 4, 8, 4)
+ for line in _rows(after):
+ lbl = QLabel(line)
+ lbl.setTextFormat(Qt.TextFormat.RichText)
+ rv.addWidget(lbl)
+ v.addWidget(rows)
+
+ if not after:
+ # BEFORE: the countdown row (auto-close).
+ wait = QLabel("Please wait 5secs")
+ v.addWidget(wait)
+ else:
+ # AFTER: a Close button row, right aligned (standard dialog layout).
+ v.addSpacing(8)
+ btn_row = QWidget()
+ h = QHBoxLayout(btn_row)
+ h.setContentsMargins(0, 0, 0, 0)
+ h.addStretch(1)
+ close = QPushButton("Close")
+ close.setDefault(True)
+ close.setMinimumWidth(90)
+ h.addWidget(close)
+ v.addWidget(btn_row)
+
+ panel.resize(620, 230)
+ return panel
+
+
+def main():
+ app = QApplication.instance() or QApplication([])
+ for after, name in ((False, "preview_buildwill_before.png"),
+ (True, "preview_buildwill_after.png")):
+ panel = _build(after)
+ panel.show()
+ app.processEvents()
+ panel.grab().save(name)
+ print("wrote", name)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tests/preview_wizard_button.py b/tests/preview_wizard_button.py
new file mode 100644
index 0000000..a11c6ee
--- /dev/null
+++ b/tests/preview_wizard_button.py
@@ -0,0 +1,139 @@
+#!/usr/bin/env python3
+"""Visual PREVIEW: proposals to make the Wizard button more visible.
+
+Renders the toolbar Wizard button (using the real icons/wizard.png) in several
+styles so the user can pick one. Nothing is changed in the plugin yet.
+
+Variants:
+ 0. CURRENT : icon only, default size (what ships today).
+ A. BIGGER : same icon, larger button + larger iconSize.
+ B. ICON+TEXT: bigger icon plus a "Create your will" label.
+ C. ACCENT : icon + text on a colored (Bitcoin-orange) rounded button.
+ D. ACCENT-BLUE: icon + text on a BAL-blue (#2bc8ed) rounded button.
+
+Run:
+ QT_QPA_PLATFORM=offscreen python3 tests/preview_wizard_button.py
+Writes preview_wizardbtn_.png in the repo root.
+"""
+
+import os
+
+os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
+
+from PyQt6.QtWidgets import ( # noqa: E402
+ QApplication, QWidget, QHBoxLayout, QPushButton, QComboBox, QLineEdit,
+ QLabel,
+)
+from PyQt6.QtGui import QIcon, QPixmap # noqa: E402
+from PyQt6.QtCore import QSize, Qt # noqa: E402
+
+ICON_PATH = os.path.join(os.path.dirname(__file__), "..", "bal", "icons",
+ "wizard.png")
+
+
+def _icon():
+ pm = QPixmap(ICON_PATH)
+ return QIcon(pm)
+
+
+def _toolbar_tail(parent):
+ """The widgets that sit to the right of the wizard button, for context."""
+ out = []
+ icon = QPushButton(parent)
+ icon.setText("📅")
+ combo = QComboBox(parent)
+ combo.addItems(["Raw", "Date"])
+ combo.setCurrentIndex(1)
+ field = QLineEdit("04/06/2028 00:00", parent)
+ field.setFixedWidth(140)
+ out += [icon, combo, field]
+ return out
+
+
+def _frame(make_wizard, label):
+ panel = QWidget()
+ v = QHBoxLayout(panel)
+ v.setContentsMargins(10, 10, 10, 10)
+ tag = QLabel(label, panel)
+ tag.setFixedWidth(120)
+ v.addWidget(tag)
+ wiz = make_wizard(panel)
+ v.addWidget(wiz)
+ for w in _toolbar_tail(panel):
+ v.addWidget(w)
+ v.addStretch(1)
+ panel.resize(720, 70)
+ return panel
+
+
+# ---- variants -------------------------------------------------------------
+def v_current(parent):
+ b = QPushButton(parent)
+ b.setIcon(_icon())
+ b.setToolTip("Wizard - Build your will")
+ return b
+
+
+def v_bigger(parent):
+ b = QPushButton(parent)
+ b.setIcon(_icon())
+ b.setIconSize(QSize(36, 36))
+ b.setFixedSize(48, 44)
+ b.setToolTip("Wizard - Build your will")
+ return b
+
+
+def v_icon_text(parent):
+ b = QPushButton(" Create your will", parent)
+ b.setIcon(_icon())
+ b.setIconSize(QSize(28, 28))
+ b.setMinimumHeight(40)
+ b.setStyleSheet("QPushButton{font-weight:bold;}")
+ return b
+
+
+def v_accent_orange(parent):
+ b = QPushButton(" Create your will", parent)
+ b.setIcon(_icon())
+ b.setIconSize(QSize(28, 28))
+ b.setMinimumHeight(40)
+ b.setStyleSheet(
+ "QPushButton{background-color:#f7931a;color:white;font-weight:bold;"
+ "border:none;border-radius:8px;padding:6px 14px;}"
+ "QPushButton:hover{background-color:#ffa733;}"
+ )
+ return b
+
+
+def v_accent_blue(parent):
+ b = QPushButton(" Create your will", parent)
+ b.setIcon(_icon())
+ b.setIconSize(QSize(28, 28))
+ b.setMinimumHeight(40)
+ b.setStyleSheet(
+ "QPushButton{background-color:#2bc8ed;color:white;font-weight:bold;"
+ "border:none;border-radius:8px;padding:6px 14px;}"
+ "QPushButton:hover{background-color:#4fd6f3;}"
+ )
+ return b
+
+
+def main():
+ app = QApplication.instance() or QApplication([])
+ variants = [
+ (v_current, "0-CURRENT", "preview_wizardbtn_0_current.png"),
+ (v_bigger, "A-BIGGER", "preview_wizardbtn_A_bigger.png"),
+ (v_icon_text, "B-ICON+TEXT", "preview_wizardbtn_B_icontext.png"),
+ (v_accent_orange, "C-ORANGE", "preview_wizardbtn_C_orange.png"),
+ (v_accent_blue, "D-BLUE", "preview_wizardbtn_D_blue.png"),
+ ]
+ for make, label, name in variants:
+ panel = _frame(make, label)
+ panel.show()
+ app.processEvents()
+ panel.grab().save(name)
+ print("wrote", name)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tests/preview_wizard_settings_align.py b/tests/preview_wizard_settings_align.py
new file mode 100644
index 0000000..4870b16
--- /dev/null
+++ b/tests/preview_wizard_settings_align.py
@@ -0,0 +1,151 @@
+#!/usr/bin/env python3
+"""Visual PREVIEW for the wizard "Bitcoin After Life Will Settings" rows.
+
+Reproduces the structure of WillSettingsWidget in its VERTICAL layout (the one
+used by the "Build your will" wizard):
+
+ row 1: [icon][combo "Date"][date field] (delivery time / locktime)
+ row 2: [icon][combo "Date"][date field] (check alive / threshold)
+ row 3: [calendar button] (calendar export)
+ row 4: [icon "丰"][spin "5"] (tx fees)
+
+The icons are HelpButtons, which pin themselves to a fixed width
+(2.2 * char_width_in_lineedit()). This preview reproduces that original icon
+size and shows:
+
+ * BEFORE: calendar/fee stretch to the right edge -> rows wider than dates.
+ * AFTER : icons keep their ORIGINAL fixed width; every row is capped to the
+ date-row width and left aligned, so all rows fit in the same block
+ and line up on both edges.
+
+Run:
+ QT_QPA_PLATFORM=offscreen python3 tests/preview_wizard_settings_align.py
+Writes preview_wizard_before.png / preview_wizard_after.png in the repo root.
+"""
+
+import os
+
+os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
+
+from PyQt6.QtWidgets import ( # noqa: E402
+ QApplication, QWidget, QVBoxLayout, QHBoxLayout, QToolButton, QComboBox,
+ QLineEdit, QSpinBox, QLabel,
+)
+from PyQt6.QtGui import QFontMetrics # noqa: E402
+from PyQt6.QtCore import Qt # noqa: E402
+
+
+def _char_w():
+ fm = QFontMetrics(QApplication.instance().font())
+ return fm.horizontalAdvance("0")
+
+
+def _icon(text):
+ """Mimic HelpButton: a QToolButton pinned to 2.2 * char width."""
+ b = QToolButton()
+ b.setText(text)
+ b.setFixedWidth(round(2.2 * _char_w()))
+ return b
+
+
+def _date_row():
+ w = QWidget()
+ h = QHBoxLayout(w)
+ h.setContentsMargins(0, 0, 0, 0)
+ h.setSpacing(0)
+ icon = _icon("📅")
+ combo = QComboBox()
+ combo.addItems(["Raw", "Date"])
+ combo.setCurrentIndex(1)
+ field = QLineEdit("04/06/2028 00:00")
+ h.addWidget(icon)
+ h.addWidget(combo)
+ h.addWidget(field)
+ h.addStretch(1)
+ w._prefix = icon
+ return w
+
+
+def _fee_row():
+ w = QWidget()
+ h = QHBoxLayout(w)
+ h.setContentsMargins(0, 0, 0, 0)
+ h.setSpacing(0)
+ icon = _icon("丰")
+ spin = QSpinBox()
+ spin.setValue(5)
+ spin.setMaximum(10000)
+ h.addWidget(icon)
+ h.addWidget(spin)
+ w._prefix = icon
+ return w
+
+
+def _calendar_button():
+ return QToolButton()
+
+
+def _panel():
+ panel = QWidget()
+ box = QVBoxLayout(panel)
+ box.addWidget(QLabel("Bitcoin After Life Will Settings"))
+ return panel, box
+
+
+def build_before():
+ panel, box = _panel()
+ box.addWidget(_date_row())
+ box.addWidget(_date_row())
+ box.addWidget(_calendar_button())
+ box.addWidget(_fee_row())
+ panel.resize(760, 220)
+ return panel
+
+
+def build_after():
+ panel, box = _panel()
+ r1 = _date_row()
+ r2 = _date_row()
+ cal = _calendar_button()
+ r4 = _fee_row()
+
+ # icons keep their original fixed width (no resizing)
+ icon_w = r1._prefix.sizeHint().width()
+
+ row_w = max(r1.sizeHint().width(), r2.sizeHint().width())
+ for r in (r1, r2, r4):
+ r.setFixedWidth(row_w)
+
+ cal_row = QWidget()
+ cb = QHBoxLayout(cal_row)
+ cb.setContentsMargins(0, 0, 0, 0)
+ cb.setSpacing(0)
+ sp = QWidget()
+ sp.setFixedWidth(icon_w)
+ cb.addWidget(sp)
+ cb.addWidget(cal)
+ cal_row.setFixedWidth(row_w)
+
+ box.addWidget(r1, alignment=Qt.AlignmentFlag.AlignLeft)
+ box.addWidget(r2, alignment=Qt.AlignmentFlag.AlignLeft)
+ box.addWidget(cal_row, alignment=Qt.AlignmentFlag.AlignLeft)
+ box.addWidget(r4, alignment=Qt.AlignmentFlag.AlignLeft)
+ panel.resize(760, 220)
+ return panel
+
+
+def main():
+ app = QApplication.instance() or QApplication([])
+ for builder, name in (
+ (build_before, "preview_wizard_before.png"),
+ (build_after, "preview_wizard_after.png"),
+ ):
+ panel = builder()
+ panel.show()
+ app.processEvents()
+ panel.grab().save(name)
+ print("wrote", name)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tests/test_core_will.py b/tests/test_core_will.py
index 7165e73..3c422ea 100644
--- a/tests/test_core_will.py
+++ b/tests/test_core_will.py
@@ -227,17 +227,22 @@ def test_check_heir_added_triggers_rebuild():
def test_needs_server_check():
- """Check button selection logic: a VALID will with a will-executor that is
- not yet CHECKED must be queried on the server, even if it is not PUSHED
- (regression for the 'New / Not sent' wills that Check ignored)."""
+ """Check button selection logic: only a VALID, PUSHED will with a
+ will-executor that is not yet CHECKED must be queried on the server.
+
+ A will that was never sent (not PUSHED) must NOT be queried: the server
+ would correctly answer "I don't have it", which would be recorded as
+ CHECK_FAIL and turn a merely signed-but-not-sent will red instead of leaving
+ it blue (#2bc8ed). This matches the original BAL ``check()`` behaviour."""
we = {"url": "https://we.example.com"}
- # New (not PUSHED) but has a will-executor -> must be checked.
+ # New (not PUSHED) but has a will-executor -> must NOT be checked, otherwise
+ # a signed-but-not-sent will would falsely turn CHECK_FAIL (red).
item_new = _make_willitem_blank()
item_new.we = we
- assert Will.needs_server_check(item_new) is True
+ assert Will.needs_server_check(item_new) is False
- # PUSHED but not CHECKED -> must be checked (previous behaviour).
+ # PUSHED but not CHECKED -> must be checked.
item_pushed = _make_willitem_blank()
item_pushed.we = we
item_pushed.set_status("PUSHED", True)