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

@@ -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`).

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)
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",

View File

@@ -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 "<font color='{}'><b>{}</b></font>".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:\t<b>Nothing to do</b>",
"Broadcasting your will to executors:\t<b>Nothing to do</b>",
"All done:\t" + _ok("Ok"),
]
return [
"Checking variables:\t" + _ok("Ok"),
"Checking your will:\t" + _ok("Ok"),
"Signing your will:\t<b>Nothing to do</b>",
"Broadcasting your will to executors:\t<b>Nothing to do</b>",
"", # blank separator row
"All done:\t" + _ok("Ok"),
]
def _build(after: bool) -> QWidget:
panel = QWidget()
panel.setMinimumWidth(600)
v = QVBoxLayout(panel)
v.addWidget(QLabel("<b>Building Will:</b>"))
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()

View File

@@ -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_<id>.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()

View File

@@ -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()

View File

@@ -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)