diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a6385e..7b7f00b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -678,3 +678,34 @@ change takes effect immediately (the method is already re-run from **Outcome:** DONE (delivered together with fix #10 in a single ZIP for user testing before commit). + +## 12. Translated the refactoring docs to English and renamed two of them + +**Request:** per rule R1 (all output, including documentation, must be in +English), translate the remaining Italian documentation files. The user also +asked to translate the file *names* of two of them. + +**What changed:** +- `CHANGELOG_REFACTOR.md` + - Translated sections 1-16 (the whole report header plus §1-§16) from Italian + to English. Sections 17-18 were already in English and were left untouched. + - All technical content was preserved verbatim: code blocks, commit hashes, + tables, file/class/method names, version numbers and structure. + - Updated the §12 history line to mention the new name of the diagnosis file + (`GUI_DIAGNOSIS.md`, originally `DIAGNOSI_GUI.md`). +- `DIAGNOSI_GUI.md` → renamed to `GUI_DIAGNOSIS.md` (via `git mv`) and fully + translated to English, **title included**. All code blocks, line references, + severity emojis, tables and structure were preserved. +- `REPORT_NETWORKING_PARALLELO.md` → renamed to `PARALLEL_NETWORKING_REPORT.md` + (via `git mv`). Its content was already in English, so only the file name was + translated. + +**Verification:** +- Scanned the translated regions for leftover Italian words: none found (only + English false-positives from the regex). +- Confirmed sections 17-18 of `CHANGELOG_REFACTOR.md` are unchanged + (the diff hunks stop before §17). +- Confirmed no other file in the repo still references the old file names. + +**Outcome:** DONE (documentation-only change; no plugin code touched, so the +zip-first step does not apply). diff --git a/CHANGELOG_REFACTOR.md b/CHANGELOG_REFACTOR.md index 4d7484d..de6714c 100644 --- a/CHANGELOG_REFACTOR.md +++ b/CHANGELOG_REFACTOR.md @@ -1,66 +1,66 @@ -# BAL — Resoconto del refactoring (per l'autore originale) +# BAL — Refactoring report (for the original author) -Questo documento elenca **tutte** le modifiche apportate al plugin BAL -(Bitcoin After Life) rispetto alla versione originale `0.2.8`. +This document lists **all** the changes made to the BAL plugin +(Bitcoin After Life) with respect to the original version `0.2.8`. -**Principio guida:** refactoring **conservativo e a comportamento invariato** -(Approccio A). La logica di business è stata mantenuta **byte-identica** dove -possibile; sono cambiati soprattutto la **disposizione dei file** e gli -**import**. Nessuna riscrittura algoritmica. +**Guiding principle:** **conservative, behaviour-preserving** refactoring +(Approach A). The business logic was kept **byte-identical** where possible; +what changed are mainly the **file layout** and the **imports**. No algorithmic +rewrite. -Ambiente di verifica: **Electrum 4.7.2** + **PyQt6** (l'ultima release stabile -che espone `json_db.register_dict`). +Verification environment: **Electrum 4.7.2** + **PyQt6** (the latest stable +release that exposes `json_db.register_dict`). --- -## 1. Riorganizzazione della struttura (separazione logica / GUI) +## 1. Structure reorganization (logic / GUI separation) -Il problema principale segnalato era che la logica e la grafica erano -mescolate, in particolare in un unico file `qt.py` da **4131 righe**. +The main reported problem was that logic and graphics were mixed together, in +particular in a single `qt.py` file of **4131 lines**. -### Struttura PRIMA (flat, 7 file) +### Structure BEFORE (flat, 7 files) ``` BAL/ -├── __init__.py (vuoto, 0 righe) -├── bal.py (243) logica + plugin base -├── util.py (533) helper -├── heirs.py (791) modello eredi + costruzione tx -├── will.py (927) modello will/WillItem -├── willexecutors.py (374) networking will-executor -├── qt.py (4131) TUTTA la GUI + il Plugin in un solo file +├── __init__.py (empty, 0 lines) +├── bal.py (243) logic + plugin base +├── util.py (533) helpers +├── heirs.py (791) heirs model + tx building +├── will.py (927) will/WillItem model +├── willexecutors.py (374) will-executor networking +├── qt.py (4131) ALL the GUI + the Plugin in a single file └── bal_resources.py (14) ``` -### Struttura DOPO (core/ vs gui/) +### Structure AFTER (core/ vs gui/) ``` bal/ -├── manifest.json metadati conformi allo standard -├── qt.py shim di caricamento (re-export di Plugin) -├── __init__.py docstring di architettura + __version__ -├── core/ LOGICA senza dipendenze Qt -│ ├── util.py (ex util.py) -│ ├── plugin_base.py (ex bal.py) -│ ├── heirs.py (ex heirs.py) -│ ├── will.py (ex will.py) -│ └── willexecutors.py (ex willexecutors.py) -└── gui/qt/ PRESENTAZIONE PyQt6 - ├── theme.py (59) mappatura stato → colore - ├── common.py (155) import condivisi + helper GUI - ├── widgets.py (782) widget "foglia" +├── manifest.json standard-compliant metadata +├── qt.py loading shim (re-export of Plugin) +├── __init__.py architecture docstring + __version__ +├── core/ LOGIC with no Qt dependencies +│ ├── util.py (was util.py) +│ ├── plugin_base.py (was bal.py) +│ ├── heirs.py (was heirs.py) +│ ├── will.py (was will.py) +│ └── willexecutors.py (was willexecutors.py) +└── gui/qt/ PyQt6 PRESENTATION + ├── theme.py (59) status → colour mapping + ├── common.py (155) shared imports + GUI helpers + ├── widgets.py (782) "leaf" widgets ├── calendar.py (80) BalCalendar - ├── dialogs.py (1127) finestre di dialogo - ├── lists.py (957) viste ad albero (eredi/preview/executor) - ├── window.py (952) controller GUI per-wallet (BalWindow) - └── plugin.py (273) classe Plugin (@hook Electrum → GUI) + ├── dialogs.py (1127) dialog windows + ├── lists.py (957) tree views (heirs/preview/executor) + ├── window.py (952) per-wallet GUI controller (BalWindow) + └── plugin.py (273) Plugin class (@hook Electrum → GUI) ``` -Il file `qt.py` da 4131 righe è stato suddiviso per **responsabilità**. I -**corpi delle classi sono stati copiati verbatim** (riga per riga) per non -toccare la logica delicata delle transazioni di eredità. +The 4131-line `qt.py` file was split by **responsibility**. The **class bodies +were copied verbatim** (line by line) so as not to touch the delicate +inheritance-transaction logic. -### Mappa: dove sono finite le 40 classi/funzioni di `qt.py` +### Map: where the 40 classes/functions of `qt.py` ended up -| Classe/funzione (riga orig.) | Nuovo modulo | +| Class/function (orig. line) | New module | |-------------------------------------|-------------------------| | `Plugin` (67) | `gui/qt/plugin.py` | | `shown_cv` (317) | `gui/qt/common.py` | @@ -104,257 +104,255 @@ toccare la logica delicata delle transazioni di eredità. --- -## 2. Rimozioni (codice morto / debug) — comportamento invariato +## 2. Removals (dead / debug code) — behaviour unchanged -Tutte le rimozioni seguenti sono state verificate come **non utilizzate** o -**puramente di debug**, quindi non alterano il comportamento del plugin. +All the following removals were verified as **unused** or **purely debug**, so +they do not alter the plugin's behaviour. -1. **`util.py` → `core/util.py`**: rimossi tre helper di debug usati solo per - stampe a console: - - `print_var()` (orig. riga 439) - - `print_utxo()` (orig. riga 474) - - `print_prevout()` (orig. riga 486) +1. **`util.py` → `core/util.py`**: removed three debug helpers used only for + console printing: + - `print_var()` (orig. line 439) + - `print_utxo()` (orig. line 474) + - `print_prevout()` (orig. line 486) -2. **`bal.py` → `core/plugin_base.py`**: rimossa la funzione **stub vuota** - `get_will_settings(x)` (orig. righe 12-14): +2. **`bal.py` → `core/plugin_base.py`**: removed the **empty stub** function + `get_will_settings(x)` (orig. lines 12-14): ```python def get_will_settings(x): # print(x) pass ``` - ⚠️ Verificato: **non era riferita da nessun `register_dict`** — i tre - `register_dict` usano `tuple`, `dict`, `lambda x: x`. Quindi era codice - morto. La funzione **usata** `get_will(x)` è stata mantenuta identica. + ⚠️ Verified: **it was not referenced by any `register_dict`** — the three + `register_dict` calls use `tuple`, `dict`, `lambda x: x`. So it was dead + code. The **used** function `get_will(x)` was kept identical. -3. **`will.py` (`WillItem`) → spostato in `gui/qt/theme.py`**: il metodo - `WillItem.get_color()` (orig. riga 852) restituiva colori esadecimali — - è logica di **presentazione**, non di dominio. È stato spostato fuori dal - modello e trasformato nella funzione `status_color(will_item)` in - `gui/qt/theme.py`. **Verificato byte-identico** su tutte le combinazioni di - stato (stessa catena di `get_status(...)`, stessi codici colore). +3. **`will.py` (`WillItem`) → moved to `gui/qt/theme.py`**: the method + `WillItem.get_color()` (orig. line 852) returned hexadecimal colours — + it is **presentation** logic, not domain logic. It was moved out of the + model and turned into the function `status_color(will_item)` in + `gui/qt/theme.py`. **Verified byte-identical** across all status + combinations (same `get_status(...)` chain, same colour codes). --- -## 3. Cambi di import (necessari per la nuova struttura) +## 3. Import changes (required by the new structure) -Gli import sono stati aggiornati da "flat" a "a package". Esempi: +The imports were updated from "flat" to "package" style. Examples: -| Prima | Dopo | +| Before | After | |------------------------------------|---------------------------------------| | `from .bal import BalPlugin` | `from .plugin_base import BalPlugin` (in willexecutors) | -| `from .util import Util` | `from .util import Util` (invariato, ora dentro core/) | -| (in qt.py) `from .bal import ...` | i moduli GUI importano da `...core.X` | +| `from .util import Util` | `from .util import Util` (unchanged, now inside core/) | +| (in qt.py) `from .bal import ...` | the GUI modules import from `...core.X` | -- Aggiunti `from .common import _, _logger` nei moduli GUI, perché `import *` - **non** esporta i nomi che iniziano con underscore. -- Aggiunti 3 import "lazy" (dentro le funzioni) in `dialogs.py` per spezzare il - ciclo `dialogs ↔ lists` (lists importa `BalBuildWillDialog` da dialogs). +- Added `from .common import _, _logger` in the GUI modules, because `import *` + does **not** export names starting with an underscore. +- Added 3 "lazy" imports (inside the functions) in `dialogs.py` to break the + `dialogs ↔ lists` cycle (lists imports `BalBuildWillDialog` from dialogs). -La **logica interna dei metodi** non è stata toccata: `prepare_transactions()`, -`buildTransactions()` ecc. sono verbatim. +The **internal logic of the methods** was not touched: `prepare_transactions()`, +`buildTransactions()`, etc. are verbatim. --- -## 4. Packaging conforme allo standard Electrum +## 4. Electrum-standard packaging -`manifest.json` reso conforme a https://plugins.electrum.org/developers.html : +`manifest.json` made compliant with https://plugins.electrum.org/developers.html : -| Campo | Prima | Dopo | +| Field | Before | After | |------------------|--------------------|-------------------------------| -| `name` | `"BAL"` | `"bal"` (minuscolo = nome dir) | -| `version` | (assente, era solo nella description) | `"0.2.8"` | -| `description` | con `
` HTML | testo pulito | -| `licence` | (assente) | `"MIT"` | -| `fullname`/`author`/`available_for`/`icon` | presenti | invariati | +| `name` | `"BAL"` | `"bal"` (lowercase = dir name) | +| `version` | (absent, was only in the description) | `"0.2.8"` | +| `description` | with HTML `
` | clean text | +| `licence` | (absent) | `"MIT"` | +| `fullname`/`author`/`available_for`/`icon` | present | unchanged | -- `__init__.py` (era **vuoto**): ora contiene la docstring di architettura e +- `__init__.py` (was **empty**): now contains the architecture docstring and `__version__ = "0.2.8"`. -- Portati nel package: `LICENSE`, `VERSION`, `README.md`, `bal_resources.py`, - e la cartella `wallet_util/` (invariata). +- Brought into the package: `LICENSE`, `VERSION`, `README.md`, `bal_resources.py`, + and the `wallet_util/` folder (unchanged). --- -## 5. CORREZIONE BUG: caricamento come plugin esterno (.zip) +## 5. BUG FIX: loading as an external plugin (.zip) -Durante i test su **Electrum 4.7.2 portable per Windows** sono emersi due -problemi reali nel caricare il plugin come **plugin esterno da .zip**: +During testing on **Electrum 4.7.2 portable for Windows**, two real problems +emerged when loading the plugin as an **external plugin from .zip**: ### Bug 5a — `ModuleNotFoundError: No module named 'electrum_external_plugins'` -- **Causa:** Electrum carica i plugin esterni da zip sotto il package sintetico - `electrum_external_plugins.bal`, ed esegue **solo** l'`__init__` del package e - il modulo `qt`. Non registra il package radice sintetico né i sotto-package - annidati (`gui`, `gui.qt`). Un semplice `from .gui.qt.plugin import Plugin` - fallisce risalendo ai parent mancanti. -- **Fix:** `qt.py` ora è uno shim resiliente che (1) rileva a runtime il proprio - nome di package (`__package__`), (2) ricostruisce in `sys.modules` gli - eventuali package padre mancanti, (3) importa `Plugin` con - `importlib.import_module`. Funziona **sia** come plugin interno - (`electrum.plugins.bal`) **sia** esterno (`electrum_external_plugins.bal`). +- **Cause:** Electrum loads external plugins from zip under the synthetic + package `electrum_external_plugins.bal`, and runs **only** the package + `__init__` and the `qt` module. It does not register the synthetic root + package nor the nested sub-packages (`gui`, `gui.qt`). A simple + `from .gui.qt.plugin import Plugin` fails when walking up to the missing + parents. +- **Fix:** `qt.py` is now a resilient shim that (1) detects its own package + name at runtime (`__package__`), (2) rebuilds in `sys.modules` any missing + parent packages, (3) imports `Plugin` with `importlib.import_module`. It + works **both** as an internal plugin (`electrum.plugins.bal`) **and** as an + external one (`electrum_external_plugins.bal`). ### Bug 5b — `zlib.error: Error -5 ... incomplete or truncated stream` -- **Causa:** alcune build portable di Electrum su Windows non riescono a - decomprimere con `zipimport` archivi che contengono **voci di directory** o - compressione non standard. -- **Fix:** aggiunto `build_zip.py`, che genera un archivio "zipimport-friendly": - solo file (nessuna voce di directory), DEFLATE standard, ordine deterministico - (SHA-256 riproducibile), escludendo `__pycache__`/`*.pyc`. Stampa anche - l'hash SHA-256 per verificare l'integrità del download. +- **Cause:** some Electrum portable builds on Windows fail to decompress with + `zipimport` archives that contain **directory entries** or non-standard + compression. +- **Fix:** added `build_zip.py`, which generates a "zipimport-friendly" + archive: files only (no directory entries), standard DEFLATE, deterministic + ordering (reproducible SHA-256), excluding `__pycache__`/`*.pyc`. It also + prints the SHA-256 hash to verify the integrity of the download. --- -## 6. Test aggiunti +## 6. Tests added -- `tests/smoke_test.py` — verifica import + comportamento di base - (`BalTimestamp`, helper di `Util`, costanti `HEIR_*`, stati di `WillItem`, - hook del `Plugin`). -- `tests/external_zip_test.py` — riproduce **fedelmente** la sequenza di - caricamento di un plugin esterno da zip di Electrum (regressione per il - Bug 5a/5b). +- `tests/smoke_test.py` — checks imports + basic behaviour + (`BalTimestamp`, `Util` helpers, `HEIR_*` constants, `WillItem` states, + `Plugin` hooks). +- `tests/external_zip_test.py` — **faithfully** reproduces Electrum's loading + sequence for an external plugin from zip (regression test for Bug 5a/5b). -Tutti i test passano sotto Electrum 4.7.2 + PyQt6. +All tests pass under Electrum 4.7.2 + PyQt6. --- -## 7. Riepilogo: cosa NON è cambiato +## 7. Summary: what did NOT change -- La logica di costruzione delle transazioni (`heirs.py`, `will.py`). -- I valori e i tipi di `json_db.register_dict(...)`. -- I codici colore degli stati (solo spostati in `theme.py`). -- L'algoritmo di tutte le classi GUI (copiate verbatim). -- Il formato dei dati salvati nel wallet. +- The transaction-building logic (`heirs.py`, `will.py`). +- The values and types of `json_db.register_dict(...)`. +- The status colour codes (only moved to `theme.py`). +- The algorithm of all the GUI classes (copied verbatim). +- The format of the data saved in the wallet. -## 8. Note / raccomandazioni +## 8. Notes / recommendations -- Il plugin **richiede Electrum 4.7.2**: `json_db.register_dict` è stato - **rimosso** nelle versioni successive (master), dove andrebbe sostituito con - `stored_dict.register_name`. Valutare un adeguamento se si vuole supportare - Electrum più recente. -- Prima del rilascio è consigliata una prova **end-to-end in una sessione - Electrum reale** (preferibilmente su testnet), oltre agli smoke test. +- The plugin **requires Electrum 4.7.2**: `json_db.register_dict` was + **removed** in later versions (master), where it should be replaced with + `stored_dict.register_name`. Consider an update if you want to support a + more recent Electrum. +- Before release, an **end-to-end test in a real Electrum session** is + recommended (preferably on testnet), in addition to the smoke tests. --- -## 9. CORREZIONI GUI — finestre e ciclo di vita (B1-B10) +## 9. GUI FIXES — windows and lifecycle (B1-B10) -Dopo il refactoring di struttura sono stati corretti **dieci difetti grafici e -di ciclo di vita** delle finestre, già presenti nel codice originale. La logica -di business è rimasta **byte-identica** (nessuna modifica a `bal/core/*`): sono -cambiati solo **presentazione, parent, modalità, z-order, ciclo di vita e -cleanup** delle finestre Qt. +After the structural refactoring, **ten graphical and lifecycle defects** of +the windows were fixed, all already present in the original code. The business +logic remained **byte-identical** (no changes to `bal/core/*`): only the +**presentation, parent, modality, z-order, lifecycle and cleanup** of the Qt +windows changed. -Sintomi segnalati dall'utente, ora risolti: -- **(S1)** le finestre del plugin sparivano dietro la finestra di Electrum; -- **(S2)** alcuni meccanismi funzionavano solo dopo aver chiuso e riavviato - Electrum. +Symptoms reported by the user, now resolved: +- **(S1)** the plugin windows disappeared behind the Electrum window; +- **(S2)** some mechanisms worked only after closing and restarting Electrum. -| ID | Problema (presente nell'originale) | Correzione applicata | +| ID | Problem (present in the original) | Applied fix | |-----|----------------------------------------------------------------------|----------------------| -| B1 | `self.parent = parent` sovrascriveva il metodo `parent()` di Qt, rompendo la gerarchia delle finestre | rinominato in `self._bal_parent` (in `dialogs.py`, `lists.py`, `widgets.py`); il parent reale passa da `top_level_of(parent)` | -| B2 | dialoghi aperti con `.show()` non modale → finivano sotto la finestra principale | sostituiti con `show_on_top()` / `show_modal()` e parent corretto | -| B3 | messaggio "Please restart Electrum to activate the BAL plugin": il plugin si attivava solo dopo riavvio | inizializzazione **a caldo** con `_setup_window()` che replica `load_wallet` — niente più riavvio | -| B4 | chiave del dizionario finestre usava il **metodo** `winId` invece del valore | chiave stabile `_window_key()` basata su `id(window)` | -| B5 | `on_close` ingoiava tutti gli errori con `except: pass` | riscritto: niente `except:pass`, log per ogni passo, reset pulito dello stato | -| B6 | `BalBlockingWaitingDialog` bloccava il thread della GUI (`processEvents` commentato) | ripristinato `processEvents()` → GUI reattiva durante l'attesa | -| B7 | `closeEvent`/`hideEvent` con cleanup del thread commentato | gestione esplicita di `closeEvent`/`hideEvent` + chiamata a `super()` | -| B8 | `closeEvent` incompleto in alcuni dialog | gestione uniforme dello stato di chiusura | -| B9 | `show()+raise_()` senza `activateWindow()` né modalità → finestra non in primo piano | `bring_to_front()` = `raise_()` + `activateWindow()` | -| B10 | gestione multi-wallet / multi-finestra fragile; menu cercato per titolo `&Tools` | uso dell'API ufficiale `window.tools_menu` | +| B1 | `self.parent = parent` overrode Qt's `parent()` method, breaking the window hierarchy | renamed to `self._bal_parent` (in `dialogs.py`, `lists.py`, `widgets.py`); the real parent comes from `top_level_of(parent)` | +| B2 | dialogs opened with non-modal `.show()` → ended up under the main window | replaced with `show_on_top()` / `show_modal()` and the correct parent | +| B3 | message "Please restart Electrum to activate the BAL plugin": the plugin activated only after a restart | **hot** initialization with `_setup_window()` that replicates `load_wallet` — no more restart | +| B4 | the windows-dictionary key used the `winId` **method** instead of its value | stable `_window_key()` key based on `id(window)` | +| B5 | `on_close` swallowed all errors with `except: pass` | rewritten: no `except:pass`, logging at every step, clean state reset | +| B6 | `BalBlockingWaitingDialog` blocked the GUI thread (`processEvents` commented out) | restored `processEvents()` → responsive GUI during the wait | +| B7 | `closeEvent`/`hideEvent` with the thread cleanup commented out | explicit handling of `closeEvent`/`hideEvent` + call to `super()` | +| B8 | incomplete `closeEvent` in some dialogs | uniform handling of the closing state | +| B9 | `show()+raise_()` without `activateWindow()` nor modality → window not in the foreground | `bring_to_front()` = `raise_()` + `activateWindow()` | +| B10 | fragile multi-wallet / multi-window handling; menu looked up by the `&Tools` title | use of the official `window.tools_menu` API | -### Nuovo modulo: `gui/qt/window_utils.py` (119 righe) +### New module: `gui/qt/window_utils.py` (119 lines) -Gli helper per la gestione delle finestre sono stati **centralizzati** in un -unico modulo, così la stessa logica non viene duplicata nei vari dialog: +The window-management helpers were **centralized** in a single module, so that +the same logic is not duplicated across the various dialogs: -- `top_level_of(widget)` — risale alla finestra di primo livello corretta da - usare come parent; -- `bring_to_front(window)` — `raise_()` + `activateWindow()` per portare in - primo piano; -- `stop_thread(thread)` — stop+wait sicuro di un `TaskThread`; -- `show_modal(dialog)` — apertura modale corretta (`exec()`); -- `show_on_top(window)` — apertura non modale ma sopra le altre finestre. +- `top_level_of(widget)` — walks up to the correct top-level window to use as + parent; +- `bring_to_front(window)` — `raise_()` + `activateWindow()` to bring to the + foreground; +- `stop_thread(thread)` — safe stop+wait of a `TaskThread`; +- `show_modal(dialog)` — correct modal opening (`exec()`); +- `show_on_top(window)` — non-modal opening but above the other windows. -`gui/qt/common.py` importa questi helper e li rende disponibili al resto della -GUI. +`gui/qt/common.py` imports these helpers and makes them available to the rest +of the GUI. --- -## 10. CORREZIONE BUG: download lista will-executor +## 10. BUG FIX: will-executor list download -Dopo l'installazione del pacchetto con le correzioni GUI, l'utente ha -segnalato che il comando **"download list"** dei will-executor non scaricava -più la lista. +After installing the package with the GUI fixes, the user reported that the +will-executor **"download list"** command no longer downloaded the list. -### Indagine +### Investigation -Il codice di rete (`core/willexecutors.py`: `send_request`, `handle_response`, -`download_list`, `initialize_willexecutor`) è stato confrontato riga per riga -con l'originale Gitea ed è risultato **byte-identico** (l'unica differenza è il -parametro aggiuntivo `welist_server` in `download_list`, retro-compatibile). +The network code (`core/willexecutors.py`: `send_request`, `handle_response`, +`download_list`, `initialize_willexecutor`) was compared line by line with the +original Gitea version and turned out to be **byte-identical** (the only +difference is the extra `welist_server` parameter in `download_list`, which is +backward-compatible). -Durante l'indagine sono comunque emersi e stati corretti **due difetti reali** -introdotti dalle correzioni GUI, che potevano "perdere" il risultato del -download: +During the investigation, **two real defects** introduced by the GUI fixes were +nevertheless found and corrected, which could "lose" the download result: -1. **`BalDialog.closeEvent`/`hideEvent` fermavano il `TaskThread`.** In Electrum - `TaskThread.on_done` esegue `cb_done` (cioè `self.accept`, che **chiude** il - dialog) **prima** di `cb_result` (cioè `on_success`, che **aggiorna** la - lista). Fermare il thread alla chiusura del dialog **scartava** quindi il - risultato appena scaricato. → I due metodi sono stati riportati a **non** - fermare il thread (con commento esplicativo nel codice). -2. **`BalWaitingDialog.exe()` usava una modalità sbagliata** (`show_modal` / - `WindowModal`). → Ripristinato l'originale `self.exec()`, aggiungendo prima - `bring_to_front(self)` per garantire il primo piano. +1. **`BalDialog.closeEvent`/`hideEvent` stopped the `TaskThread`.** In Electrum, + `TaskThread.on_done` runs `cb_done` (i.e. `self.accept`, which **closes** the + dialog) **before** `cb_result` (i.e. `on_success`, which **updates** the + list). Stopping the thread when the dialog closed therefore **discarded** the + result that had just been downloaded. → The two methods were restored so they + do **not** stop the thread (with an explanatory comment in the code). +2. **`BalWaitingDialog.exe()` used the wrong modality** (`show_modal` / + `WindowModal`). → Restored the original `self.exec()`, first adding + `bring_to_front(self)` to guarantee the foreground. -Inoltre i percorsi del **pulsante** e del **wizard** (che prima scaricavano in -modi diversi e con messaggi diversi) sono stati **unificati** in un unico -helper `fetch_will_executors_list`, eseguito dentro il worker del `TaskThread`. +In addition, the **button** and **wizard** paths (which previously downloaded +in different ways and with different messages) were **unified** into a single +helper `fetch_will_executors_list`, run inside the `TaskThread` worker. -### Causa vera del mancato download: ambientale, NON del plugin +### Real cause of the failed download: environmental, NOT the plugin -Una probe di controllo con `urllib` che **bypassava completamente Electrum** -falliva ugualmente con `WinError 10054` ("connection forcibly closed by remote -host"): segno che la **rete/ISP dell'utente resettava la connessione HTTPS** -verso `welist.bitcoin-after.life`. La conferma definitiva: **attivando una VPN -il download è andato a buon fine.** +A control probe with `urllib` that **completely bypassed Electrum** also failed +with `WinError 10054` ("connection forcibly closed by remote host"): a sign +that the **user's network/ISP was resetting the HTTPS connection** to +`welist.bitcoin-after.life`. The definitive confirmation: **with a VPN enabled +the download succeeded.** -L'originale "sembrava" funzionare perché spedisce comunque un will-executor di -**default già incorporato** (`https://we.bitcoin-after.life`), quindi la lista -non risultava mai del tutto vuota anche senza un download riuscito. +The original "seemed" to work because it ships anyway a **built-in default** +will-executor (`https://we.bitcoin-after.life`), so the list never appeared +completely empty even without a successful download. -### Pulizia finale (scelta dall'utente — "Opzione 1") +### Final cleanup (chosen by the user — "Option 1") -- **Finestra di attesa non bloccante** mantenuta (`BalWaitingDialog`), così la - GUI non si congela durante il download. -- **Fallback dell'URL**: prima l'URL configurato (`WELIST_SERVER`), poi quello +- **Non-blocking waiting window** kept (`BalWaitingDialog`), so the GUI does + not freeze during the download. +- **URL fallback**: first the configured URL (`WELIST_SERVER`), then the hardcoded `https://welist.bitcoin-after.life/`. -- **Diagnostica dettagliata spostata nei soli log** (rimossa la probe `urllib` - dall'interfaccia). -- **Messaggio d'errore semplice per l'utente, in inglese** (`DOWNLOAD_FAILED_MESSAGE`): +- **Detailed diagnostics moved to the logs only** (removed the `urllib` probe + from the interface). +- **Simple error message for the user, in English** (`DOWNLOAD_FAILED_MESSAGE`): > *"Could not download the will-executors list. This is usually caused by > your internet connection or a firewall, not by the plugin. Please check > your connection (a VPN often helps) and try again."* -### File toccati (solo presentazione/GUI, logica invariata) +### Files touched (presentation/GUI only, logic unchanged) -- `gui/qt/window.py` — helper condiviso `fetch_will_executors_list`, - `download_list` con `TaskThread` + `BalWaitingDialog`, costante +- `gui/qt/window.py` — shared helper `fetch_will_executors_list`, + `download_list` with `TaskThread` + `BalWaitingDialog`, constant `DOWNLOAD_FAILED_MESSAGE`. -- `gui/qt/lists.py` — `WillExecutorWidget.download_list` instradato sul - percorso condiviso con `on_success` che aggiorna/salva la lista. -- `gui/qt/dialogs.py` — `BalDialog.closeEvent`/`hideEvent` **non** fermano più - il thread; `BalWaitingDialog.exe()` torna a `self.exec()` + `bring_to_front`. -- `tests/gui_fixes_test.py` — asserzione di **regressione**: verifica che - `closeEvent`/`hideEvent` **non** contengano `stop_thread` (per non - reintrodurre il bug che scartava il download). +- `gui/qt/lists.py` — `WillExecutorWidget.download_list` routed onto the shared + path with an `on_success` that updates/saves the list. +- `gui/qt/dialogs.py` — `BalDialog.closeEvent`/`hideEvent` no longer stop the + thread; `BalWaitingDialog.exe()` goes back to `self.exec()` + `bring_to_front`. +- `tests/gui_fixes_test.py` — **regression** assertion: verifies that + `closeEvent`/`hideEvent` do **not** contain `stop_thread` (so as not to + reintroduce the bug that discarded the download). --- -## 11. Confronto strutturale finale (originale Gitea → refactor) +## 11. Final structural comparison (original Gitea → refactor) -Conteggio file `.py` (escluse cartelle generate): +`.py` file count (excluding generated folders): -| Originale (Gitea) | righe | → | Refactor (`bal/`) | righe | +| Original (Gitea) | lines | → | Refactor (`bal/`) | lines | |------------------------------|------:|----|-------------------------------------------|------:| | `__init__.py` | 1 | → | `__init__.py` | 37 | | `bal.py` | 161 | → | `core/plugin_base.py` | 351 | @@ -362,56 +360,56 @@ Conteggio file `.py` (escluse cartelle generate): | `heirs.py` | 792 | → | `core/heirs.py` | 806 | | `will.py` | 903 | → | `core/will.py` | 938 | | `willexecutors.py` | 547 | → | `core/willexecutors.py` | 390 | -| `qt.py` (monolite GUI) | 3777 | → | suddiviso in `gui/qt/*` (vedi sotto) | — | +| `qt.py` (GUI monolith) | 3777 | → | split into `gui/qt/*` (see below) | — | | `bal_resources.py` | 14 | → | `bal_resources.py` | 14 | -| `wallet_util/*.py` | 275 | → | `wallet_util/*.py` (invariati) | 280 | +| `wallet_util/*.py` | 275 | → | `wallet_util/*.py` (unchanged) | 280 | -Suddivisione del vecchio `qt.py` (3777 righe) nei moduli GUI: +Split of the old `qt.py` (3777 lines) into the GUI modules: -| Modulo refactor | righe | Contenuto | +| Refactor module | lines | Content | |-----------------------------|------:|-----------| -| `gui/qt/plugin.py` | 303 | classe `Plugin` (`@hook` Electrum → GUI) | -| `gui/qt/window.py` | 1048 | `BalWindow` (controller per-wallet) | -| `gui/qt/dialogs.py` | 1155 | finestre di dialogo + wizard | -| `gui/qt/lists.py` | 964 | viste ad albero (eredi/preview/executor) | -| `gui/qt/widgets.py` | 782 | widget "foglia" | -| `gui/qt/common.py` | 157 | import condivisi + helper | -| `gui/qt/window_utils.py` | 119 | helper finestre (NUOVO — vedi §9) | +| `gui/qt/plugin.py` | 303 | `Plugin` class (`@hook` Electrum → GUI) | +| `gui/qt/window.py` | 1048 | `BalWindow` (per-wallet controller) | +| `gui/qt/dialogs.py` | 1155 | dialog windows + wizard | +| `gui/qt/lists.py` | 964 | tree views (heirs/preview/executor) | +| `gui/qt/widgets.py` | 782 | "leaf" widgets | +| `gui/qt/common.py` | 157 | shared imports + helpers | +| `gui/qt/window_utils.py` | 119 | window helpers (NEW — see §9) | | `gui/qt/calendar.py` | 80 | `BalCalendar` | -| `gui/qt/theme.py` | 59 | mappatura stato → colore | -| `gui/qt/__init__.py` | 17 | init package GUI | +| `gui/qt/theme.py` | 59 | status → colour mapping | +| `gui/qt/__init__.py` | 17 | GUI package init | -> Le differenze nei conteggi di righe rispetto all'originale derivano da: -> riformattazione/commenti, separazione degli import per modulo, e spostamento -> di funzioni tra `util.py`/`bal.py` e i nuovi moduli. **Gli algoritmi non sono -> stati modificati.** +> The differences in the line counts compared to the original come from: +> reformatting/comments, separation of imports per module, and the movement +> of functions between `util.py`/`bal.py` and the new modules. **The algorithms +> were not modified.** --- -## 12. Cronologia delle modifiche su GitHub +## 12. Change history on GitHub -- **`4198a51`** — import iniziale del refactor strutturale (v0.2.8): separazione - `core/` (logica) vs `gui/qt/` (presentazione), packaging conforme, fix - caricamento zip esterno, smoke test (sezioni §1-§8). -- **`d56fa36`** — questo changelog del refactoring (in italiano). -- **`4806997`** — `DIAGNOSI_GUI.md`: diagnosi dei bug GUI di z-order e ciclo di - vita (Fase A). -- **`dd6f677`** (PR **#2**, squash) — correzioni GUI **B1-B10** + fix download - lista will-executor + `window_utils.py` + test di regressione (sezioni §9-§10). -- **PR #3** — fix **OverflowError su Windows (anno 2038)** che rompeva le schede - Will/Heirs e la voce di menu (sezione §13). +- **`4198a51`** — initial import of the structural refactor (v0.2.8): + separation of `core/` (logic) vs `gui/qt/` (presentation), compliant + packaging, external-zip load fix, smoke test (sections §1-§8). +- **`d56fa36`** — this refactoring changelog (in Italian). +- **`4806997`** — `GUI_DIAGNOSIS.md` (originally `DIAGNOSI_GUI.md`): diagnosis + of the z-order and lifecycle GUI bugs (Phase A). +- **`dd6f677`** (PR **#2**, squash) — GUI fixes **B1-B10** + will-executor list + download fix + `window_utils.py` + regression test (sections §9-§10). +- **PR #3** — fix for the **OverflowError on Windows (year 2038)** that broke + the Will/Heirs tabs and the menu entry (section §13). --- -## 13. CORREZIONE BUG: OverflowError su Windows (limite anno 2038) +## 13. BUG FIX: OverflowError on Windows (year-2038 limit) -### Sintomo (Windows 11) -Dopo aver **riavviato Electrum** o **cambiato wallet**, le schede **Will** e -**Heirs** sparivano e compariva una **voce di menu condensata/illeggibile** -(icona + testo sovrapposti) sotto il logo di Electrum, accanto a *Portafogli*. -Su Linux il problema non si manifestava. +### Symptom (Windows 11) +After **restarting Electrum** or **switching wallet**, the **Will** and +**Heirs** tabs disappeared and a **condensed/illegible menu entry** +(overlapping icon + text) appeared under the Electrum logo, next to *Wallets*. +On Linux the problem did not occur. -### Causa vera (dal log di Electrum dell'utente) +### Real cause (from the user's Electrum log) ``` OverflowError: Python int too large to convert to C int window.py __init__ -> create_heirs_tab -> WillSettingsWidget @@ -419,196 +417,192 @@ OverflowError: Python int too large to convert to C int -> datetime.fromtimestamp(NLOCKTIME_MAX) ``` -- `NLOCKTIME_MAX = 2**32 - 1 = 4294967295` viene usato come locktime di - **default/sentinella**. -- Su **Windows** `time_t` è a **32 bit**, quindi `datetime.fromtimestamp(ts)` - solleva **`OverflowError`** per qualsiasi timestamp oltre il **2038**. -- Su **Linux 64-bit** la stessa chiamata **funziona**: ecco perché il bug si - vedeva solo su Windows e i test su Linux non lo intercettavano. -- L'eccezione interrompeva `BalWindow.__init__` durante `init_menubar` / - `load_wallet`, lasciando le schede Will/Heirs e la voce di menu **a metà - costruzione** → l'elemento grafico condensato/illeggibile sotto il logo. +- `NLOCKTIME_MAX = 2**32 - 1 = 4294967295` is used as the + **default/sentinel** locktime. +- On **Windows** `time_t` is **32-bit**, so `datetime.fromtimestamp(ts)` + raises **`OverflowError`** for any timestamp beyond **2038**. +- On **64-bit Linux** the same call **works**: that is why the bug was visible + only on Windows and the Linux tests did not catch it. +- The exception interrupted `BalWindow.__init__` during `init_menubar` / + `load_wallet`, leaving the Will/Heirs tabs and the menu entry **half-built** + → the condensed/illegible graphical element under the logo. -> Nota: i due primi tentativi di correzione (status-bar no-op e idempotenza di -> `init_menubar_tools`) **non** centravano la causa; sono stati comunque -> mantenuti perché innocui e leggermente migliorativi, ma il vero colpevole era -> questo crash a monte. +> Note: the first two correction attempts (a no-op status bar and the +> idempotency of `init_menubar_tools`) did **not** hit the cause; they were +> kept anyway because they are harmless and slightly improving, but the real +> culprit was this upstream crash. -### Fix (comportamento invariato per tutti i valori normali) -- **`BalTimestamp._safe_fromtimestamp()`**: `datetime.fromtimestamp` con - **clamp a INT32_MAX** (anno 2038) in caso di `OverflowError`/`OSError`/ - `ValueError`, **esattamente** come la funzione `get_max_allowed_timestamp()` - dell'originale (workaround per Electrum issue **#6170**). -- Usato in `to_date` / `to_timestamp` / `__str__` / `__repr__` di +### Fix (behaviour unchanged for all normal values) +- **`BalTimestamp._safe_fromtimestamp()`**: `datetime.fromtimestamp` with + a **clamp to INT32_MAX** (year 2038) on `OverflowError`/`OSError`/ + `ValueError`, **exactly** like the original's `get_max_allowed_timestamp()` + function (workaround for Electrum issue **#6170**). +- Used in `to_date` / `to_timestamp` / `__str__` / `__repr__` of `BalTimestamp`. -- `gui/qt/widgets.py` (`set_value`): usa il converter sicuro. -- `core/util.py` (`timestamp_minus`): stessa protezione inline con clamp a +- `gui/qt/widgets.py` (`set_value`): uses the safe converter. +- `core/util.py` (`timestamp_minus`): same inline protection with a clamp to INT32_MAX. -I valori entro il 2038 (date assolute normali, durate relative come `90d`/`5y`) -producono **lo stesso identico risultato** di prima. +Values within 2038 (normal absolute dates, relative durations such as +`90d`/`5y`) produce **exactly the same result** as before. ### Test -- `tests/windows_overflow_test.py` riproduce il limite 32-bit di Windows - (monkeypatch di `datetime.fromtimestamp`) e dimostra che **senza** il fix si - ottiene lo **stesso** `OverflowError` del log, mentre **con** il fix passa. - Verificato anche che il test **fallisce** senza il fix. +- `tests/windows_overflow_test.py` reproduces the Windows 32-bit limit + (monkeypatch of `datetime.fromtimestamp`) and proves that **without** the fix + you get the **same** `OverflowError` as in the log, while **with** the fix it + passes. It was also verified that the test **fails** without the fix. -Confermato dall'utente: **"si ora funziona"**. +Confirmed by the user: **"yes, it works now"**. -## 14. NUOVA FUNZIONE: invalidazione automatica al posticipo dell'eredità +## 14. NEW FEATURE: automatic invalidation when postponing the inheritance -### Problema -Una transazione di eredità viene firmata con un **locktime fisso e immutabile** -e inviata ai will-executor, che sono economicamente incentivati a trasmetterla -(incassano le fee). Se l'utente, dopo aver firmato/inviato, **posticipa** la -data di consegna (es. di un anno), la **vecchia** transazione gia firmata resta -valida sui server dei will-executor. Poiche ha il locktime piu basso, un -will-executor potrebbe trasmetterla appena scade, eseguendo l'eredita **in -anticipo** rispetto alla nuova volonta dell'utente. La versione precedente -**non gestiva** questo caso: il posticipo non produceva alcuna azione. +### Problem +An inheritance transaction is signed with a **fixed, immutable locktime** and +sent to the will-executors, who are economically incentivized to broadcast it +(they collect the fees). If the user, after signing/sending, **postpones** the +delivery date (e.g. by one year), the **old** already-signed transaction +remains valid on the will-executors' servers. Since it has the lower locktime, +a will-executor could broadcast it as soon as it expires, executing the +inheritance **earlier** than the user's new intent. The previous version did +**not handle** this case: postponing produced no action at all. -### Soluzione (Strategia B — invalidazione esplicita on-chain) -Al posticipo di un'eredita **gia firmata e/o inviata** (stato `COMPLETE` o -`PUSHED`), il plugin chiede di **invalidare on-chain** i fondi prima di -ricostruire la nuova eredita. L'invalidazione spende gli stessi UTXO verso un -nuovo indirizzo di change con `locktime = altezza corrente` (RBF), quindi e -trasmettibile subito: una volta confermata, la vecchia transazione pre-firmata -diventa **definitivamente inutilizzabile**, vincendo la corsa contro qualunque -will-executor. +### Solution (Strategy B — explicit on-chain invalidation) +When postponing an inheritance that is **already signed and/or sent** (state +`COMPLETE` or `PUSHED`), the plugin asks to **invalidate the funds on-chain** +before rebuilding the new inheritance. The invalidation spends the same UTXOs +to a new change address with `locktime = current height` (RBF), so it is +broadcastable immediately: once confirmed, the old pre-signed transaction +becomes **permanently unusable**, winning the race against any will-executor. -### Dettagli tecnici +### Technical details - **`core/will.py`**: - - nuova eccezione `WillPostponedException` (sottoclasse di + - new exception `WillPostponedException` (subclass of `NotCompleteWillException`); - - `check_willexecutors_and_heirs`: il confronto del locktime non usa piu - l'entry dell'erede memorizzata (`their[2]`), che viene aggiornata in memoria - insieme al nuovo valore al momento del posticipo e quindi risulterebbe - sempre uguale. Ora confronta il locktime richiesto con **`w.tx.locktime`**, - cioe il locktime **congelato** nella transazione firmata (immutabile, e - quello che i will-executor possiedono). Tre casi: invariato → coerente; - nuovo > tx su will firmato/inviato → `WillPostponedException`; nuovo > tx su - will mai inviato → semplice ricostruzione (nessuna fee on-chain). -- **`gui/qt/dialogs.py`** (`BalBuildWillDialog.task_phase1`, il percorso reale - usato da **Tools → Prepare**): aggiunto il ramo `except WillPostponedException` - **prima** di `NotCompleteWillException`; si comporta come il caso "will - scaduto" e ritorna `(None, tx)` per innescare firma + broadcast - dell'invalidazione. L'utente preme di nuovo **Prepare** per ricostruire, - rifirmare e reinviare la nuova eredita (due passi espliciti, per maggior - controllo). -- **`gui/qt/window.py`** (`build_inheritance_transaction`): aggiunto lo stesso - ramo per completezza del percorso alternativo, con messaggio esplicativo. -- **`gui/qt/common.py`**: `WillPostponedException` esportato. + - `check_willexecutors_and_heirs`: the locktime comparison no longer uses the + stored heir entry (`their[2]`), which is updated in memory together with the + new value at the moment of postponement and would therefore always look + equal. It now compares the requested locktime with **`w.tx.locktime`**, i.e. + the locktime **frozen** in the signed transaction (immutable, and the one + the will-executors hold). Three cases: unchanged → coherent; new > tx on a + signed/sent will → `WillPostponedException`; new > tx on a will never sent + → simple rebuild (no on-chain fee). +- **`gui/qt/dialogs.py`** (`BalBuildWillDialog.task_phase1`, the real path used + by **Tools → Prepare**): added the `except WillPostponedException` branch + **before** `NotCompleteWillException`; it behaves like the "expired will" + case and returns `(None, tx)` to trigger signing + broadcasting of the + invalidation. The user presses **Prepare** again to rebuild, re-sign and + re-send the new inheritance (two explicit steps, for greater control). +- **`gui/qt/window.py`** (`build_inheritance_transaction`): added the same + branch for completeness of the alternative path, with an explanatory message. +- **`gui/qt/common.py`**: `WillPostponedException` exported. -### NUOVA COLONNA "Server" nella lista transazioni -Per dare all'utente visibilita costante sullo stato online delle proprie -transazioni di eredita, e stata aggiunta una colonna dedicata **"Server"** in -`PreviewList` (`gui/qt/lists.py`), con etichetta sempre leggibile -(`Confirmed on server`, `Sent (not checked)`, `Send failed`, `Not on server`, -`Signed (not sent)`, `Not sent`) e **tooltip** con URL del will-executor e -stato. Le funzioni `server_status_text()` e `server_status_tooltip()` sono in -`gui/qt/theme.py` e riusano gli stessi flag di stato gia esistenti. +### NEW "Server" COLUMN in the transactions list +To give the user constant visibility on the online status of their inheritance +transactions, a dedicated **"Server"** column was added in `PreviewList` +(`gui/qt/lists.py`), with an always-readable label (`Confirmed on server`, +`Sent (not checked)`, `Send failed`, `Not on server`, `Signed (not sent)`, +`Not sent`) and a **tooltip** with the will-executor URL and status. The +functions `server_status_text()` and `server_status_tooltip()` are in +`gui/qt/theme.py` and reuse the same already-existing status flags. ### Test -- I 182 test ufficiali continuano a passare; smoke test ed external-zip test - OK; `ruff` senza nuove segnalazioni reali. -- Verificato sui dati reali del log dell'utente: il posticipo di un'eredita - firmata ora rileva correttamente la condizione e avvia l'invalidazione. +- The 182 official tests keep passing; smoke test and external-zip test OK; + `ruff` with no new real warnings. +- Verified against the real data from the user's log: postponing a signed + inheritance now correctly detects the condition and starts the invalidation. -Confermato dall'utente: **"mi pare che funziona"**. +Confirmed by the user: **"it seems to work"**. -## 15. TENTATIVO E REVERT: fix doppia invalidazione al posticipo (v0.3.1 -> v0.3.2) +## 15. ATTEMPT AND REVERT: fix for double invalidation on postpone (v0.3.1 -> v0.3.2) -### v0.3.1 (RITIRATA) -Per risolvere la doppia firma dell'invalidazione al posticipo, era stato -introdotto `Will.mark_invalidated_by_tx()`, chiamato in -`loop_broadcast_invalidating` dopo il broadcast dell'invalidazione, per marcare -`INVALIDATED` le will che spendevano gli stessi UTXO della tx di invalidazione -e persistere lo stato con `save_willitems`. +### v0.3.1 (WITHDRAWN) +To solve the double signing of the invalidation on postpone, +`Will.mark_invalidated_by_tx()` had been introduced, called in +`loop_broadcast_invalidating` after broadcasting the invalidation, to mark as +`INVALIDATED` the wills that spent the same UTXOs as the invalidation tx and to +persist the state with `save_willitems`. -### Perche e stata ritirata -La modifica ha introdotto una regressione grave segnalata dall'utente: -**la lista eredita mostrava ancora le vecchie eredita e l'aggiornamento di -eredi/date risultava incoerente**. +### Why it was withdrawn +The change introduced a serious regression reported by the user: +**the inheritance list still showed the old inheritances and the update of +heirs/dates was inconsistent**. -Causa: `loop_broadcast_invalidating` e il punto di broadcast usato per **TUTTI** -i tipi di invalidazione (posticipo, CheckAlive, will scaduto/anticipato), non -solo per il posticipo. Inoltre il metodo marcava e **persisteva** lo stato -`INVALIDATED` su tutte le will item che condividevano gli UTXO del wallet -(tipicamente tutte). Queste will item invalidate restavano poi in memoria e su -disco, inquinando la ricostruzione di eredi/date e lasciando vecchie voci nella -lista. +Cause: `loop_broadcast_invalidating` is the broadcast point used for **ALL** +types of invalidation (postpone, CheckAlive, expired/anticipated will), not +only for postpone. In addition, the method marked and **persisted** the +`INVALIDATED` state on all the will items that shared the wallet's UTXOs +(typically all of them). These invalidated will items then stayed in memory and +on disk, polluting the rebuild of heirs/dates and leaving old entries in the +list. -### v0.3.2 (questa versione): REVERT completo -- Rimosso `Will.mark_invalidated_by_tx()` da `core/will.py`. -- Rimossa la chiamata in `gui/qt/dialogs.py` (`loop_broadcast_invalidating`): - il metodo torna **identico** alla v0.3.0. -- Rimossi i due test relativi; mantenuto solo l'assert di gerarchia su - `WillPostponedException` (corretto e indipendente). -- `core/will.py` e `gui/qt/dialogs.py` sono ora **byte-identici** alla v0.3.0 - funzionante (verificato con `git diff a394cde`). +### v0.3.2 (this version): full REVERT +- Removed `Will.mark_invalidated_by_tx()` from `core/will.py`. +- Removed the call in `gui/qt/dialogs.py` (`loop_broadcast_invalidating`): the + method goes back to being **identical** to v0.3.0. +- Removed the two related tests; kept only the hierarchy assertion on + `WillPostponedException` (correct and independent). +- `core/will.py` and `gui/qt/dialogs.py` are now **byte-identical** to the + working v0.3.0 (verified with `git diff a394cde`). -Il bug della doppia invalidazione al posticipo resta quindi **aperto** e andra -riaffrontato in modo piu mirato (senza toccare il percorso di broadcast comune e -senza persistere stati su will che condividono gli UTXO), previa conferma -dell'utente. La priorita era ripristinare il comportamento corretto di -lista/eredi/date. +The double-invalidation-on-postpone bug therefore remains **open** and will +have to be tackled in a more targeted way (without touching the common +broadcast path and without persisting states on wills that share the UTXOs), +subject to the user's confirmation. The priority was to restore the correct +behaviour of the list/heirs/dates. -## 16. Aggiornamenti mancati, Check/Close coerenti, e rifinitura UI (v0.3.2) +## 16. Missed updates, consistent Check/Close, and UI polish (v0.3.2) -### FIX 1 - Rimozione di un erede rilevata su Check / chiusura Electrum -`core/will.py` (`check_willexecutors_and_heirs`): prima il plugin -rilevava solo l'**aggiunta** di un erede (raise `HeirNotFoundException` quando un -erede corrente non era piu nella will). Mancava il caso inverso: la -**rimozione** di un erede. Aggiunto il ramo `else` che lancia -`HeirNotFoundException` anche quando la will porta ancora un erede che non e piu -presente nel set di eredi corrente. Cosi la ricostruzione dell'eredita scatta su -**Check** e alla **chiusura di Electrum** (entrambi usano lo stesso percorso -`BalBuildWillDialog.build_will_task()`), come deciso dall'utente: nessun -aggiornamento automatico dopo la modifica, solo manuale con Check / alla -chiusura. +### FIX 1 - Removal of an heir detected on Check / Electrum close +`core/will.py` (`check_willexecutors_and_heirs`): previously the plugin +detected only the **addition** of an heir (raising `HeirNotFoundException` when +a current heir was no longer in the will). The opposite case was missing: the +**removal** of an heir. Added the `else` branch that raises +`HeirNotFoundException` also when the will still carries an heir that is no +longer present in the current heir set. This way the inheritance rebuild is +triggered on **Check** and on **Electrum close** (both use the same +`BalBuildWillDialog.build_will_task()` path), as decided by the user: no +automatic update after the change, only a manual one via Check / on close. -### FIX 2 - Check interroga i server anche per le will gia inviate -`core/will.py` (nuovo `Will.needs_server_check(w)`) e -`gui/qt/lists.py` (`PreviewList.check`): prima il Check interrogava i server solo -per le will in stato `PUSHED`. Le will gia inviate ma rimaste su "New / Not sent" -non venivano ricontrollate ("nothing to do"). Ora `needs_server_check` include -ogni will **VALID** con un will-executor e **non ancora CHECKED**, anche se non -in stato `PUSHED`. Stesso controllo usato sia dal pulsante Check sia da +### FIX 2 - Check also queries the servers for already-sent wills +`core/will.py` (new `Will.needs_server_check(w)`) and `gui/qt/lists.py` +(`PreviewList.check`): previously Check queried the servers only for wills in +the `PUSHED` state. Wills already sent but left at "New / Not sent" were not +re-checked ("nothing to do"). Now `needs_server_check` includes every **VALID** +will with a will-executor that is **not yet CHECKED**, even if not in the +`PUSHED` state. The same check is used both by the Check button and by `on_close`. -### FIX 3 - Hide invalidated/replaced da finestra Impostazioni aggiornava la lista -`core/plugin_base.py` (nuovo `sync_hide_filters()`) e -`gui/qt/window.py` (`update_all`): le checkbox "Hide Replaced" / "Hide -Invalidated" nella finestra Impostazioni scrivono direttamente la config -(`BalConfig.set`) senza toccare i flag in cache `_hide_invalidated` / -`_hide_replaced` usati dalla lista per filtrare. Risultato: la lista continuava -a filtrare col valore vecchio finche non si riavviava Electrum. Ora -`update_all()` chiama `sync_hide_filters()` che ri-legge i flag dalla config, -quindi qualunque sorgente del cambiamento (toolbar o finestra Impostazioni) -aggiorna subito la lista. +### FIX 3 - Hide invalidated/replaced from the Settings window updated the list +`core/plugin_base.py` (new `sync_hide_filters()`) and `gui/qt/window.py` +(`update_all`): the "Hide Replaced" / "Hide Invalidated" checkboxes in the +Settings window write the config directly (`BalConfig.set`) without touching the +cached flags `_hide_invalidated` / `_hide_replaced` that the list uses to +filter. Result: the list kept filtering with the old value until Electrum was +restarted. Now `update_all()` calls `sync_hide_filters()`, which re-reads the +flags from the config, so whatever the source of the change (toolbar or +Settings window) the list updates immediately. -### Rifinitura UI - Risultati in grassetto nel dialog "Building Will" -`gui/qt/dialogs.py` (`BalBuildWillDialog`): i **risultati** mostrati a destra di -ogni riga di stato (es. `Ok`, `Ko`, `Nothing to do`, `Skipped`, `Wait`, -`Timeout`) sono ora resi in **grassetto**, mantenendo i loro colori -(verde/rosso/giallo). Le etichette di stato a sinistra restano in peso normale. -Modifica centralizzata negli helper `msg_ok`, `msg_error`, `msg_warning`, -`msg_set_status`, piu le righe dei will-executor (push e check) che ora mostrano -`Ok/Ko` e `True/False` in grassetto + colore (verde/rosso). +### UI polish - Bold results in the "Building Will" dialog +`gui/qt/dialogs.py` (`BalBuildWillDialog`): the **results** shown to the right +of each status line (e.g. `Ok`, `Ko`, `Nothing to do`, `Skipped`, `Wait`, +`Timeout`) are now rendered in **bold**, keeping their colours +(green/red/yellow). The status labels on the left stay in normal weight. The +change is centralized in the helpers `msg_ok`, `msg_error`, `msg_warning`, +`msg_set_status`, plus the will-executor lines (push and check) that now show +`Ok/Ko` and `True/False` in bold + colour (green/red). ### Test -- 186 test ufficiali passano; smoke test, external-zip test e simulazione dei - flussi di aggiornamento (`tests/sim_update_flows.py`) OK; `ruff` senza nuove - segnalazioni reali (solo falsi positivi pre-esistenti da star-import). -- Aggiunti test in `tests/test_core_will.py`: +- 186 official tests pass; smoke test, external-zip test and the update-flow + simulation (`tests/sim_update_flows.py`) OK; `ruff` with no new real warnings + (only pre-existing false positives from star-imports). +- Added tests in `tests/test_core_will.py`: `test_check_heirs_unchanged_is_coherent`, `test_check_heir_removed_triggers_rebuild`, `test_check_heir_added_triggers_rebuild`, `test_needs_server_check`. -Confermato dall'utente sui dati reali: dopo Sign -> Broadcast -> Check le -transazioni gia inviate sono tornate verdi ("confirmed on server"); la lista -torna pulita; il grassetto e l'aggiornamento delle hide-flag funzionano. +Confirmed by the user against real data: after Sign -> Broadcast -> Check the +already-sent transactions turned green again ("confirmed on server"); the list +goes back clean; the bold rendering and the hide-flag update work. ## 17. UI polish and bug fix — signed-tx colour, wizard, Building Will dialog (v0.3.3) diff --git a/DIAGNOSI_GUI.md b/DIAGNOSI_GUI.md deleted file mode 100644 index 440f17a..0000000 --- a/DIAGNOSI_GUI.md +++ /dev/null @@ -1,319 +0,0 @@ -# BAL — Diagnosi dei problemi GUI (Fase A) → ✅ RISOLTI (Fase B) - -> **STATO: tutti i bug B1-B10 sono stati CORRETTI** e mergiati in `main` -> (PR #2, squash `dd6f677`). La logica di business resta **byte-identica** -> (nessuna modifica a `bal/core/*`): sono cambiati solo presentazione, parent, -> modalità, ciclo di vita e cleanup delle finestre. -> -> | ID | Stato | Fix applicato | -> |----|-------|---------------| -> | B1 | ✅ FIXED | `self.parent` → `self._bal_parent` (dialogs/lists/widgets); parent = `top_level_of(parent)` | -> | B2 | ✅ FIXED | `.show()` → `show_on_top()` / `show_modal()` con parent corretto | -> | B3 | ✅ FIXED | init a caldo: `_setup_window()` replica `load_wallet`, niente "restart Electrum" | -> | B4 | ✅ FIXED | chiave finestra stabile `_window_key()` = `id(window)` | -> | B5 | ✅ FIXED | `on_close` riscritto: niente `except:pass`, log per-step, reset stato | -> | B6 | ✅ FIXED | `BalBlockingWaitingDialog`: `processEvents()` ripristinato | -> | B7 | ✅ FIXED | `closeEvent/hideEvent`: `stop_thread()` + `super()` | -> | B8 | ✅ FIXED | `closeEvent`: `stop_thread()` (stop+wait) + `super()` | -> | B9 | ✅ FIXED | `bring_to_front()` = `raise_()` + `activateWindow()` | -> | B10| ✅ FIXED | uso di `window.tools_menu` (API ufficiale), niente ricerca per titolo `&Tools` | -> -> Helper centralizzati in `bal/gui/qt/window_utils.py`: -> `top_level_of`, `bring_to_front`, `stop_thread`, `show_modal`, `show_on_top`. -> Test di regressione: `tests/gui_fixes_test.py` (oltre a smoke + external_zip). - ---- - -## (Storico) Diagnosi originale - -Documento di sola **diagnosi**: nessuna riga di codice funzionale era stata -modificata in Fase A. Elenca i problemi grafici/di ciclo di vita riscontrati nel -codice, la loro **causa tecnica** e il **fix proposto**, con riferimenti riga. - -I due sintomi che hai segnalato: -- **(S1)** Le finestre del plugin spariscono dietro la finestra di Electrum. -- **(S2)** Alcuni meccanismi funzionano solo dopo aver chiuso e "ripulito" - Electrum. - -Sono entrambi spiegati dai bug qui sotto. - ---- - -## Riepilogo (tabella) - -| ID | Gravità | Sintomo | File:riga | Causa breve | -|----|---------|---------|-----------|-------------| -| B1 | 🔴 Alta | S1 | `dialogs.py:40,69,475` | `self.parent = parent` sovrascrive il metodo `QWidget.parent()` | -| B2 | 🔴 Alta | S1 | `window.py:148,936`, `window.py:566` | dialoghi aperti con `.show()` (non-modali, senza stare in primo piano) | -| B3 | 🔴 Alta | S2 | `plugin.py:38-42` | messaggio "Please restart Electrum" = init a caldo non gestito | -| B4 | 🔴 Alta | S2 | `plugin.py:45,111` | chiave dizionario `winId` (metodo) invece di `winId()` (valore) | -| B5 | 🟠 Media | S2 | `window.py:664-677` | `on_close` con `except: pass` che nasconde errori di cleanup | -| B6 | 🟠 Media | S1/S2 | `dialogs.py:445-462` | `BalBlockingWaitingDialog` blocca il thread GUI, `processEvents` commentato | -| B7 | 🟠 Media | S2 | `dialogs.py:48-58` | `closeEvent/hideEvent` con cleanup thread commentato | -| B8 | 🟠 Media | S2 | `dialogs.py:828-830` | `closeEvent` chiama `thread.stop()` ma non `thread.wait()` né `super()` | -| B9 | 🟡 Bassa | S1 | `dialogs.py:1121-1122` | `show()+raise_()` senza `activateWindow()` né modalità | -| B10| 🟡 Bassa | — | `plugin.py:36` (init), vari | gestione finestre multiple/ multi-wallet fragile | - ---- - -## Dettaglio dei problemi - -### B1 — `self.parent = parent` rompe il sistema di finestre di Qt 🔴 -**Dove:** `dialogs.py:40` (in `BalDialog.__init__`), ripetuto a `:69` e `:475`; -analoghi in altri dialoghi. - -```python -self.parent = parent # <-- PROBLEMA -super().__init__(parent) -``` - -**Causa:** in Qt, `parent()` è un **metodo** di `QWidget` che restituisce il -widget genitore. Assegnando un **attributo** `self.parent`, lo si maschera: da -quel punto `self.parent` non è più il metodo ma il valore salvato. Qualunque -codice (anche interno a Qt o di Electrum) che si aspetta `widget.parent()` come -metodo può comportarsi in modo imprevisto. Inoltre il `parent` passato non è -sempre la **top-level window** corretta, quindi il dialogo non viene agganciato -gerarchicamente alla finestra di Electrum e finisce **dietro** (S1). - -**Fix proposto:** -- Non sovrascrivere `parent`: rinominare l'attributo (es. `self._bal_parent`). -- Passare sempre come `parent` la **top-level window** di Electrum - (`window.top_level_window()`), così il dialogo resta in primo piano rispetto - ad essa. - ---- - -### B2 — Dialoghi aperti con `.show()` invece che modali 🔴 -**Dove:** -- `window.py:148` `show_willexecutor_dialog` → `self.willexecutor_dialog.show()` -- `window.py:936` `preview_modal_dialog` → `self.dw.show()` (il nome dice - "modal" ma usa `show()`!) -- `window.py:566` `show_transaction_real` → `d.show()` - -**Causa:** `show()` apre una finestra **non-modale e indipendente**: se il -`parent` non è impostato correttamente (vedi B1), la finestra non resta sopra -Electrum e ci "sparisce dietro" (S1). Si nota l'incoerenza: altrove si usa -correttamente `.exec()` (es. `init_wizard` a `window.py:144`, `settings_dialog` -a `plugin.py:254`), che è modale e resta in primo piano. - -**Fix proposto:** -- Per i dialoghi che devono restare in primo piano: usare `exec()` (modale) **o** - `show()` + parent corretto + `setWindowModality(Qt.WindowModal)` + - `raise_()` + `activateWindow()`. -- Mantenere la stessa logica di "cosa fa il dialogo" (nessun cambio di - comportamento funzionale, solo z-order/modalità). - ---- - -### B3 — "Please restart Electrum to activate the BAL plugin" 🔴 -**Dove:** `plugin.py:38-42` (hook `init_qt`). - -```python -if wallet: - window.show_warning(_("Please restart Electrum to activate the BAL plugin"), ...) - return -``` - -**Causa:** quando il plugin viene **abilitato a caldo** (wallet già aperto), -l'hook `init_qt` si arrende e chiede il riavvio invece di inizializzare le tab -e i menu sul wallet già caricato. È **la causa diretta del sintomo S2**: "devi -chiudere/riavviare Electrum perché funzioni". - -**Fix proposto:** -- In `init_qt`, se c'è già un wallet aperto, eseguire la stessa inizializzazione - che normalmente avviene in `load_wallet` (creare `BalWindow`, tab, menu, - caricare il will) **senza** richiedere il riavvio. -- Simmetricamente, gestire bene `close_wallet` per smontare tab/menu, così - ri-abilitare/ricaricare non lascia stato sporco. - ---- - -### B4 — Chiave del dizionario `winId` (metodo) invece di `winId()` 🔴 -**Dove:** `plugin.py:45` (scrittura) e `plugin.py:111` (lettura). - -```python -self.bal_windows[top_level_window.winId] = w # scrive con la *funzione* winId -... -w = self.bal_windows.get(window.winId, None) # legge con la *funzione* winId -``` - -**Causa:** `winId` senza parentesi è il **metodo legato** (bound method), non -l'identificatore della finestra. Usato come chiave "funziona per caso" perché -lo stesso oggetto-finestra produce lo stesso bound method; ma è fragile e -semanticamente errato: con più finestre/wallet o dopo riaperture la -corrispondenza può saltare, creando `BalWindow` duplicati o non trovando quello -giusto → stato incoerente (contribuisce a S2). - -**Fix proposto:** -- Usare una chiave stabile e corretta, es. `int(window.winId())` oppure - `id(window)`, in modo **coerente** sia in scrittura sia in lettura. - ---- - -### B5 — `on_close` ingoia tutti gli errori 🟠 -**Dove:** `window.py:664-677`. - -```python -def on_close(self): - try: - if not self.disable_plugin: - close_window = BalBuildWillDialog(self) - close_window.build_will_task() - self.save_willitems() - self.heirs_tab.close() - ... - except Exception: - pass # <-- nasconde qualsiasi errore di cleanup -``` - -**Causa:** se una qualsiasi di queste operazioni fallisce, l'eccezione viene -silenziata: tab/menu non vengono rimossi, lo stato (`willitems`, `heirs`, tab) -resta in memoria e "sporco" finché non si riavvia Electrum (S2). - -**Fix proposto:** -- Non silenziare: loggare l'errore con `_logger`. -- Rendere il cleanup **robusto e idempotente** (ogni passo in un try/except - separato con log), così un fallimento parziale non blocca gli altri passi. -- Azzerare esplicitamente lo stato (`willitems={}`, riferimenti a tab/menu a - `None`) a fine `on_close`. - ---- - -### B6 — `BalBlockingWaitingDialog` blocca il thread della GUI 🟠 -**Dove:** `dialogs.py:445-462`. - -```python -self.show() -# QCoreApplication.processEvents() # <-- commentato -# QCoreApplication.processEvents() -try: - task() # esegue il task SUL thread GUI -> finestra "congelata" -finally: - self.accept() -``` - -**Causa:** dopo `show()` non si dà alla GUI il tempo di disegnarsi -(`processEvents` è commentato) e poi si esegue `task()` **bloccando** il thread -dell'interfaccia. Risultato: la finestra "Please wait" può apparire vuota, -non ridisegnarsi, e l'app sembra bloccata (contribuisce a S1/percezione di -freeze). - -**Fix proposto:** -- O eseguire il task in un `TaskThread` (come fa già `BalWaitingDialog`), -- oppure, se deve restare bloccante, ripristinare un `processEvents()` dopo - `show()` per far disegnare la finestra prima del task. - ---- - -### B7 — `closeEvent`/`hideEvent` con cleanup thread commentato 🟠 -**Dove:** `dialogs.py:48-58` (`BalDialog`). - -```python -def closeEvent(self, event): - self._stopping = True - #if self.thread: - # self.thread.stop() # <-- disattivato - super().closeEvent(event) -``` - -**Causa:** alla chiusura del dialogo i thread eventualmente attivi **non** -vengono fermati. Restano in esecuzione in background, possono scrivere su widget -già distrutti o tenere risorse/connessioni → comportamenti erratici finché non -si riavvia (S2). - -**Fix proposto:** -- Ripristinare in modo sicuro lo stop dei thread: `if self.thread: - self.thread.stop(); self.thread.wait()` con guardia su `None`. - ---- - -### B8 — `BalBuildWillDialog.closeEvent` incompleto 🟠 -**Dove:** `dialogs.py:828-830`. - -```python -def closeEvent(self, event): - self._stopping = True - self.thread.stop() - # manca self.thread.wait() e manca super().closeEvent(event) -``` - -**Causa:** `stop()` segnala lo stop ma non attende la fine del thread -(`wait()`), e non viene chiamato `super().closeEvent(event)`: l'evento di -chiusura non è propagato correttamente. Possibili thread orfani e finestre che -non si chiudono pulite. - -**Fix proposto:** -- `self.thread.stop(); self.thread.wait(); super().closeEvent(event)` con - guardia su `self.thread is None`. - ---- - -### B9 — `show()+raise_()` senza `activateWindow()`/modalità 🟡 -**Dove:** `dialogs.py:1121-1122` (es. `WillExecutorDialog`/dettaglio). - -```python -self.show() -self.raise_() -# manca self.activateWindow(); nessuna modalità impostata -``` - -**Causa:** `raise_()` alza la finestra nello stack ma su alcuni window manager -(incluso Windows) senza `activateWindow()` non riceve il focus e può comunque -finire dietro. Senza modalità, l'utente può tornare alla finestra principale -lasciando il dialogo nascosto. - -**Fix proposto:** -- Aggiungere `self.activateWindow()` dopo `raise_()`, e valutare - `setWindowModality(Qt.WindowModal)` dove ha senso. - ---- - -### B10 — Gestione finestre multiple / multi-wallet fragile 🟡 -**Dove:** `plugin.py:30-62` (`init_qt`), `get_window` (`plugin.py:109-115`). - -**Causa:** la mappa `bal_windows` e l'aggancio ai menu si basano su assunzioni -(B4) e sull'iterazione dei figli del menubar per nome (`"&Tools"`), che è -sensibile alla **localizzazione** (tu usi `Locale: Italian_Italy`!). Se il menu -non si chiama esattamente `&Tools` nella lingua corrente, l'aggancio può -fallire silenziosamente. - -**Fix proposto:** -- Usare l'API ufficiale `window.tools_menu` (già usata in `init_menubar`, - `plugin.py:79`) invece di cercare il menu per titolo tradotto. -- Unificare la creazione/lookup di `BalWindow` su una chiave stabile (B4). - ---- - -## Strategia di correzione proposta (per la Fase B/C) - -Per **non cambiare la logica di funzionamento** e ridurre i rischi, propongo di -introdurre un **unico punto centralizzato** di gestione finestre (un piccolo -helper, es. `gui/qt/window_utils.py`) con funzioni tipo: - -- `show_modal(dialog)` → imposta parent corretto, modalità, `exec()`. -- `show_on_top(dialog)` → `show()` + `raise_()` + `activateWindow()` per i - pochi casi che devono restare non-modali. - -E poi sostituire i `.show()`/`.exec()` sparsi con queste funzioni. Vantaggi: -- la **logica di business resta intatta** (cosa fa il dialogo non cambia); -- si tocca **solo** il "come" viene mostrato/chiuso; -- più facile da testare e da revisionare (diff piccolo e localizzato). - -### Ordine consigliato -1. **B3 + B4** (init a caldo + chiave finestre): risolvono la radice di S2. -2. **B1 + B2 + B9** (parent/modalità/z-order): risolvono S1. -3. **B5 + B7 + B8** (cleanup robusto + thread): chiudono i residui di S2. -4. **B6 + B10** (waiting dialog + menu localizzati): rifiniture. - ---- - -## Cosa serve da te per la Fase B/C -- Conferma che posso modificare il **comportamento della GUI** (parent, - modalità, cleanup, init a caldo) mantenendo invariata la logica di business. -- Test su **Electrum portable Windows** dopo ogni gruppo di fix, con descrizione - /screenshot di cosa succede (apertura dialoghi, abilitazione a caldo, - chiusura wallet). - -> Nota: i bug B1–B10 esistono **identici nell'originale** — questo refactor li -> ha preservati fedelmente (era l'obiettivo della fase precedente). La Fase B/C -> li corregge. diff --git a/GUI_DIAGNOSIS.md b/GUI_DIAGNOSIS.md new file mode 100644 index 0000000..6542435 --- /dev/null +++ b/GUI_DIAGNOSIS.md @@ -0,0 +1,320 @@ +# BAL — Diagnosis of the GUI problems (Phase A) → ✅ RESOLVED (Phase B) + +> **STATUS: all bugs B1-B10 have been FIXED** and merged into `main` +> (PR #2, squash `dd6f677`). The business logic remains **byte-identical** +> (no changes to `bal/core/*`): only the presentation, parent, modality, +> lifecycle and cleanup of the windows changed. +> +> | ID | Status | Applied fix | +> |----|--------|-------------| +> | B1 | ✅ FIXED | `self.parent` → `self._bal_parent` (dialogs/lists/widgets); parent = `top_level_of(parent)` | +> | B2 | ✅ FIXED | `.show()` → `show_on_top()` / `show_modal()` with the correct parent | +> | B3 | ✅ FIXED | hot init: `_setup_window()` replicates `load_wallet`, no more "restart Electrum" | +> | B4 | ✅ FIXED | stable window key `_window_key()` = `id(window)` | +> | B5 | ✅ FIXED | `on_close` rewritten: no `except:pass`, per-step logging, state reset | +> | B6 | ✅ FIXED | `BalBlockingWaitingDialog`: `processEvents()` restored | +> | B7 | ✅ FIXED | `closeEvent/hideEvent`: `stop_thread()` + `super()` | +> | B8 | ✅ FIXED | `closeEvent`: `stop_thread()` (stop+wait) + `super()` | +> | B9 | ✅ FIXED | `bring_to_front()` = `raise_()` + `activateWindow()` | +> | B10| ✅ FIXED | use of `window.tools_menu` (official API), no lookup by the `&Tools` title | +> +> Helpers centralized in `bal/gui/qt/window_utils.py`: +> `top_level_of`, `bring_to_front`, `stop_thread`, `show_modal`, `show_on_top`. +> Regression test: `tests/gui_fixes_test.py` (in addition to smoke + external_zip). + +--- + +## (Historical) Original diagnosis + +A **diagnosis-only** document: no line of functional code had been modified in +Phase A. It lists the graphical/lifecycle problems found in the code, their +**technical cause** and the **proposed fix**, with line references. + +The two symptoms you reported: +- **(S1)** The plugin windows disappear behind the Electrum window. +- **(S2)** Some mechanisms work only after closing and "cleaning up" + Electrum. + +Both are explained by the bugs below. + +--- + +## Summary (table) + +| ID | Severity | Symptom | File:line | Short cause | +|----|----------|---------|-----------|-------------| +| B1 | 🔴 High | S1 | `dialogs.py:40,69,475` | `self.parent = parent` overrides the `QWidget.parent()` method | +| B2 | 🔴 High | S1 | `window.py:148,936`, `window.py:566` | dialogs opened with `.show()` (non-modal, not staying in the foreground) | +| B3 | 🔴 High | S2 | `plugin.py:38-42` | "Please restart Electrum" message = unhandled hot init | +| B4 | 🔴 High | S2 | `plugin.py:45,111` | dictionary key `winId` (method) instead of `winId()` (value) | +| B5 | 🟠 Medium | S2 | `window.py:664-677` | `on_close` with `except: pass` that hides cleanup errors | +| B6 | 🟠 Medium | S1/S2 | `dialogs.py:445-462` | `BalBlockingWaitingDialog` blocks the GUI thread, `processEvents` commented out | +| B7 | 🟠 Medium | S2 | `dialogs.py:48-58` | `closeEvent/hideEvent` with the thread cleanup commented out | +| B8 | 🟠 Medium | S2 | `dialogs.py:828-830` | `closeEvent` calls `thread.stop()` but not `thread.wait()` nor `super()` | +| B9 | 🟡 Low | S1 | `dialogs.py:1121-1122` | `show()+raise_()` without `activateWindow()` nor modality | +| B10| 🟡 Low | — | `plugin.py:36` (init), various | fragile multiple-window / multi-wallet handling | + +--- + +## Detail of the problems + +### B1 — `self.parent = parent` breaks Qt's window system 🔴 +**Where:** `dialogs.py:40` (in `BalDialog.__init__`), repeated at `:69` and `:475`; +similar in other dialogs. + +```python +self.parent = parent # <-- PROBLEM +super().__init__(parent) +``` + +**Cause:** in Qt, `parent()` is a **method** of `QWidget` that returns the +parent widget. By assigning an **attribute** `self.parent`, you mask it: from +that point on `self.parent` is no longer the method but the saved value. Any +code (even inside Qt or Electrum) that expects `widget.parent()` as a method +may behave unexpectedly. In addition, the `parent` that is passed is not always +the correct **top-level window**, so the dialog is not attached hierarchically +to the Electrum window and ends up **behind** it (S1). + +**Proposed fix:** +- Do not override `parent`: rename the attribute (e.g. `self._bal_parent`). +- Always pass as `parent` Electrum's **top-level window** + (`window.top_level_window()`), so the dialog stays in the foreground relative + to it. + +--- + +### B2 — Dialogs opened with `.show()` instead of modally 🔴 +**Where:** +- `window.py:148` `show_willexecutor_dialog` → `self.willexecutor_dialog.show()` +- `window.py:936` `preview_modal_dialog` → `self.dw.show()` (the name says + "modal" but it uses `show()`!) +- `window.py:566` `show_transaction_real` → `d.show()` + +**Cause:** `show()` opens a **non-modal, independent** window: if the `parent` +is not set correctly (see B1), the window does not stay above Electrum and +"disappears behind it" (S1). The inconsistency is noticeable: elsewhere `.exec()` +is used correctly (e.g. `init_wizard` at `window.py:144`, `settings_dialog` at +`plugin.py:254`), which is modal and stays in the foreground. + +**Proposed fix:** +- For the dialogs that must stay in the foreground: use `exec()` (modal) **or** + `show()` + correct parent + `setWindowModality(Qt.WindowModal)` + + `raise_()` + `activateWindow()`. +- Keep the same logic of "what the dialog does" (no change of functional + behaviour, only z-order/modality). + +--- + +### B3 — "Please restart Electrum to activate the BAL plugin" 🔴 +**Where:** `plugin.py:38-42` (`init_qt` hook). + +```python +if wallet: + window.show_warning(_("Please restart Electrum to activate the BAL plugin"), ...) + return +``` + +**Cause:** when the plugin is **enabled hot** (wallet already open), the +`init_qt` hook gives up and asks for a restart instead of initializing the tabs +and menus on the already-loaded wallet. It is **the direct cause of symptom +S2**: "you have to close/restart Electrum for it to work". + +**Proposed fix:** +- In `init_qt`, if there is already an open wallet, run the same initialization + that normally happens in `load_wallet` (create `BalWindow`, tabs, menu, load + the will) **without** requiring a restart. +- Symmetrically, handle `close_wallet` properly to tear down the tabs/menu, so + that re-enabling/reloading does not leave dirty state. + +--- + +### B4 — Dictionary key `winId` (method) instead of `winId()` 🔴 +**Where:** `plugin.py:45` (write) and `plugin.py:111` (read). + +```python +self.bal_windows[top_level_window.winId] = w # writes with the *function* winId +... +w = self.bal_windows.get(window.winId, None) # reads with the *function* winId +``` + +**Cause:** `winId` without parentheses is the **bound method**, not the window +identifier. Used as a key it "works by accident" because the same window object +produces the same bound method; but it is fragile and semantically wrong: with +more windows/wallets or after reopening, the matching can break, creating +duplicate `BalWindow` objects or failing to find the right one → inconsistent +state (contributes to S2). + +**Proposed fix:** +- Use a stable, correct key, e.g. `int(window.winId())` or `id(window)`, + **consistently** both when writing and when reading. + +--- + +### B5 — `on_close` swallows all errors 🟠 +**Where:** `window.py:664-677`. + +```python +def on_close(self): + try: + if not self.disable_plugin: + close_window = BalBuildWillDialog(self) + close_window.build_will_task() + self.save_willitems() + self.heirs_tab.close() + ... + except Exception: + pass # <-- hides any cleanup error +``` + +**Cause:** if any of these operations fails, the exception is silenced: +tabs/menu are not removed, the state (`willitems`, `heirs`, tabs) stays in +memory and "dirty" until Electrum is restarted (S2). + +**Proposed fix:** +- Do not silence it: log the error with `_logger`. +- Make the cleanup **robust and idempotent** (each step in a separate + try/except with logging), so a partial failure does not block the other + steps. +- Explicitly reset the state (`willitems={}`, references to tabs/menu set to + `None`) at the end of `on_close`. + +--- + +### B6 — `BalBlockingWaitingDialog` blocks the GUI thread 🟠 +**Where:** `dialogs.py:445-462`. + +```python +self.show() +# QCoreApplication.processEvents() # <-- commented out +# QCoreApplication.processEvents() +try: + task() # runs the task ON the GUI thread -> "frozen" window +finally: + self.accept() +``` + +**Cause:** after `show()` the GUI is not given time to paint itself +(`processEvents` is commented out) and then `task()` is run **blocking** the +interface thread. Result: the "Please wait" window can appear empty, fail to +repaint, and the app seems stuck (contributes to S1/the perception of a +freeze). + +**Proposed fix:** +- Either run the task in a `TaskThread` (as `BalWaitingDialog` already does), +- or, if it must stay blocking, restore a `processEvents()` after `show()` so + the window is painted before the task. + +--- + +### B7 — `closeEvent`/`hideEvent` with the thread cleanup commented out 🟠 +**Where:** `dialogs.py:48-58` (`BalDialog`). + +```python +def closeEvent(self, event): + self._stopping = True + #if self.thread: + # self.thread.stop() # <-- disabled + super().closeEvent(event) +``` + +**Cause:** when the dialog closes, any active threads are **not** stopped. They +keep running in the background, can write to already-destroyed widgets or hold +resources/connections → erratic behaviour until a restart (S2). + +**Proposed fix:** +- Safely restore stopping the threads: `if self.thread: + self.thread.stop(); self.thread.wait()` with a guard on `None`. + +--- + +### B8 — `BalBuildWillDialog.closeEvent` incomplete 🟠 +**Where:** `dialogs.py:828-830`. + +```python +def closeEvent(self, event): + self._stopping = True + self.thread.stop() + # missing self.thread.wait() and missing super().closeEvent(event) +``` + +**Cause:** `stop()` signals the stop but does not wait for the thread to finish +(`wait()`), and `super().closeEvent(event)` is not called: the close event is +not propagated correctly. Possible orphan threads and windows that do not close +cleanly. + +**Proposed fix:** +- `self.thread.stop(); self.thread.wait(); super().closeEvent(event)` with a + guard on `self.thread is None`. + +--- + +### B9 — `show()+raise_()` without `activateWindow()`/modality 🟡 +**Where:** `dialogs.py:1121-1122` (e.g. `WillExecutorDialog`/detail). + +```python +self.show() +self.raise_() +# missing self.activateWindow(); no modality set +``` + +**Cause:** `raise_()` raises the window in the stack but on some window +managers (including Windows) without `activateWindow()` it does not receive +focus and may still end up behind. Without modality, the user can go back to the +main window leaving the dialog hidden. + +**Proposed fix:** +- Add `self.activateWindow()` after `raise_()`, and consider + `setWindowModality(Qt.WindowModal)` where it makes sense. + +--- + +### B10 — Fragile multiple-window / multi-wallet handling 🟡 +**Where:** `plugin.py:30-62` (`init_qt`), `get_window` (`plugin.py:109-115`). + +**Cause:** the `bal_windows` map and the menu attachment rely on assumptions +(B4) and on iterating the menubar's children by name (`"&Tools"`), which is +sensitive to **localization** (you use `Locale: Italian_Italy`!). If the menu +is not named exactly `&Tools` in the current language, the attachment can fail +silently. + +**Proposed fix:** +- Use the official `window.tools_menu` API (already used in `init_menubar`, + `plugin.py:79`) instead of looking up the menu by its translated title. +- Unify the creation/lookup of `BalWindow` on a stable key (B4). + +--- + +## Proposed correction strategy (for Phase B/C) + +In order to **not change the operating logic** and reduce the risks, I propose +to introduce a **single centralized point** for window management (a small +helper, e.g. `gui/qt/window_utils.py`) with functions such as: + +- `show_modal(dialog)` → sets the correct parent, modality, `exec()`. +- `show_on_top(dialog)` → `show()` + `raise_()` + `activateWindow()` for the + few cases that must stay non-modal. + +And then replace the scattered `.show()`/`.exec()` calls with these functions. +Advantages: +- the **business logic stays intact** (what the dialog does does not change); +- only the "how" it is shown/closed is touched; +- easier to test and to review (small, localized diff). + +### Recommended order +1. **B3 + B4** (hot init + window key): fix the root of S2. +2. **B1 + B2 + B9** (parent/modality/z-order): fix S1. +3. **B5 + B7 + B8** (robust cleanup + threads): close the remaining S2 issues. +4. **B6 + B10** (waiting dialog + localized menus): polish. + +--- + +## What is needed from you for Phase B/C +- Confirmation that I may modify the **GUI behaviour** (parent, modality, + cleanup, hot init) while keeping the business logic unchanged. +- Testing on **Electrum portable Windows** after each group of fixes, with a + description/screenshot of what happens (opening dialogs, hot enabling, + closing the wallet). + +> Note: bugs B1–B10 exist **identically in the original** — this refactor +> preserved them faithfully (that was the goal of the previous phase). Phase B/C +> fixes them. diff --git a/REPORT_NETWORKING_PARALLELO.md b/PARALLEL_NETWORKING_REPORT.md similarity index 100% rename from REPORT_NETWORKING_PARALLELO.md rename to PARALLEL_NETWORKING_REPORT.md