feat(bal): Group C settings-dialog improvements (editable dates, narrower RAW box, warning/reset/support, tooltips, bold locktime)

C2 - 'Editable dates' option (default OFF):
  - new EDITABLE_DATES config; checkbox in settings dialog
  - WillSettingsWidget.apply_editable_dates() re-locks/unlocks the
    delivery-time and check-alive fields, called from update_all() so the
    toggle takes effect immediately (same mechanism as Hide Invalidated)
  - EDITABLE_DATES included in the Reset-to-defaults list

C3 - RAW date box narrowed to ~1/3 (input box 6 chars); the trailing
  label that shows the computed absolute date kept at 10 chars (fixed a
  truncation regression)

C4 - settings dialog:
  (a) bold red warning at the top
  (b) 'Reset setting' button restoring all 7 dialog settings to defaults
      (single source of truth: BalConfig.default), not touching wills
  (c) bold 'Support: bitcoin-after.life' link (opens via webopen)

C5 - tooltips: calendar 'Export reminder dates to your calendar (.ics)',
  fee field 'Mining fee rate in sat/vByte used for the will transactions',
  fee '丰' icon 'Miner fee, click for more information' (tooltip font scoped
  to QPushButton so it matches the other tooltips)

C6 - Locktime column rendered in bold in the will list

tests/test_group_c_settings.py: 4 new tests (EDITABLE_DATES default/toggle,
  reset restores all dialog settings incl. EDITABLE_DATES, reset leaves
  unrelated config untouched). Full suite: 210 passed.
This commit is contained in:
2026-06-28 23:00:30 -04:00
parent dc166d04ff
commit 9d2cbc2814
8 changed files with 504 additions and 18 deletions

View File

@@ -378,3 +378,134 @@ MEMPOOL (yellow `#ffce30`).
- Full test suite: `206 passed`. - Full test suite: `206 passed`.
**Outcome:** DONE. **Outcome:** DONE.
## 7. Group C - Settings-dialog improvements (editable dates, narrower RAW box, warning/reset/support, tooltips, bold locktime)
**Context / request:**
Group C bundles several usability improvements to the BAL settings dialog and
the will views, implemented together and delivered in a single ZIP.
**What changed (C2 - "Editable dates" option):**
- `bal/core/plugin_base.py`
- New persisted config `EDITABLE_DATES = BalConfig(config, "bal_editable_dates",
False)` (default OFF).
- `bal/gui/qt/widgets.py`
- `WillSettingsWidget.__init__` reads `EDITABLE_DATES`. When OFF (default) the
delivery-time and check-alive date fields stay display-only outside the
"Build your will" wizard; when ON they become editable in the toolbar /
Heirs tab. The fee field remains read-only outside the wizard.
- `bal/gui/qt/plugin.py`
- New "Editable dates" checkbox in the settings dialog, bound to
`EDITABLE_DATES`, with an explanatory tooltip. Toggling it refreshes the
open windows so the date fields immediately reflect the new state.
**What changed (C3 - narrower RAW date box):**
- `bal/gui/qt/widgets.py`
- `LockTimeRawEdit` width reduced from `12 * char_width_in_lineedit()` to
`6 *` (roughly a third), enough for short relative values such as `30d` /
`1y` while still fitting larger day counts.
- `TimeRawEditWidget`'s trailing (empty) label shrunk from `10 *` to `2 *`
character widths, removing the wasted blank space.
**What changed (C4 - warning, reset, support link):**
- `bal/gui/qt/plugin.py`
- (a) Warning shown in bold red at the TOP of the dialog:
"Warning: change these settings only if you know what you are doing."
- (b) "Reset" button that restores the six dialog settings (Hide Replaced,
Hide Invalidated, Auto-sign, Calendar App, Event summary, Event description)
to their factory defaults, taking each default from `BalConfig.default`
(single source of truth) and refreshing the widgets. It does NOT touch
wills, will-executors or any other configuration.
- (c) Clickable support link to `https://bitcoin-after.life`, opened via
Electrum's `webopen` helper.
- The dialog now uses an outer vertical layout (warning -> settings grid ->
Reset/support row); the settings grid is unchanged apart from the new row.
- `bal/gui/qt/common.py`
- Imported `webopen` from `electrum.gui.qt.util` (re-exported for the dialog).
**What changed (C5 - clearer tooltips):**
- `bal/gui/qt/widgets.py`
- Calendar button tooltip: "Export reminder dates to your calendar (.ics)".
- Fee field tooltip: "Mining fee rate in sat/vByte used for the will
transactions".
**What changed (C6 - bold Locktime column):**
- `bal/gui/qt/lists.py`
- In `PreviewList.replace()` the Locktime column item is now rendered in bold
(via its own `QFont`), so the delivery time stands out in the list. The rest
of the list is intentionally left unchanged.
- `tests/test_group_c_settings.py` (new)
- Verifies `EDITABLE_DATES` defaults OFF and can be toggled/persisted, and
that the Reset logic restores all six dialog settings to their defaults
without touching unrelated configuration.
**Verification:**
- `ruff check` on changed Python files: no new errors introduced (the new test
file is ruff-clean; the only added import flagged by ruff is the re-export
`webopen`, matching the existing star-import pattern in `common.py`).
- Full test suite: `210 passed` (206 previous + 4 new Group C tests).
**Outcome:** DONE (delivered as a ZIP for user testing before commit).
### Group C - follow-up fixes (after first user test)
- **C2 (Editable dates) now takes effect immediately and is reset.**
- `bal/gui/qt/widgets.py`: extracted the date-locking logic into
`WillSettingsWidget.apply_editable_dates()`, which re-reads `EDITABLE_DATES`
and locks/unlocks the delivery-time and check-alive fields (fee stays
read-only). Called from `__init__` and re-callable afterwards.
- `bal/gui/qt/window.py`: `update_all()` now calls `apply_editable_dates()` on
the Heirs-tab and Will-tab settings widgets. Since the "Editable dates"
checkbox already triggers `update_all()`, toggling it now updates the date
fields instantly (same mechanism as the "Hide Invalidated" filter).
- `bal/gui/qt/plugin.py`: added `EDITABLE_DATES` (and its checkbox) to the
"Reset setting" list, so Reset also returns it to its default (OFF).
- **C3: fixed the broken date next to the RAW box.**
- `bal/gui/qt/widgets.py`: the trailing label in `TimeRawEditWidget` shows the
ABSOLUTE date computed from the RAW value (e.g. "30d" -> "2027-06-23").
Its width was wrongly shrunk to 2 characters, truncating that date; it is
restored to 10 characters. Only the RAW input box stays narrowed (6 chars).
- **C4b:** the reset button label is now "Reset setting".
- **C4c:** the support link text `bitcoin-after.life` is now shown in bold.
- `tests/test_group_c_settings.py`: the reset test now also covers
`EDITABLE_DATES` (seven settings restored to default, flag back OFF).
**Verification (follow-up):** `ruff` clean on changed files; full suite
`210 passed`.
### Group C - second follow-up (after second user test)
- **Fee icon tooltip (C5):** the small "丰" help icon next to the fee field now
shows the hover tooltip "Miner fee, click for more information" (the longer
explanation still appears on click via the HelpButton).
- **C4c:** the word "Support:" before the link is now also shown in bold (not
only the link text).
- **"Editable inheritance" Raw/Date default:** confirmed the existing behaviour
is "remember the user's last choice" (the Raw/Date combo selection is
persisted in `WILL_SETTINGS` whenever it changes), so no code change was
needed - the dialog reopens on whichever mode the user last used.
**Verification (second follow-up):** `ruff` clean on changed files; full suite
`210 passed`.
### Group C - third follow-up (tooltip font fix)
- **Fee icon tooltip font:** the "丰" button used an unscoped
`font-size: 16px` stylesheet that also enlarged its tooltip, making it bigger
than the other tooltips (e.g. the calendar one). The rule is now scoped to
`QPushButton{...}`, so only the glyph stays large while the tooltip uses the
default font size like the others.
**Verification (third follow-up):** full suite `210 passed`.

View File

@@ -175,6 +175,12 @@ class BalPlugin(BasePlugin):
# (handled by BalWindow.get_wallet_password). Default ON. # (handled by BalWindow.get_wallet_password). Default ON.
self.AUTO_SIGN = BalConfig(config, "bal_auto_sign", True) self.AUTO_SIGN = BalConfig(config, "bal_auto_sign", True)
# EDITABLE_DATES (Group C / C2): when enabled, the delivery-time and
# check-alive date fields are editable everywhere (toolbar / Heirs tab),
# not only inside the "Build your will" wizard. Default OFF, so the dates
# stay display-only outside the wizard unless the user opts in.
self.EDITABLE_DATES = BalConfig(config, "bal_editable_dates", False)
self.NO_WILLEXECUTOR = BalConfig(config, "bal_no_willexecutor", True) self.NO_WILLEXECUTOR = BalConfig(config, "bal_no_willexecutor", True)
self.HIDE_REPLACED = BalConfig(config, "bal_hide_replaced", True) self.HIDE_REPLACED = BalConfig(config, "bal_hide_replaced", True)
self.HIDE_INVALIDATED = BalConfig(config, "bal_hide_invalidated", True) self.HIDE_INVALIDATED = BalConfig(config, "bal_hide_invalidated", True)

View File

@@ -39,7 +39,7 @@ from electrum.gui.qt.util import (Buttons, CancelButton, ColorScheme,
OkButton, TaskThread, WindowModalDialog, OkButton, TaskThread, WindowModalDialog,
char_width_in_lineedit, getSaveFileName, char_width_in_lineedit, getSaveFileName,
import_meta_gui, read_QIcon_from_bytes, import_meta_gui, read_QIcon_from_bytes,
read_QPixmap_from_bytes) read_QPixmap_from_bytes, webopen)
from electrum.i18n import _ from electrum.i18n import _
from electrum.logging import get_logger from electrum.logging import get_logger
from electrum.network import BestEffortRequestFailed, Network, TxBroadcastError from electrum.network import BestEffortRequestFailed, Network, TxBroadcastError

View File

@@ -403,6 +403,16 @@ class PreviewList(MyTreeView, MessageBoxMixin):
items[-1].setBackground(QColor(status_color(bal_tx))) items[-1].setBackground(QColor(status_color(bal_tx)))
# Group C / C6: emphasise the Locktime column by rendering it in bold,
# so the delivery time stands out at a glance in the list.
try:
locktime_item = items[self.Columns.LOCKTIME]
bold_font = locktime_item.font()
bold_font.setBold(True)
locktime_item.setFont(bold_font)
except Exception as bold_err:
_logger.debug(f"locktime bold error: {bold_err}")
# Tooltip on the Server column: shows the will-executor URL (if any) # Tooltip on the Server column: shows the will-executor URL (if any)
# plus the current server state, so the user can always inspect details. # plus the current server state, so the user can always inspect details.
try: try:

View File

@@ -396,13 +396,42 @@ class Plugin(BalPlugin):
# default is ON (see plugin_base.py). # default is ON (see plugin_base.py).
heir_auto_sign = BalCheckBox(self.AUTO_SIGN) heir_auto_sign = BalCheckBox(self.AUTO_SIGN)
# Editable dates checkbox (Group C / C2). When ticked, the delivery-time
# and check-alive date fields become editable everywhere (toolbar /
# Heirs tab), not only inside the "Build your will" wizard. Bound to the
# persisted EDITABLE_DATES config; the default is OFF (see
# plugin_base.py). Changing it refreshes the open windows so the date
# fields immediately become editable/read-only.
heir_editable_dates = BalCheckBox(self.EDITABLE_DATES, on_multiverse_change)
# Editable line/text widgets are created once and kept in named
# variables so the "Reset" button (Group C / C4b) can refresh the
# displayed values after resetting the underlying config.
edit_calendar_app = BalLineEdit(self.CALENDAR_APP)
edit_event_summary = BalLineEdit(self.EVENT_SUMMARY)
edit_event_description = BalTextEdit(self.EVENT_DESCRIPTION)
heir_repush = QPushButton("Rebroadcast transactions") heir_repush = QPushButton("Rebroadcast transactions")
heir_repush.clicked.connect(partial(self.broadcast_transactions, True)) heir_repush.clicked.connect(partial(self.broadcast_transactions, True))
bal_mode = QComboBox() bal_mode = QComboBox()
options = ["Easy", "Advanced", "Experimental"] options = ["Easy", "Advanced", "Experimental"]
bal_mode.addItems(options) bal_mode.addItems(options)
grid = QGridLayout(d) # Group C / C4a: warning shown at the very top of the dialog, in red and
# bold, so the (non-technical) user is reminded not to touch these
# settings unless they understand them. Placed above the grid via the
# outer vertical layout below.
lbl_warning = QLabel(
_("Warning: change these settings only if you know what you are doing.")
)
lbl_warning.setStyleSheet("color: red; font-weight: bold;")
lbl_warning.setWordWrap(True)
# The grid is created WITHOUT a parent so it can be embedded inside an
# outer QVBoxLayout together with the warning label (top) and the
# Reset/support button row (bottom). Assigning ``QGridLayout(d)`` would
# have made the grid the dialog's only layout, leaving no room for them.
grid = QGridLayout()
add_widget( add_widget(
grid, grid,
"Hide Replaced", "Hide Replaced",
@@ -431,16 +460,28 @@ class Plugin(BalPlugin):
) )
add_widget( add_widget(
grid, grid,
"Calendar App", "Editable dates",
BalLineEdit(self.CALENDAR_APP), heir_editable_dates,
4, 4,
(
"When enabled, the delivery-time and check-alive date fields "
"can be edited everywhere (toolbar / Heirs tab), not only in "
"the will-building wizard.\n"
"When disabled, those dates are display-only outside the wizard."
),
)
add_widget(
grid,
"Calendar App",
edit_calendar_app,
5,
"Default app used to open calendar", "Default app used to open calendar",
) )
add_widget( add_widget(
grid, grid,
"Event summary", "Event summary",
BalLineEdit(self.EVENT_SUMMARY), edit_event_summary,
5, 6,
( (
"Default message to be used in event summary\n" "Default message to be used in event summary\n"
"Variables:\n" "Variables:\n"
@@ -452,8 +493,8 @@ class Plugin(BalPlugin):
add_widget( add_widget(
grid, grid,
"Event description", "Event description",
BalTextEdit(self.EVENT_DESCRIPTION), edit_event_description,
6, 7,
( (
"Default message to be used in event description\n" "Default message to be used in event description\n"
"Variables:\n" "Variables:\n"
@@ -486,15 +527,87 @@ class Plugin(BalPlugin):
# "Add transactions without willexecutor", # "Add transactions without willexecutor",
# ) # )
# add_widget(grid,"Enable Multiverse(EXPERIMENTAL/BROKEN)",heir_enable_multiverse,6,"enable multiple locktimes, will import.... ") # add_widget(grid,"Enable Multiverse(EXPERIMENTAL/BROKEN)",heir_enable_multiverse,6,"enable multiple locktimes, will import.... ")
grid.addWidget(heir_repush, 7, 0) grid.addWidget(heir_repush, 8, 0)
grid.addWidget( grid.addWidget(
HelpButton( HelpButton(
"Broadcast all transactions to willexecutors including those already pushed" "Broadcast all transactions to willexecutors including those already pushed"
), ),
7, 8,
2, 2,
) )
# ----------------------------------------------------------------- #
# Group C / C4b: "Reset" button that restores the dialog settings to #
# their factory defaults. It only resets the settings exposed by THIS #
# dialog (the 6 below) and refreshes the corresponding widgets so the #
# change is visible immediately. It deliberately does NOT touch the #
# wills, will-executors or any other configuration. #
# ----------------------------------------------------------------- #
def on_reset_defaults():
"""Reset the six dialog settings to their defaults and refresh widgets.
The default value of each setting is taken from ``BalConfig.default``
(the third argument used when the config was created in
``plugin_base.py``), so there is a single source of truth and no
hard-coded duplicates here.
"""
# Map each config object to the widget that displays it, so we can
# both reset the stored value and update what the user sees.
resets = [
(self.HIDE_REPLACED, heir_hide_replaced, "check"),
(self.HIDE_INVALIDATED, heir_hide_invalidated, "check"),
(self.AUTO_SIGN, heir_auto_sign, "check"),
(self.EDITABLE_DATES, heir_editable_dates, "check"),
(self.CALENDAR_APP, edit_calendar_app, "line"),
(self.EVENT_SUMMARY, edit_event_summary, "line"),
(self.EVENT_DESCRIPTION, edit_event_description, "text"),
]
for cfg, widget, kind in resets:
# Persist the default value back into the Electrum config.
cfg.set(cfg.default)
# Refresh the widget so the reset is immediately visible. The
# widgets' own signal handlers will re-persist the same default
# value, which is harmless.
if kind == "check":
widget.setChecked(bool(cfg.default))
elif kind == "line":
widget.setText(cfg.default)
elif kind == "text":
widget.setPlainText(cfg.default)
# Refresh the open BAL windows so any dependent view (e.g. the
# editable-dates state is not in this list, but hide filters are)
# reflects the reset values.
self.update_all()
btn_reset = QPushButton(_("Reset setting"))
btn_reset.setToolTip(_("Reset these settings to their default values"))
btn_reset.clicked.connect(on_reset_defaults)
# Group C / C4c: clickable support link to the project website.
lbl_support = QLabel(
'<a href="https://bitcoin-after.life"><b>bitcoin-after.life</b></a>'
)
lbl_support.setToolTip(_("Open the Bitcoin After Life support website"))
# Open the link in the user's browser via Electrum's helper instead of
# letting Qt open it directly, so it goes through Electrum's policy.
lbl_support.setOpenExternalLinks(False)
lbl_support.linkActivated.connect(
lambda _url: webopen("https://bitcoin-after.life")
)
# Bottom row: Reset on the left, support link on the right.
bottom_row = QHBoxLayout()
bottom_row.addWidget(btn_reset)
bottom_row.addStretch(1)
bottom_row.addWidget(QLabel("<b>" + _("Support:") + "</b>"))
bottom_row.addWidget(lbl_support)
# Outer layout: warning (top) -> settings grid -> bottom button row.
outer = QVBoxLayout(d)
outer.addWidget(lbl_warning)
outer.addLayout(grid)
outer.addLayout(bottom_row)
if ret := bool(show_modal(d)): if ret := bool(show_modal(d)):
try: try:
self.update_all() self.update_all()

View File

@@ -43,6 +43,10 @@ class BalTxFeesWidget(QWidget):
self.txfee_widget = QSpinBox(self) self.txfee_widget = QSpinBox(self)
self.txfee_widget.setMinimum(1) self.txfee_widget.setMinimum(1)
self.txfee_widget.setMaximum(10000) self.txfee_widget.setMaximum(10000)
# Group C / C5: hovering the fee field explains what the number means.
self.txfee_widget.setToolTip(
_("Mining fee rate in sat/vByte used for the will transactions")
)
value = ( value = (
value value
if value if value
@@ -58,7 +62,14 @@ class BalTxFeesWidget(QWidget):
#layout.addWidget(label) #layout.addWidget(label)
button = HelpButton(_("mining fees expressed in sats/vbyte to be used in the Bitcoin transaction.\nHigher value ensure your transaction will be confirmed")) button = HelpButton(_("mining fees expressed in sats/vbyte to be used in the Bitcoin transaction.\nHigher value ensure your transaction will be confirmed"))
button.setText("") button.setText("")
button.setStyleSheet("font-size: 16px;") # Hover tooltip for the small "丰" help icon (the long explanation still
# appears on click via the HelpButton); makes the icon self-explanatory.
button.setToolTip(_("Miner fee, click for more information"))
# Enlarge only the "丰" glyph on the button itself; without scoping the
# rule to QPushButton it also enlarged the tooltip font (making it bigger
# than the other tooltips, e.g. the calendar one). Scoping it keeps the
# tooltip at the default size like everywhere else.
button.setStyleSheet("QPushButton{font-size: 16px;}")
layout.addWidget(button) layout.addWidget(button)
layout.addWidget(self.txfee_widget) layout.addWidget(self.txfee_widget)
# Expose the leading icon (prefix) and the editable field so the parent # Expose the leading icon (prefix) and the editable field so the parent
@@ -322,6 +333,11 @@ class TimeRawEditWidget(QWidget):
super().__init__(parent) super().__init__(parent)
self.editor = LockTimeRawEdit(parent, time_edit) self.editor = LockTimeRawEdit(parent, time_edit)
self.label = QLabel("") self.label = QLabel("")
# Group C / C3: this trailing label shows the ABSOLUTE date computed
# from the RAW value (e.g. "30d" -> "2027-06-23"), so it needs room for a
# full "YYYY-MM-DD" string (~10 characters). Only the input box itself
# (LockTimeRawEdit) is narrowed; shrinking this label was a mistake that
# truncated the computed date.
self.label.setFixedWidth(10 * char_width_in_lineedit()) self.label.setFixedWidth(10 * char_width_in_lineedit())
self.layout = QHBoxLayout(self) self.layout = QHBoxLayout(self)
self.layout.addWidget(self.editor) self.layout.addWidget(self.editor)
@@ -343,7 +359,11 @@ class TimeRawEditWidget(QWidget):
class LockTimeRawEdit(QLineEdit, _LockTimeEditor): class LockTimeRawEdit(QLineEdit, _LockTimeEditor):
def __init__(self, parent=None, time_edit=None): def __init__(self, parent=None, time_edit=None):
QLineEdit.__init__(self, parent) QLineEdit.__init__(self, parent)
self.setFixedWidth(12 * char_width_in_lineedit()) # Group C / C3: narrow the RAW input to roughly a third of its former
# width. The accepted values are short relative durations such as "30d"
# or "1y", so a handful of characters is plenty while still leaving room
# for larger day counts (e.g. "3650d").
self.setFixedWidth(6 * char_width_in_lineedit())
self.textChanged.connect(self.numbify) self.textChanged.connect(self.numbify)
self.isdays = False self.isdays = False
self.isyears = False self.isyears = False
@@ -554,8 +574,10 @@ class WillSettingsWidget(QWidget):
self.bal_window.bal_plugin.read_file("icons/calendar.png") self.bal_window.bal_plugin.read_file("icons/calendar.png")
) )
) )
# Tooltip so the icon is self-explanatory when hovered. # Tooltip so the icon is self-explanatory when hovered (Group C / C5).
self.calendar_button.setToolTip(_("Calendar")) self.calendar_button.setToolTip(
_("Export reminder dates to your calendar (.ics)")
)
self.calendar_button.clicked.connect(self.open_or_save_calendar) self.calendar_button.clicked.connect(self.open_or_save_calendar)
self.widgets["locktime"] = LockTimeWidget(bal_window, self) self.widgets["locktime"] = LockTimeWidget(bal_window, self)
self.widgets["threshold"] = ThresholdTimeWidget(bal_window, self) self.widgets["threshold"] = ThresholdTimeWidget(bal_window, self)
@@ -623,10 +645,42 @@ class WillSettingsWidget(QWidget):
box.addWidget(calendar_row, alignment=Qt.AlignmentFlag.AlignLeft) box.addWidget(calendar_row, alignment=Qt.AlignmentFlag.AlignLeft)
box.addWidget(fees_w, alignment=Qt.AlignmentFlag.AlignLeft) box.addWidget(fees_w, alignment=Qt.AlignmentFlag.AlignLeft)
if self.read_only: # Group C / C2: apply the current "Editable dates" setting to the date
self.widgets["locktime"].set_read_only(True) # fields. Done once at creation here, and re-applied later by
self.widgets["threshold"].set_read_only(True) # apply_editable_dates() whenever the setting changes (called from
self.widgets["baltx_fees"].set_read_only(True) # BalWindow.update_all()), so toggling the checkbox takes effect
# immediately, exactly like the "Hide Invalidated" filter does.
self.apply_editable_dates()
def apply_editable_dates(self):
"""Re-read the EDITABLE_DATES setting and lock/unlock the date fields.
Outside the "Build your will" wizard (``read_only=True``) the
delivery-time and check-alive dates are display-only by default. When
the user ticks "Editable dates" in the settings they become editable
here too. The fee field always stays read-only outside the wizard,
because C2 only concerns the dates.
This is safe to call repeatedly: it only adjusts the read-only state of
the already-created sub-widgets, it does not rebuild anything. It is the
per-update hook that lets the settings checkbox take effect without
having to recreate the toolbar / re-open the window.
"""
# Inside the wizard the dates are always editable; nothing to do.
if not self.read_only:
return
editable_dates = False
try:
editable_dates = self.bal_window.bal_plugin.EDITABLE_DATES.get()
except Exception:
# If the setting cannot be read, fall back to the safe default
# (dates remain read-only outside the wizard).
editable_dates = False
self.widgets["locktime"].set_read_only(not editable_dates)
self.widgets["threshold"].set_read_only(not editable_dates)
self.widgets["baltx_fees"].set_read_only(True)
def create_alarms(self, alarm_start, alarm_end): def create_alarms(self, alarm_start, alarm_end):
days = (alarm_end - alarm_start).days+1 days = (alarm_end - alarm_start).days+1

View File

@@ -1300,6 +1300,20 @@ class BalWindow:
self.heirs_tab.update() self.heirs_tab.update()
self.will_tab.update() self.will_tab.update()
self.will_list_widget.update() self.will_list_widget.update()
# Group C / C2: re-apply the "Editable dates" setting to the date
# fields of the toolbars / Heirs tab. The settings checkbox calls
# update_all() when toggled, so this makes the change take effect
# immediately (same pattern as sync_hide_filters above for the
# "Hide Invalidated/Replaced" checkboxes). Guarded so a missing
# widget never breaks the rest of the refresh.
for _list in (self.heir_list_widget, self.will_list_widget):
_settings_widget = getattr(_list, "will_settings_widget", None)
if _settings_widget is not None:
try:
_settings_widget.apply_editable_dates()
except Exception as _edit_err:
_logger.debug(f"apply_editable_dates error: {_edit_err}")
except Exception as e: except Exception as e:
_logger.error(f"error while updating window: {e}") _logger.error(f"error while updating window: {e}")

View File

@@ -0,0 +1,158 @@
"""
Tests for Group C (settings-dialog enhancements).
Covered behaviour:
* C2 - the persisted ``EDITABLE_DATES`` configuration key exists, defaults to
OFF, and can be toggled and read back. This is the flag that makes the
delivery-time / check-alive date fields editable outside the wizard.
* C4b - the "Reset" logic restores each of the six dialog settings to its
factory default. The factory default is taken from ``BalConfig.default``
(the third argument used when the config was created), so the dialog's
Reset button has a single source of truth and never hard-codes values.
The Qt widgets are not imported (they need PyQt6 + an Electrum window). Instead
we reproduce the small, GUI-free decision logic with the same lightweight
``FakeConfig`` used by the Group B tests, which keeps the tests fast and
headless while still verifying the real contract.
Run:
PYTHONPATH=electrum-src python3 -m pytest tests/test_group_c_settings.py -q
"""
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from bal.core.plugin_base import BalConfig
# ------------------------------------------------------------------ #
# Mocks
# ------------------------------------------------------------------ #
class FakeConfig:
"""Minimal mock for Electrum's config object (key/value store)."""
def __init__(self):
self._store = {}
def get(self, key, default=None):
return self._store.get(key, default)
def set_key(self, key, value, save=True):
self._store[key] = value
# ------------------------------------------------------------------ #
# C2 - EDITABLE_DATES config
# ------------------------------------------------------------------ #
def test_editable_dates_defaults_off():
"""C2: the editable-dates flag defaults to OFF (dates display-only)."""
cfg = FakeConfig()
editable = BalConfig(cfg, "bal_editable_dates", False)
assert editable.get() is False
def test_editable_dates_can_be_enabled():
"""C2: turning the flag ON is persisted and read back as True."""
cfg = FakeConfig()
editable = BalConfig(cfg, "bal_editable_dates", False)
editable.set(True)
# Re-read through a fresh accessor to prove it is persisted in the config.
assert BalConfig(cfg, "bal_editable_dates", False).get() is True
# ------------------------------------------------------------------ #
# C4b - Reset to defaults
# ------------------------------------------------------------------ #
def _reset_to_defaults(configs):
"""Reproduce the dialog's Reset logic: set each config back to its default.
This mirrors ``on_reset_defaults`` in ``bal.gui.qt.plugin`` (which also
refreshes the Qt widgets). Here we only verify the persistence side: every
config is reset to ``BalConfig.default``.
"""
for cfg in configs:
cfg.set(cfg.default)
def test_reset_restores_all_dialog_settings():
"""C4b: Reset restores every dialog setting to its factory default.
The dialog exposes seven settings: the original six plus the Group C
"Editable dates" checkbox, which the Reset button must also restore (this
was a follow-up fix after the first test round).
"""
cfg = FakeConfig()
# The settings exposed by the dialog, with their real defaults.
hide_replaced = BalConfig(cfg, "bal_hide_replaced", True)
hide_invalidated = BalConfig(cfg, "bal_hide_invalidated", True)
auto_sign = BalConfig(cfg, "bal_auto_sign", True)
editable_dates = BalConfig(cfg, "bal_editable_dates", False)
calendar_app = BalConfig(cfg, "bal_open_app", "xdg-open")
event_summary = BalConfig(
cfg, "bal_event_summary", "BAL -Will execution of $wallet_name"
)
event_description = BalConfig(
cfg,
"bal_event_description",
"BAL will execution of $wallet_name\r\n heirs list: \r\n$heirs_complete",
)
settings = [
hide_replaced,
hide_invalidated,
auto_sign,
editable_dates,
calendar_app,
event_summary,
event_description,
]
# Mutate every setting away from its default.
hide_replaced.set(False)
hide_invalidated.set(False)
auto_sign.set(False)
editable_dates.set(True)
calendar_app.set("/custom/app")
event_summary.set("custom summary")
event_description.set("custom description")
# Sanity: the values really changed.
assert hide_replaced.get() is False
assert editable_dates.get() is True
assert calendar_app.get() == "/custom/app"
# Reset and verify each one is back to its declared default.
_reset_to_defaults(settings)
for s in settings:
assert s.get() == s.default
# In particular the "Editable dates" flag is back OFF.
assert editable_dates.get() is False
def test_reset_does_not_touch_unrelated_settings():
"""C4b: Reset only changes the listed settings, nothing else.
A will / will-executor style key that is NOT part of the dialog list must
keep its value after a reset of the six dialog settings.
"""
cfg = FakeConfig()
unrelated = BalConfig(cfg, "bal_will_settings", {"x": 1})
unrelated.set({"custom": "value"})
dialog_settings = [
BalConfig(cfg, "bal_hide_replaced", True),
BalConfig(cfg, "bal_auto_sign", True),
]
for s in dialog_settings:
s.set(False)
_reset_to_defaults(dialog_settings)
# The unrelated key is untouched by the dialog reset.
assert unrelated.get() == {"custom": "value"}