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
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user