From 9d2cbc28144c5adb23d4acd86a5ded72f060300e Mon Sep 17 00:00:00 2001 From: donkey-ai Date: Sun, 28 Jun 2026 23:00:30 -0400 Subject: [PATCH] feat(bal): Group C settings-dialog improvements (editable dates, narrower RAW box, warning/reset/support, tooltips, bold locktime) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CHANGELOG.md | 131 +++++++++++++++++++++++++++ bal/core/plugin_base.py | 6 ++ bal/gui/qt/common.py | 2 +- bal/gui/qt/lists.py | 10 +++ bal/gui/qt/plugin.py | 131 +++++++++++++++++++++++++-- bal/gui/qt/widgets.py | 70 +++++++++++++-- bal/gui/qt/window.py | 14 +++ tests/test_group_c_settings.py | 158 +++++++++++++++++++++++++++++++++ 8 files changed, 504 insertions(+), 18 deletions(-) create mode 100644 tests/test_group_c_settings.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d9a87b..0b06e76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -378,3 +378,134 @@ MEMPOOL (yellow `#ffce30`). - Full test suite: `206 passed`. **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`. diff --git a/bal/core/plugin_base.py b/bal/core/plugin_base.py index 9f98afb..794980e 100644 --- a/bal/core/plugin_base.py +++ b/bal/core/plugin_base.py @@ -175,6 +175,12 @@ class BalPlugin(BasePlugin): # (handled by BalWindow.get_wallet_password). Default ON. 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.HIDE_REPLACED = BalConfig(config, "bal_hide_replaced", True) self.HIDE_INVALIDATED = BalConfig(config, "bal_hide_invalidated", True) diff --git a/bal/gui/qt/common.py b/bal/gui/qt/common.py index 38a4710..e3ede1c 100644 --- a/bal/gui/qt/common.py +++ b/bal/gui/qt/common.py @@ -39,7 +39,7 @@ from electrum.gui.qt.util import (Buttons, CancelButton, ColorScheme, OkButton, TaskThread, WindowModalDialog, char_width_in_lineedit, getSaveFileName, import_meta_gui, read_QIcon_from_bytes, - read_QPixmap_from_bytes) + read_QPixmap_from_bytes, webopen) from electrum.i18n import _ from electrum.logging import get_logger from electrum.network import BestEffortRequestFailed, Network, TxBroadcastError diff --git a/bal/gui/qt/lists.py b/bal/gui/qt/lists.py index 08fdc71..0116637 100644 --- a/bal/gui/qt/lists.py +++ b/bal/gui/qt/lists.py @@ -403,6 +403,16 @@ class PreviewList(MyTreeView, MessageBoxMixin): 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) # plus the current server state, so the user can always inspect details. try: diff --git a/bal/gui/qt/plugin.py b/bal/gui/qt/plugin.py index bef91ae..564975c 100644 --- a/bal/gui/qt/plugin.py +++ b/bal/gui/qt/plugin.py @@ -396,13 +396,42 @@ class Plugin(BalPlugin): # default is ON (see plugin_base.py). 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.clicked.connect(partial(self.broadcast_transactions, True)) bal_mode = QComboBox() options = ["Easy", "Advanced", "Experimental"] 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( grid, "Hide Replaced", @@ -431,16 +460,28 @@ class Plugin(BalPlugin): ) add_widget( grid, - "Calendar App", - BalLineEdit(self.CALENDAR_APP), + "Editable dates", + heir_editable_dates, 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", ) add_widget( grid, "Event summary", - BalLineEdit(self.EVENT_SUMMARY), - 5, + edit_event_summary, + 6, ( "Default message to be used in event summary\n" "Variables:\n" @@ -452,8 +493,8 @@ class Plugin(BalPlugin): add_widget( grid, "Event description", - BalTextEdit(self.EVENT_DESCRIPTION), - 6, + edit_event_description, + 7, ( "Default message to be used in event description\n" "Variables:\n" @@ -486,15 +527,87 @@ class Plugin(BalPlugin): # "Add transactions without willexecutor", # ) # 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( HelpButton( "Broadcast all transactions to willexecutors including those already pushed" ), - 7, + 8, 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( + 'bitcoin-after.life' + ) + 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("" + _("Support:") + "")) + 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)): try: self.update_all() diff --git a/bal/gui/qt/widgets.py b/bal/gui/qt/widgets.py index 31c8d00..2c12af5 100644 --- a/bal/gui/qt/widgets.py +++ b/bal/gui/qt/widgets.py @@ -43,6 +43,10 @@ class BalTxFeesWidget(QWidget): self.txfee_widget = QSpinBox(self) self.txfee_widget.setMinimum(1) 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 if value @@ -58,7 +62,14 @@ class BalTxFeesWidget(QWidget): #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.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(self.txfee_widget) # Expose the leading icon (prefix) and the editable field so the parent @@ -322,6 +333,11 @@ class TimeRawEditWidget(QWidget): super().__init__(parent) self.editor = LockTimeRawEdit(parent, time_edit) 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.layout = QHBoxLayout(self) self.layout.addWidget(self.editor) @@ -343,7 +359,11 @@ class TimeRawEditWidget(QWidget): class LockTimeRawEdit(QLineEdit, _LockTimeEditor): def __init__(self, parent=None, time_edit=None): 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.isdays = False self.isyears = False @@ -554,8 +574,10 @@ class WillSettingsWidget(QWidget): self.bal_window.bal_plugin.read_file("icons/calendar.png") ) ) - # Tooltip so the icon is self-explanatory when hovered. - self.calendar_button.setToolTip(_("Calendar")) + # Tooltip so the icon is self-explanatory when hovered (Group C / C5). + self.calendar_button.setToolTip( + _("Export reminder dates to your calendar (.ics)") + ) self.calendar_button.clicked.connect(self.open_or_save_calendar) self.widgets["locktime"] = LockTimeWidget(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(fees_w, alignment=Qt.AlignmentFlag.AlignLeft) - if self.read_only: - self.widgets["locktime"].set_read_only(True) - self.widgets["threshold"].set_read_only(True) - self.widgets["baltx_fees"].set_read_only(True) + # Group C / C2: apply the current "Editable dates" setting to the date + # fields. Done once at creation here, and re-applied later by + # apply_editable_dates() whenever the setting changes (called from + # 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): days = (alarm_end - alarm_start).days+1 diff --git a/bal/gui/qt/window.py b/bal/gui/qt/window.py index e088177..6b4593a 100644 --- a/bal/gui/qt/window.py +++ b/bal/gui/qt/window.py @@ -1300,6 +1300,20 @@ class BalWindow: self.heirs_tab.update() self.will_tab.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: _logger.error(f"error while updating window: {e}") diff --git a/tests/test_group_c_settings.py b/tests/test_group_c_settings.py new file mode 100644 index 0000000..4f200eb --- /dev/null +++ b/tests/test_group_c_settings.py @@ -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"}