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

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