forked from bitcoinafterlife/bal-electrum-plugin
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:
committed by
steal
parent
365824767b
commit
30bab62247
105
tests/preview_building_will_close_btn.py
Normal file
105
tests/preview_building_will_close_btn.py
Normal 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()
|
||||
139
tests/preview_wizard_button.py
Normal file
139
tests/preview_wizard_button.py
Normal 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()
|
||||
151
tests/preview_wizard_settings_align.py
Normal file
151
tests/preview_wizard_settings_align.py
Normal 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()
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user