forked from bitcoinafterlife/bal-electrum-plugin
docs(bal): translate refactoring docs to English and rename two files
- CHANGELOG_REFACTOR.md: translated sections 1-16 (header + §1-§16) from Italian to English; sections 17-18 were already English and left untouched. All code blocks, commit hashes, tables, names, versions and structure kept. - DIAGNOSI_GUI.md -> GUI_DIAGNOSIS.md: renamed and fully translated to English (title included), preserving code, line refs, emojis and tables. - REPORT_NETWORKING_PARALLELO.md -> PARALLEL_NETWORKING_REPORT.md: renamed only (content was already in English). - CHANGELOG.md: added entry #12 documenting this task. Documentation-only change; no plugin code touched (zip-first not applicable).
This commit is contained in:
31
CHANGELOG.md
31
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
|
**Outcome:** DONE (delivered together with fix #10 in a single ZIP for user
|
||||||
testing before commit).
|
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).
|
||||||
|
|||||||
@@ -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
|
This document lists **all** the changes made to the BAL plugin
|
||||||
(Bitcoin After Life) rispetto alla versione originale `0.2.8`.
|
(Bitcoin After Life) with respect to the original version `0.2.8`.
|
||||||
|
|
||||||
**Principio guida:** refactoring **conservativo e a comportamento invariato**
|
**Guiding principle:** **conservative, behaviour-preserving** refactoring
|
||||||
(Approccio A). La logica di business è stata mantenuta **byte-identica** dove
|
(Approach A). The business logic was kept **byte-identical** where possible;
|
||||||
possibile; sono cambiati soprattutto la **disposizione dei file** e gli
|
what changed are mainly the **file layout** and the **imports**. No algorithmic
|
||||||
**import**. Nessuna riscrittura algoritmica.
|
rewrite.
|
||||||
|
|
||||||
Ambiente di verifica: **Electrum 4.7.2** + **PyQt6** (l'ultima release stabile
|
Verification environment: **Electrum 4.7.2** + **PyQt6** (the latest stable
|
||||||
che espone `json_db.register_dict`).
|
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
|
The main reported problem was that logic and graphics were mixed together, in
|
||||||
mescolate, in particolare in un unico file `qt.py` da **4131 righe**.
|
particular in a single `qt.py` file of **4131 lines**.
|
||||||
|
|
||||||
### Struttura PRIMA (flat, 7 file)
|
### Structure BEFORE (flat, 7 files)
|
||||||
```
|
```
|
||||||
BAL/
|
BAL/
|
||||||
├── __init__.py (vuoto, 0 righe)
|
├── __init__.py (empty, 0 lines)
|
||||||
├── bal.py (243) logica + plugin base
|
├── bal.py (243) logic + plugin base
|
||||||
├── util.py (533) helper
|
├── util.py (533) helpers
|
||||||
├── heirs.py (791) modello eredi + costruzione tx
|
├── heirs.py (791) heirs model + tx building
|
||||||
├── will.py (927) modello will/WillItem
|
├── will.py (927) will/WillItem model
|
||||||
├── willexecutors.py (374) networking will-executor
|
├── willexecutors.py (374) will-executor networking
|
||||||
├── qt.py (4131) TUTTA la GUI + il Plugin in un solo file
|
├── qt.py (4131) ALL the GUI + the Plugin in a single file
|
||||||
└── bal_resources.py (14)
|
└── bal_resources.py (14)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Struttura DOPO (core/ vs gui/)
|
### Structure AFTER (core/ vs gui/)
|
||||||
```
|
```
|
||||||
bal/
|
bal/
|
||||||
├── manifest.json metadati conformi allo standard
|
├── manifest.json standard-compliant metadata
|
||||||
├── qt.py shim di caricamento (re-export di Plugin)
|
├── qt.py loading shim (re-export of Plugin)
|
||||||
├── __init__.py docstring di architettura + __version__
|
├── __init__.py architecture docstring + __version__
|
||||||
├── core/ LOGICA senza dipendenze Qt
|
├── core/ LOGIC with no Qt dependencies
|
||||||
│ ├── util.py (ex util.py)
|
│ ├── util.py (was util.py)
|
||||||
│ ├── plugin_base.py (ex bal.py)
|
│ ├── plugin_base.py (was bal.py)
|
||||||
│ ├── heirs.py (ex heirs.py)
|
│ ├── heirs.py (was heirs.py)
|
||||||
│ ├── will.py (ex will.py)
|
│ ├── will.py (was will.py)
|
||||||
│ └── willexecutors.py (ex willexecutors.py)
|
│ └── willexecutors.py (was willexecutors.py)
|
||||||
└── gui/qt/ PRESENTAZIONE PyQt6
|
└── gui/qt/ PyQt6 PRESENTATION
|
||||||
├── theme.py (59) mappatura stato → colore
|
├── theme.py (59) status → colour mapping
|
||||||
├── common.py (155) import condivisi + helper GUI
|
├── common.py (155) shared imports + GUI helpers
|
||||||
├── widgets.py (782) widget "foglia"
|
├── widgets.py (782) "leaf" widgets
|
||||||
├── calendar.py (80) BalCalendar
|
├── calendar.py (80) BalCalendar
|
||||||
├── dialogs.py (1127) finestre di dialogo
|
├── dialogs.py (1127) dialog windows
|
||||||
├── lists.py (957) viste ad albero (eredi/preview/executor)
|
├── lists.py (957) tree views (heirs/preview/executor)
|
||||||
├── window.py (952) controller GUI per-wallet (BalWindow)
|
├── window.py (952) per-wallet GUI controller (BalWindow)
|
||||||
└── plugin.py (273) classe Plugin (@hook Electrum → GUI)
|
└── plugin.py (273) Plugin class (@hook Electrum → GUI)
|
||||||
```
|
```
|
||||||
|
|
||||||
Il file `qt.py` da 4131 righe è stato suddiviso per **responsabilità**. I
|
The 4131-line `qt.py` file was split by **responsibility**. The **class bodies
|
||||||
**corpi delle classi sono stati copiati verbatim** (riga per riga) per non
|
were copied verbatim** (line by line) so as not to touch the delicate
|
||||||
toccare la logica delicata delle transazioni di eredità.
|
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` |
|
| `Plugin` (67) | `gui/qt/plugin.py` |
|
||||||
| `shown_cv` (317) | `gui/qt/common.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
|
All the following removals were verified as **unused** or **purely debug**, so
|
||||||
**puramente di debug**, quindi non alterano il comportamento del plugin.
|
they do not alter the plugin's behaviour.
|
||||||
|
|
||||||
1. **`util.py` → `core/util.py`**: rimossi tre helper di debug usati solo per
|
1. **`util.py` → `core/util.py`**: removed three debug helpers used only for
|
||||||
stampe a console:
|
console printing:
|
||||||
- `print_var()` (orig. riga 439)
|
- `print_var()` (orig. line 439)
|
||||||
- `print_utxo()` (orig. riga 474)
|
- `print_utxo()` (orig. line 474)
|
||||||
- `print_prevout()` (orig. riga 486)
|
- `print_prevout()` (orig. line 486)
|
||||||
|
|
||||||
2. **`bal.py` → `core/plugin_base.py`**: rimossa la funzione **stub vuota**
|
2. **`bal.py` → `core/plugin_base.py`**: removed the **empty stub** function
|
||||||
`get_will_settings(x)` (orig. righe 12-14):
|
`get_will_settings(x)` (orig. lines 12-14):
|
||||||
```python
|
```python
|
||||||
def get_will_settings(x):
|
def get_will_settings(x):
|
||||||
# print(x)
|
# print(x)
|
||||||
pass
|
pass
|
||||||
```
|
```
|
||||||
⚠️ Verificato: **non era riferita da nessun `register_dict`** — i tre
|
⚠️ Verified: **it was not referenced by any `register_dict`** — the three
|
||||||
`register_dict` usano `tuple`, `dict`, `lambda x: x`. Quindi era codice
|
`register_dict` calls use `tuple`, `dict`, `lambda x: x`. So it was dead
|
||||||
morto. La funzione **usata** `get_will(x)` è stata mantenuta identica.
|
code. The **used** function `get_will(x)` was kept identical.
|
||||||
|
|
||||||
3. **`will.py` (`WillItem`) → spostato in `gui/qt/theme.py`**: il metodo
|
3. **`will.py` (`WillItem`) → moved to `gui/qt/theme.py`**: the method
|
||||||
`WillItem.get_color()` (orig. riga 852) restituiva colori esadecimali —
|
`WillItem.get_color()` (orig. line 852) returned hexadecimal colours —
|
||||||
è logica di **presentazione**, non di dominio. È stato spostato fuori dal
|
it is **presentation** logic, not domain logic. It was moved out of the
|
||||||
modello e trasformato nella funzione `status_color(will_item)` in
|
model and turned into the function `status_color(will_item)` in
|
||||||
`gui/qt/theme.py`. **Verificato byte-identico** su tutte le combinazioni di
|
`gui/qt/theme.py`. **Verified byte-identical** across all status
|
||||||
stato (stessa catena di `get_status(...)`, stessi codici colore).
|
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 .bal import BalPlugin` | `from .plugin_base import BalPlugin` (in willexecutors) |
|
||||||
| `from .util import Util` | `from .util import Util` (invariato, ora dentro core/) |
|
| `from .util import Util` | `from .util import Util` (unchanged, now inside core/) |
|
||||||
| (in qt.py) `from .bal import ...` | i moduli GUI importano da `...core.X` |
|
| (in qt.py) `from .bal import ...` | the GUI modules import from `...core.X` |
|
||||||
|
|
||||||
- Aggiunti `from .common import _, _logger` nei moduli GUI, perché `import *`
|
- Added `from .common import _, _logger` in the GUI modules, because `import *`
|
||||||
**non** esporta i nomi che iniziano con underscore.
|
does **not** export names starting with an underscore.
|
||||||
- Aggiunti 3 import "lazy" (dentro le funzioni) in `dialogs.py` per spezzare il
|
- Added 3 "lazy" imports (inside the functions) in `dialogs.py` to break the
|
||||||
ciclo `dialogs ↔ lists` (lists importa `BalBuildWillDialog` da dialogs).
|
`dialogs ↔ lists` cycle (lists imports `BalBuildWillDialog` from dialogs).
|
||||||
|
|
||||||
La **logica interna dei metodi** non è stata toccata: `prepare_transactions()`,
|
The **internal logic of the methods** was not touched: `prepare_transactions()`,
|
||||||
`buildTransactions()` ecc. sono verbatim.
|
`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) |
|
| `name` | `"BAL"` | `"bal"` (lowercase = dir name) |
|
||||||
| `version` | (assente, era solo nella description) | `"0.2.8"` |
|
| `version` | (absent, was only in the description) | `"0.2.8"` |
|
||||||
| `description` | con `<br>` HTML | testo pulito |
|
| `description` | with HTML `<br>` | clean text |
|
||||||
| `licence` | (assente) | `"MIT"` |
|
| `licence` | (absent) | `"MIT"` |
|
||||||
| `fullname`/`author`/`available_for`/`icon` | presenti | invariati |
|
| `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"`.
|
`__version__ = "0.2.8"`.
|
||||||
- Portati nel package: `LICENSE`, `VERSION`, `README.md`, `bal_resources.py`,
|
- Brought into the package: `LICENSE`, `VERSION`, `README.md`, `bal_resources.py`,
|
||||||
e la cartella `wallet_util/` (invariata).
|
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
|
During testing on **Electrum 4.7.2 portable for Windows**, two real problems
|
||||||
problemi reali nel caricare il plugin come **plugin esterno da .zip**:
|
emerged when loading the plugin as an **external plugin from .zip**:
|
||||||
|
|
||||||
### Bug 5a — `ModuleNotFoundError: No module named 'electrum_external_plugins'`
|
### Bug 5a — `ModuleNotFoundError: No module named 'electrum_external_plugins'`
|
||||||
- **Causa:** Electrum carica i plugin esterni da zip sotto il package sintetico
|
- **Cause:** Electrum loads external plugins from zip under the synthetic
|
||||||
`electrum_external_plugins.bal`, ed esegue **solo** l'`__init__` del package e
|
package `electrum_external_plugins.bal`, and runs **only** the package
|
||||||
il modulo `qt`. Non registra il package radice sintetico né i sotto-package
|
`__init__` and the `qt` module. It does not register the synthetic root
|
||||||
annidati (`gui`, `gui.qt`). Un semplice `from .gui.qt.plugin import Plugin`
|
package nor the nested sub-packages (`gui`, `gui.qt`). A simple
|
||||||
fallisce risalendo ai parent mancanti.
|
`from .gui.qt.plugin import Plugin` fails when walking up to the missing
|
||||||
- **Fix:** `qt.py` ora è uno shim resiliente che (1) rileva a runtime il proprio
|
parents.
|
||||||
nome di package (`__package__`), (2) ricostruisce in `sys.modules` gli
|
- **Fix:** `qt.py` is now a resilient shim that (1) detects its own package
|
||||||
eventuali package padre mancanti, (3) importa `Plugin` con
|
name at runtime (`__package__`), (2) rebuilds in `sys.modules` any missing
|
||||||
`importlib.import_module`. Funziona **sia** come plugin interno
|
parent packages, (3) imports `Plugin` with `importlib.import_module`. It
|
||||||
(`electrum.plugins.bal`) **sia** esterno (`electrum_external_plugins.bal`).
|
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`
|
### Bug 5b — `zlib.error: Error -5 ... incomplete or truncated stream`
|
||||||
- **Causa:** alcune build portable di Electrum su Windows non riescono a
|
- **Cause:** some Electrum portable builds on Windows fail to decompress with
|
||||||
decomprimere con `zipimport` archivi che contengono **voci di directory** o
|
`zipimport` archives that contain **directory entries** or non-standard
|
||||||
compressione non standard.
|
compression.
|
||||||
- **Fix:** aggiunto `build_zip.py`, che genera un archivio "zipimport-friendly":
|
- **Fix:** added `build_zip.py`, which generates a "zipimport-friendly"
|
||||||
solo file (nessuna voce di directory), DEFLATE standard, ordine deterministico
|
archive: files only (no directory entries), standard DEFLATE, deterministic
|
||||||
(SHA-256 riproducibile), escludendo `__pycache__`/`*.pyc`. Stampa anche
|
ordering (reproducible SHA-256), excluding `__pycache__`/`*.pyc`. It also
|
||||||
l'hash SHA-256 per verificare l'integrità del download.
|
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
|
- `tests/smoke_test.py` — checks imports + basic behaviour
|
||||||
(`BalTimestamp`, helper di `Util`, costanti `HEIR_*`, stati di `WillItem`,
|
(`BalTimestamp`, `Util` helpers, `HEIR_*` constants, `WillItem` states,
|
||||||
hook del `Plugin`).
|
`Plugin` hooks).
|
||||||
- `tests/external_zip_test.py` — riproduce **fedelmente** la sequenza di
|
- `tests/external_zip_test.py` — **faithfully** reproduces Electrum's loading
|
||||||
caricamento di un plugin esterno da zip di Electrum (regressione per il
|
sequence for an external plugin from zip (regression test for Bug 5a/5b).
|
||||||
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`).
|
- The transaction-building logic (`heirs.py`, `will.py`).
|
||||||
- I valori e i tipi di `json_db.register_dict(...)`.
|
- The values and types of `json_db.register_dict(...)`.
|
||||||
- I codici colore degli stati (solo spostati in `theme.py`).
|
- The status colour codes (only moved to `theme.py`).
|
||||||
- L'algoritmo di tutte le classi GUI (copiate verbatim).
|
- The algorithm of all the GUI classes (copied verbatim).
|
||||||
- Il formato dei dati salvati nel wallet.
|
- 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
|
- The plugin **requires Electrum 4.7.2**: `json_db.register_dict` was
|
||||||
**rimosso** nelle versioni successive (master), dove andrebbe sostituito con
|
**removed** in later versions (master), where it should be replaced with
|
||||||
`stored_dict.register_name`. Valutare un adeguamento se si vuole supportare
|
`stored_dict.register_name`. Consider an update if you want to support a
|
||||||
Electrum più recente.
|
more recent Electrum.
|
||||||
- Prima del rilascio è consigliata una prova **end-to-end in una sessione
|
- Before release, an **end-to-end test in a real Electrum session** is
|
||||||
Electrum reale** (preferibilmente su testnet), oltre agli smoke test.
|
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
|
After the structural refactoring, **ten graphical and lifecycle defects** of
|
||||||
di ciclo di vita** delle finestre, già presenti nel codice originale. La logica
|
the windows were fixed, all already present in the original code. The business
|
||||||
di business è rimasta **byte-identica** (nessuna modifica a `bal/core/*`): sono
|
logic remained **byte-identical** (no changes to `bal/core/*`): only the
|
||||||
cambiati solo **presentazione, parent, modalità, z-order, ciclo di vita e
|
**presentation, parent, modality, z-order, lifecycle and cleanup** of the Qt
|
||||||
cleanup** delle finestre Qt.
|
windows changed.
|
||||||
|
|
||||||
Sintomi segnalati dall'utente, ora risolti:
|
Symptoms reported by the user, now resolved:
|
||||||
- **(S1)** le finestre del plugin sparivano dietro la finestra di Electrum;
|
- **(S1)** the plugin windows disappeared behind the Electrum window;
|
||||||
- **(S2)** alcuni meccanismi funzionavano solo dopo aver chiuso e riavviato
|
- **(S2)** some mechanisms worked only after closing and restarting Electrum.
|
||||||
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)` |
|
| 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 | dialoghi aperti con `.show()` non modale → finivano sotto la finestra principale | sostituiti con `show_on_top()` / `show_modal()` e parent corretto |
|
| 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 | 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 |
|
| 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 | chiave del dizionario finestre usava il **metodo** `winId` invece del valore | chiave stabile `_window_key()` basata su `id(window)` |
|
| B4 | the windows-dictionary key used the `winId` **method** instead of its value | stable `_window_key()` key based on `id(window)` |
|
||||||
| B5 | `on_close` ingoiava tutti gli errori con `except: pass` | riscritto: niente `except:pass`, log per ogni passo, reset pulito dello stato |
|
| B5 | `on_close` swallowed all errors with `except: pass` | rewritten: no `except:pass`, logging at every step, clean state reset |
|
||||||
| B6 | `BalBlockingWaitingDialog` bloccava il thread della GUI (`processEvents` commentato) | ripristinato `processEvents()` → GUI reattiva durante l'attesa |
|
| B6 | `BalBlockingWaitingDialog` blocked the GUI thread (`processEvents` commented out) | restored `processEvents()` → responsive GUI during the wait |
|
||||||
| B7 | `closeEvent`/`hideEvent` con cleanup del thread commentato | gestione esplicita di `closeEvent`/`hideEvent` + chiamata a `super()` |
|
| B7 | `closeEvent`/`hideEvent` with the thread cleanup commented out | explicit handling of `closeEvent`/`hideEvent` + call to `super()` |
|
||||||
| B8 | `closeEvent` incompleto in alcuni dialog | gestione uniforme dello stato di chiusura |
|
| B8 | incomplete `closeEvent` in some dialogs | uniform handling of the closing state |
|
||||||
| B9 | `show()+raise_()` senza `activateWindow()` né modalità → finestra non in primo piano | `bring_to_front()` = `raise_()` + `activateWindow()` |
|
| B9 | `show()+raise_()` without `activateWindow()` nor modality → window not in the foreground | `bring_to_front()` = `raise_()` + `activateWindow()` |
|
||||||
| B10 | gestione multi-wallet / multi-finestra fragile; menu cercato per titolo `&Tools` | uso dell'API ufficiale `window.tools_menu` |
|
| 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
|
The window-management helpers were **centralized** in a single module, so that
|
||||||
unico modulo, così la stessa logica non viene duplicata nei vari dialog:
|
the same logic is not duplicated across the various dialogs:
|
||||||
|
|
||||||
- `top_level_of(widget)` — risale alla finestra di primo livello corretta da
|
- `top_level_of(widget)` — walks up to the correct top-level window to use as
|
||||||
usare come parent;
|
parent;
|
||||||
- `bring_to_front(window)` — `raise_()` + `activateWindow()` per portare in
|
- `bring_to_front(window)` — `raise_()` + `activateWindow()` to bring to the
|
||||||
primo piano;
|
foreground;
|
||||||
- `stop_thread(thread)` — stop+wait sicuro di un `TaskThread`;
|
- `stop_thread(thread)` — safe stop+wait of a `TaskThread`;
|
||||||
- `show_modal(dialog)` — apertura modale corretta (`exec()`);
|
- `show_modal(dialog)` — correct modal opening (`exec()`);
|
||||||
- `show_on_top(window)` — apertura non modale ma sopra le altre finestre.
|
- `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/qt/common.py` imports these helpers and makes them available to the rest
|
||||||
GUI.
|
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
|
After installing the package with the GUI fixes, the user reported that the
|
||||||
segnalato che il comando **"download list"** dei will-executor non scaricava
|
will-executor **"download list"** command no longer downloaded the list.
|
||||||
più la lista.
|
|
||||||
|
|
||||||
### Indagine
|
### Investigation
|
||||||
|
|
||||||
Il codice di rete (`core/willexecutors.py`: `send_request`, `handle_response`,
|
The network code (`core/willexecutors.py`: `send_request`, `handle_response`,
|
||||||
`download_list`, `initialize_willexecutor`) è stato confrontato riga per riga
|
`download_list`, `initialize_willexecutor`) was compared line by line with the
|
||||||
con l'originale Gitea ed è risultato **byte-identico** (l'unica differenza è il
|
original Gitea version and turned out to be **byte-identical** (the only
|
||||||
parametro aggiuntivo `welist_server` in `download_list`, retro-compatibile).
|
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**
|
During the investigation, **two real defects** introduced by the GUI fixes were
|
||||||
introdotti dalle correzioni GUI, che potevano "perdere" il risultato del
|
nevertheless found and corrected, which could "lose" the download result:
|
||||||
download:
|
|
||||||
|
|
||||||
1. **`BalDialog.closeEvent`/`hideEvent` fermavano il `TaskThread`.** In Electrum
|
1. **`BalDialog.closeEvent`/`hideEvent` stopped the `TaskThread`.** In Electrum,
|
||||||
`TaskThread.on_done` esegue `cb_done` (cioè `self.accept`, che **chiude** il
|
`TaskThread.on_done` runs `cb_done` (i.e. `self.accept`, which **closes** the
|
||||||
dialog) **prima** di `cb_result` (cioè `on_success`, che **aggiorna** la
|
dialog) **before** `cb_result` (i.e. `on_success`, which **updates** the
|
||||||
lista). Fermare il thread alla chiusura del dialog **scartava** quindi il
|
list). Stopping the thread when the dialog closed therefore **discarded** the
|
||||||
risultato appena scaricato. → I due metodi sono stati riportati a **non**
|
result that had just been downloaded. → The two methods were restored so they
|
||||||
fermare il thread (con commento esplicativo nel codice).
|
do **not** stop the thread (with an explanatory comment in the code).
|
||||||
2. **`BalWaitingDialog.exe()` usava una modalità sbagliata** (`show_modal` /
|
2. **`BalWaitingDialog.exe()` used the wrong modality** (`show_modal` /
|
||||||
`WindowModal`). → Ripristinato l'originale `self.exec()`, aggiungendo prima
|
`WindowModal`). → Restored the original `self.exec()`, first adding
|
||||||
`bring_to_front(self)` per garantire il primo piano.
|
`bring_to_front(self)` to guarantee the foreground.
|
||||||
|
|
||||||
Inoltre i percorsi del **pulsante** e del **wizard** (che prima scaricavano in
|
In addition, the **button** and **wizard** paths (which previously downloaded
|
||||||
modi diversi e con messaggi diversi) sono stati **unificati** in un unico
|
in different ways and with different messages) were **unified** into a single
|
||||||
helper `fetch_will_executors_list`, eseguito dentro il worker del `TaskThread`.
|
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**
|
A control probe with `urllib` that **completely bypassed Electrum** also failed
|
||||||
falliva ugualmente con `WinError 10054` ("connection forcibly closed by remote
|
with `WinError 10054` ("connection forcibly closed by remote host"): a sign
|
||||||
host"): segno che la **rete/ISP dell'utente resettava la connessione HTTPS**
|
that the **user's network/ISP was resetting the HTTPS connection** to
|
||||||
verso `welist.bitcoin-after.life`. La conferma definitiva: **attivando una VPN
|
`welist.bitcoin-after.life`. The definitive confirmation: **with a VPN enabled
|
||||||
il download è andato a buon fine.**
|
the download succeeded.**
|
||||||
|
|
||||||
L'originale "sembrava" funzionare perché spedisce comunque un will-executor di
|
The original "seemed" to work because it ships anyway a **built-in default**
|
||||||
**default già incorporato** (`https://we.bitcoin-after.life`), quindi la lista
|
will-executor (`https://we.bitcoin-after.life`), so the list never appeared
|
||||||
non risultava mai del tutto vuota anche senza un download riuscito.
|
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
|
- **Non-blocking waiting window** kept (`BalWaitingDialog`), so the GUI does
|
||||||
GUI non si congela durante il download.
|
not freeze during the download.
|
||||||
- **Fallback dell'URL**: prima l'URL configurato (`WELIST_SERVER`), poi quello
|
- **URL fallback**: first the configured URL (`WELIST_SERVER`), then the
|
||||||
hardcoded `https://welist.bitcoin-after.life/`.
|
hardcoded `https://welist.bitcoin-after.life/`.
|
||||||
- **Diagnostica dettagliata spostata nei soli log** (rimossa la probe `urllib`
|
- **Detailed diagnostics moved to the logs only** (removed the `urllib` probe
|
||||||
dall'interfaccia).
|
from the interface).
|
||||||
- **Messaggio d'errore semplice per l'utente, in inglese** (`DOWNLOAD_FAILED_MESSAGE`):
|
- **Simple error message for the user, in English** (`DOWNLOAD_FAILED_MESSAGE`):
|
||||||
|
|
||||||
> *"Could not download the will-executors list. This is usually caused by
|
> *"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 internet connection or a firewall, not by the plugin. Please check
|
||||||
> your connection (a VPN often helps) and try again."*
|
> 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`,
|
- `gui/qt/window.py` — shared helper `fetch_will_executors_list`,
|
||||||
`download_list` con `TaskThread` + `BalWaitingDialog`, costante
|
`download_list` with `TaskThread` + `BalWaitingDialog`, constant
|
||||||
`DOWNLOAD_FAILED_MESSAGE`.
|
`DOWNLOAD_FAILED_MESSAGE`.
|
||||||
- `gui/qt/lists.py` — `WillExecutorWidget.download_list` instradato sul
|
- `gui/qt/lists.py` — `WillExecutorWidget.download_list` routed onto the shared
|
||||||
percorso condiviso con `on_success` che aggiorna/salva la lista.
|
path with an `on_success` that updates/saves the list.
|
||||||
- `gui/qt/dialogs.py` — `BalDialog.closeEvent`/`hideEvent` **non** fermano più
|
- `gui/qt/dialogs.py` — `BalDialog.closeEvent`/`hideEvent` no longer stop the
|
||||||
il thread; `BalWaitingDialog.exe()` torna a `self.exec()` + `bring_to_front`.
|
thread; `BalWaitingDialog.exe()` goes back to `self.exec()` + `bring_to_front`.
|
||||||
- `tests/gui_fixes_test.py` — asserzione di **regressione**: verifica che
|
- `tests/gui_fixes_test.py` — **regression** assertion: verifies that
|
||||||
`closeEvent`/`hideEvent` **non** contengano `stop_thread` (per non
|
`closeEvent`/`hideEvent` do **not** contain `stop_thread` (so as not to
|
||||||
reintrodurre il bug che scartava il download).
|
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 |
|
| `__init__.py` | 1 | → | `__init__.py` | 37 |
|
||||||
| `bal.py` | 161 | → | `core/plugin_base.py` | 351 |
|
| `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 |
|
| `heirs.py` | 792 | → | `core/heirs.py` | 806 |
|
||||||
| `will.py` | 903 | → | `core/will.py` | 938 |
|
| `will.py` | 903 | → | `core/will.py` | 938 |
|
||||||
| `willexecutors.py` | 547 | → | `core/willexecutors.py` | 390 |
|
| `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 |
|
| `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/plugin.py` | 303 | `Plugin` class (`@hook` Electrum → GUI) |
|
||||||
| `gui/qt/window.py` | 1048 | `BalWindow` (controller per-wallet) |
|
| `gui/qt/window.py` | 1048 | `BalWindow` (per-wallet controller) |
|
||||||
| `gui/qt/dialogs.py` | 1155 | finestre di dialogo + wizard |
|
| `gui/qt/dialogs.py` | 1155 | dialog windows + wizard |
|
||||||
| `gui/qt/lists.py` | 964 | viste ad albero (eredi/preview/executor) |
|
| `gui/qt/lists.py` | 964 | tree views (heirs/preview/executor) |
|
||||||
| `gui/qt/widgets.py` | 782 | widget "foglia" |
|
| `gui/qt/widgets.py` | 782 | "leaf" widgets |
|
||||||
| `gui/qt/common.py` | 157 | import condivisi + helper |
|
| `gui/qt/common.py` | 157 | shared imports + helpers |
|
||||||
| `gui/qt/window_utils.py` | 119 | helper finestre (NUOVO — vedi §9) |
|
| `gui/qt/window_utils.py` | 119 | window helpers (NEW — see §9) |
|
||||||
| `gui/qt/calendar.py` | 80 | `BalCalendar` |
|
| `gui/qt/calendar.py` | 80 | `BalCalendar` |
|
||||||
| `gui/qt/theme.py` | 59 | mappatura stato → colore |
|
| `gui/qt/theme.py` | 59 | status → colour mapping |
|
||||||
| `gui/qt/__init__.py` | 17 | init package GUI |
|
| `gui/qt/__init__.py` | 17 | GUI package init |
|
||||||
|
|
||||||
> Le differenze nei conteggi di righe rispetto all'originale derivano da:
|
> The differences in the line counts compared to the original come from:
|
||||||
> riformattazione/commenti, separazione degli import per modulo, e spostamento
|
> reformatting/comments, separation of imports per module, and the movement
|
||||||
> di funzioni tra `util.py`/`bal.py` e i nuovi moduli. **Gli algoritmi non sono
|
> of functions between `util.py`/`bal.py` and the new modules. **The algorithms
|
||||||
> stati modificati.**
|
> were not modified.**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 12. Cronologia delle modifiche su GitHub
|
## 12. Change history on GitHub
|
||||||
|
|
||||||
- **`4198a51`** — import iniziale del refactor strutturale (v0.2.8): separazione
|
- **`4198a51`** — initial import of the structural refactor (v0.2.8):
|
||||||
`core/` (logica) vs `gui/qt/` (presentazione), packaging conforme, fix
|
separation of `core/` (logic) vs `gui/qt/` (presentation), compliant
|
||||||
caricamento zip esterno, smoke test (sezioni §1-§8).
|
packaging, external-zip load fix, smoke test (sections §1-§8).
|
||||||
- **`d56fa36`** — questo changelog del refactoring (in italiano).
|
- **`d56fa36`** — this refactoring changelog (in Italian).
|
||||||
- **`4806997`** — `DIAGNOSI_GUI.md`: diagnosi dei bug GUI di z-order e ciclo di
|
- **`4806997`** — `GUI_DIAGNOSIS.md` (originally `DIAGNOSI_GUI.md`): diagnosis
|
||||||
vita (Fase A).
|
of the z-order and lifecycle GUI bugs (Phase A).
|
||||||
- **`dd6f677`** (PR **#2**, squash) — correzioni GUI **B1-B10** + fix download
|
- **`dd6f677`** (PR **#2**, squash) — GUI fixes **B1-B10** + will-executor list
|
||||||
lista will-executor + `window_utils.py` + test di regressione (sezioni §9-§10).
|
download fix + `window_utils.py` + regression test (sections §9-§10).
|
||||||
- **PR #3** — fix **OverflowError su Windows (anno 2038)** che rompeva le schede
|
- **PR #3** — fix for the **OverflowError on Windows (year 2038)** that broke
|
||||||
Will/Heirs e la voce di menu (sezione §13).
|
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)
|
### Symptom (Windows 11)
|
||||||
Dopo aver **riavviato Electrum** o **cambiato wallet**, le schede **Will** e
|
After **restarting Electrum** or **switching wallet**, the **Will** and
|
||||||
**Heirs** sparivano e compariva una **voce di menu condensata/illeggibile**
|
**Heirs** tabs disappeared and a **condensed/illegible menu entry**
|
||||||
(icona + testo sovrapposti) sotto il logo di Electrum, accanto a *Portafogli*.
|
(overlapping icon + text) appeared under the Electrum logo, next to *Wallets*.
|
||||||
Su Linux il problema non si manifestava.
|
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
|
OverflowError: Python int too large to convert to C int
|
||||||
window.py __init__ -> create_heirs_tab -> WillSettingsWidget
|
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)
|
-> datetime.fromtimestamp(NLOCKTIME_MAX)
|
||||||
```
|
```
|
||||||
|
|
||||||
- `NLOCKTIME_MAX = 2**32 - 1 = 4294967295` viene usato come locktime di
|
- `NLOCKTIME_MAX = 2**32 - 1 = 4294967295` is used as the
|
||||||
**default/sentinella**.
|
**default/sentinel** locktime.
|
||||||
- Su **Windows** `time_t` è a **32 bit**, quindi `datetime.fromtimestamp(ts)`
|
- On **Windows** `time_t` is **32-bit**, so `datetime.fromtimestamp(ts)`
|
||||||
solleva **`OverflowError`** per qualsiasi timestamp oltre il **2038**.
|
raises **`OverflowError`** for any timestamp beyond **2038**.
|
||||||
- Su **Linux 64-bit** la stessa chiamata **funziona**: ecco perché il bug si
|
- On **64-bit Linux** the same call **works**: that is why the bug was visible
|
||||||
vedeva solo su Windows e i test su Linux non lo intercettavano.
|
only on Windows and the Linux tests did not catch it.
|
||||||
- L'eccezione interrompeva `BalWindow.__init__` durante `init_menubar` /
|
- The exception interrupted `BalWindow.__init__` during `init_menubar` /
|
||||||
`load_wallet`, lasciando le schede Will/Heirs e la voce di menu **a metà
|
`load_wallet`, leaving the Will/Heirs tabs and the menu entry **half-built**
|
||||||
costruzione** → l'elemento grafico condensato/illeggibile sotto il logo.
|
→ the condensed/illegible graphical element under the logo.
|
||||||
|
|
||||||
> Nota: i due primi tentativi di correzione (status-bar no-op e idempotenza di
|
> Note: the first two correction attempts (a no-op status bar and the
|
||||||
> `init_menubar_tools`) **non** centravano la causa; sono stati comunque
|
> idempotency of `init_menubar_tools`) did **not** hit the cause; they were
|
||||||
> mantenuti perché innocui e leggermente migliorativi, ma il vero colpevole era
|
> kept anyway because they are harmless and slightly improving, but the real
|
||||||
> questo crash a monte.
|
> culprit was this upstream crash.
|
||||||
|
|
||||||
### Fix (comportamento invariato per tutti i valori normali)
|
### Fix (behaviour unchanged for all normal values)
|
||||||
- **`BalTimestamp._safe_fromtimestamp()`**: `datetime.fromtimestamp` con
|
- **`BalTimestamp._safe_fromtimestamp()`**: `datetime.fromtimestamp` with
|
||||||
**clamp a INT32_MAX** (anno 2038) in caso di `OverflowError`/`OSError`/
|
a **clamp to INT32_MAX** (year 2038) on `OverflowError`/`OSError`/
|
||||||
`ValueError`, **esattamente** come la funzione `get_max_allowed_timestamp()`
|
`ValueError`, **exactly** like the original's `get_max_allowed_timestamp()`
|
||||||
dell'originale (workaround per Electrum issue **#6170**).
|
function (workaround for Electrum issue **#6170**).
|
||||||
- Usato in `to_date` / `to_timestamp` / `__str__` / `__repr__` di
|
- Used in `to_date` / `to_timestamp` / `__str__` / `__repr__` of
|
||||||
`BalTimestamp`.
|
`BalTimestamp`.
|
||||||
- `gui/qt/widgets.py` (`set_value`): usa il converter sicuro.
|
- `gui/qt/widgets.py` (`set_value`): uses the safe converter.
|
||||||
- `core/util.py` (`timestamp_minus`): stessa protezione inline con clamp a
|
- `core/util.py` (`timestamp_minus`): same inline protection with a clamp to
|
||||||
INT32_MAX.
|
INT32_MAX.
|
||||||
|
|
||||||
I valori entro il 2038 (date assolute normali, durate relative come `90d`/`5y`)
|
Values within 2038 (normal absolute dates, relative durations such as
|
||||||
producono **lo stesso identico risultato** di prima.
|
`90d`/`5y`) produce **exactly the same result** as before.
|
||||||
|
|
||||||
### Test
|
### Test
|
||||||
- `tests/windows_overflow_test.py` riproduce il limite 32-bit di Windows
|
- `tests/windows_overflow_test.py` reproduces the Windows 32-bit limit
|
||||||
(monkeypatch di `datetime.fromtimestamp`) e dimostra che **senza** il fix si
|
(monkeypatch of `datetime.fromtimestamp`) and proves that **without** the fix
|
||||||
ottiene lo **stesso** `OverflowError` del log, mentre **con** il fix passa.
|
you get the **same** `OverflowError` as in the log, while **with** the fix it
|
||||||
Verificato anche che il test **fallisce** senza il fix.
|
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
|
### Problem
|
||||||
Una transazione di eredità viene firmata con un **locktime fisso e immutabile**
|
An inheritance transaction is signed with a **fixed, immutable locktime** and
|
||||||
e inviata ai will-executor, che sono economicamente incentivati a trasmetterla
|
sent to the will-executors, who are economically incentivized to broadcast it
|
||||||
(incassano le fee). Se l'utente, dopo aver firmato/inviato, **posticipa** la
|
(they collect the fees). If the user, after signing/sending, **postpones** the
|
||||||
data di consegna (es. di un anno), la **vecchia** transazione gia firmata resta
|
delivery date (e.g. by one year), the **old** already-signed transaction
|
||||||
valida sui server dei will-executor. Poiche ha il locktime piu basso, un
|
remains valid on the will-executors' servers. Since it has the lower locktime,
|
||||||
will-executor potrebbe trasmetterla appena scade, eseguendo l'eredita **in
|
a will-executor could broadcast it as soon as it expires, executing the
|
||||||
anticipo** rispetto alla nuova volonta dell'utente. La versione precedente
|
inheritance **earlier** than the user's new intent. The previous version did
|
||||||
**non gestiva** questo caso: il posticipo non produceva alcuna azione.
|
**not handle** this case: postponing produced no action at all.
|
||||||
|
|
||||||
### Soluzione (Strategia B — invalidazione esplicita on-chain)
|
### Solution (Strategy B — explicit on-chain invalidation)
|
||||||
Al posticipo di un'eredita **gia firmata e/o inviata** (stato `COMPLETE` o
|
When postponing an inheritance that is **already signed and/or sent** (state
|
||||||
`PUSHED`), il plugin chiede di **invalidare on-chain** i fondi prima di
|
`COMPLETE` or `PUSHED`), the plugin asks to **invalidate the funds on-chain**
|
||||||
ricostruire la nuova eredita. L'invalidazione spende gli stessi UTXO verso un
|
before rebuilding the new inheritance. The invalidation spends the same UTXOs
|
||||||
nuovo indirizzo di change con `locktime = altezza corrente` (RBF), quindi e
|
to a new change address with `locktime = current height` (RBF), so it is
|
||||||
trasmettibile subito: una volta confermata, la vecchia transazione pre-firmata
|
broadcastable immediately: once confirmed, the old pre-signed transaction
|
||||||
diventa **definitivamente inutilizzabile**, vincendo la corsa contro qualunque
|
becomes **permanently unusable**, winning the race against any will-executor.
|
||||||
will-executor.
|
|
||||||
|
|
||||||
### Dettagli tecnici
|
### Technical details
|
||||||
- **`core/will.py`**:
|
- **`core/will.py`**:
|
||||||
- nuova eccezione `WillPostponedException` (sottoclasse di
|
- new exception `WillPostponedException` (subclass of
|
||||||
`NotCompleteWillException`);
|
`NotCompleteWillException`);
|
||||||
- `check_willexecutors_and_heirs`: il confronto del locktime non usa piu
|
- `check_willexecutors_and_heirs`: the locktime comparison no longer uses the
|
||||||
l'entry dell'erede memorizzata (`their[2]`), che viene aggiornata in memoria
|
stored heir entry (`their[2]`), which is updated in memory together with the
|
||||||
insieme al nuovo valore al momento del posticipo e quindi risulterebbe
|
new value at the moment of postponement and would therefore always look
|
||||||
sempre uguale. Ora confronta il locktime richiesto con **`w.tx.locktime`**,
|
equal. It now compares the requested locktime with **`w.tx.locktime`**, i.e.
|
||||||
cioe il locktime **congelato** nella transazione firmata (immutabile, e
|
the locktime **frozen** in the signed transaction (immutable, and the one
|
||||||
quello che i will-executor possiedono). Tre casi: invariato → coerente;
|
the will-executors hold). Three cases: unchanged → coherent; new > tx on a
|
||||||
nuovo > tx su will firmato/inviato → `WillPostponedException`; nuovo > tx su
|
signed/sent will → `WillPostponedException`; new > tx on a will never sent
|
||||||
will mai inviato → semplice ricostruzione (nessuna fee on-chain).
|
→ simple rebuild (no on-chain fee).
|
||||||
- **`gui/qt/dialogs.py`** (`BalBuildWillDialog.task_phase1`, il percorso reale
|
- **`gui/qt/dialogs.py`** (`BalBuildWillDialog.task_phase1`, the real path used
|
||||||
usato da **Tools → Prepare**): aggiunto il ramo `except WillPostponedException`
|
by **Tools → Prepare**): added the `except WillPostponedException` branch
|
||||||
**prima** di `NotCompleteWillException`; si comporta come il caso "will
|
**before** `NotCompleteWillException`; it behaves like the "expired will"
|
||||||
scaduto" e ritorna `(None, tx)` per innescare firma + broadcast
|
case and returns `(None, tx)` to trigger signing + broadcasting of the
|
||||||
dell'invalidazione. L'utente preme di nuovo **Prepare** per ricostruire,
|
invalidation. The user presses **Prepare** again to rebuild, re-sign and
|
||||||
rifirmare e reinviare la nuova eredita (due passi espliciti, per maggior
|
re-send the new inheritance (two explicit steps, for greater control).
|
||||||
controllo).
|
- **`gui/qt/window.py`** (`build_inheritance_transaction`): added the same
|
||||||
- **`gui/qt/window.py`** (`build_inheritance_transaction`): aggiunto lo stesso
|
branch for completeness of the alternative path, with an explanatory message.
|
||||||
ramo per completezza del percorso alternativo, con messaggio esplicativo.
|
- **`gui/qt/common.py`**: `WillPostponedException` exported.
|
||||||
- **`gui/qt/common.py`**: `WillPostponedException` esportato.
|
|
||||||
|
|
||||||
### NUOVA COLONNA "Server" nella lista transazioni
|
### NEW "Server" COLUMN in the transactions list
|
||||||
Per dare all'utente visibilita costante sullo stato online delle proprie
|
To give the user constant visibility on the online status of their inheritance
|
||||||
transazioni di eredita, e stata aggiunta una colonna dedicata **"Server"** in
|
transactions, a dedicated **"Server"** column was added in `PreviewList`
|
||||||
`PreviewList` (`gui/qt/lists.py`), con etichetta sempre leggibile
|
(`gui/qt/lists.py`), with an always-readable label (`Confirmed on server`,
|
||||||
(`Confirmed on server`, `Sent (not checked)`, `Send failed`, `Not on server`,
|
`Sent (not checked)`, `Send failed`, `Not on server`, `Signed (not sent)`,
|
||||||
`Signed (not sent)`, `Not sent`) e **tooltip** con URL del will-executor e
|
`Not sent`) and a **tooltip** with the will-executor URL and status. The
|
||||||
stato. Le funzioni `server_status_text()` e `server_status_tooltip()` sono in
|
functions `server_status_text()` and `server_status_tooltip()` are in
|
||||||
`gui/qt/theme.py` e riusano gli stessi flag di stato gia esistenti.
|
`gui/qt/theme.py` and reuse the same already-existing status flags.
|
||||||
|
|
||||||
### Test
|
### Test
|
||||||
- I 182 test ufficiali continuano a passare; smoke test ed external-zip test
|
- The 182 official tests keep passing; smoke test and external-zip test OK;
|
||||||
OK; `ruff` senza nuove segnalazioni reali.
|
`ruff` with no new real warnings.
|
||||||
- Verificato sui dati reali del log dell'utente: il posticipo di un'eredita
|
- Verified against the real data from the user's log: postponing a signed
|
||||||
firmata ora rileva correttamente la condizione e avvia l'invalidazione.
|
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)
|
### v0.3.1 (WITHDRAWN)
|
||||||
Per risolvere la doppia firma dell'invalidazione al posticipo, era stato
|
To solve the double signing of the invalidation on postpone,
|
||||||
introdotto `Will.mark_invalidated_by_tx()`, chiamato in
|
`Will.mark_invalidated_by_tx()` had been introduced, called in
|
||||||
`loop_broadcast_invalidating` dopo il broadcast dell'invalidazione, per marcare
|
`loop_broadcast_invalidating` after broadcasting the invalidation, to mark as
|
||||||
`INVALIDATED` le will che spendevano gli stessi UTXO della tx di invalidazione
|
`INVALIDATED` the wills that spent the same UTXOs as the invalidation tx and to
|
||||||
e persistere lo stato con `save_willitems`.
|
persist the state with `save_willitems`.
|
||||||
|
|
||||||
### Perche e stata ritirata
|
### Why it was withdrawn
|
||||||
La modifica ha introdotto una regressione grave segnalata dall'utente:
|
The change introduced a serious regression reported by the user:
|
||||||
**la lista eredita mostrava ancora le vecchie eredita e l'aggiornamento di
|
**the inheritance list still showed the old inheritances and the update of
|
||||||
eredi/date risultava incoerente**.
|
heirs/dates was inconsistent**.
|
||||||
|
|
||||||
Causa: `loop_broadcast_invalidating` e il punto di broadcast usato per **TUTTI**
|
Cause: `loop_broadcast_invalidating` is the broadcast point used for **ALL**
|
||||||
i tipi di invalidazione (posticipo, CheckAlive, will scaduto/anticipato), non
|
types of invalidation (postpone, CheckAlive, expired/anticipated will), not
|
||||||
solo per il posticipo. Inoltre il metodo marcava e **persisteva** lo stato
|
only for postpone. In addition, the method marked and **persisted** the
|
||||||
`INVALIDATED` su tutte le will item che condividevano gli UTXO del wallet
|
`INVALIDATED` state on all the will items that shared the wallet's UTXOs
|
||||||
(tipicamente tutte). Queste will item invalidate restavano poi in memoria e su
|
(typically all of them). These invalidated will items then stayed in memory and
|
||||||
disco, inquinando la ricostruzione di eredi/date e lasciando vecchie voci nella
|
on disk, polluting the rebuild of heirs/dates and leaving old entries in the
|
||||||
lista.
|
list.
|
||||||
|
|
||||||
### v0.3.2 (questa versione): REVERT completo
|
### v0.3.2 (this version): full REVERT
|
||||||
- Rimosso `Will.mark_invalidated_by_tx()` da `core/will.py`.
|
- Removed `Will.mark_invalidated_by_tx()` from `core/will.py`.
|
||||||
- Rimossa la chiamata in `gui/qt/dialogs.py` (`loop_broadcast_invalidating`):
|
- Removed the call in `gui/qt/dialogs.py` (`loop_broadcast_invalidating`): the
|
||||||
il metodo torna **identico** alla v0.3.0.
|
method goes back to being **identical** to v0.3.0.
|
||||||
- Rimossi i due test relativi; mantenuto solo l'assert di gerarchia su
|
- Removed the two related tests; kept only the hierarchy assertion on
|
||||||
`WillPostponedException` (corretto e indipendente).
|
`WillPostponedException` (correct and independent).
|
||||||
- `core/will.py` e `gui/qt/dialogs.py` sono ora **byte-identici** alla v0.3.0
|
- `core/will.py` and `gui/qt/dialogs.py` are now **byte-identical** to the
|
||||||
funzionante (verificato con `git diff a394cde`).
|
working v0.3.0 (verified with `git diff a394cde`).
|
||||||
|
|
||||||
Il bug della doppia invalidazione al posticipo resta quindi **aperto** e andra
|
The double-invalidation-on-postpone bug therefore remains **open** and will
|
||||||
riaffrontato in modo piu mirato (senza toccare il percorso di broadcast comune e
|
have to be tackled in a more targeted way (without touching the common
|
||||||
senza persistere stati su will che condividono gli UTXO), previa conferma
|
broadcast path and without persisting states on wills that share the UTXOs),
|
||||||
dell'utente. La priorita era ripristinare il comportamento corretto di
|
subject to the user's confirmation. The priority was to restore the correct
|
||||||
lista/eredi/date.
|
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
|
### FIX 1 - Removal of an heir detected on Check / Electrum close
|
||||||
`core/will.py` (`check_willexecutors_and_heirs`): prima il plugin
|
`core/will.py` (`check_willexecutors_and_heirs`): previously the plugin
|
||||||
rilevava solo l'**aggiunta** di un erede (raise `HeirNotFoundException` quando un
|
detected only the **addition** of an heir (raising `HeirNotFoundException` when
|
||||||
erede corrente non era piu nella will). Mancava il caso inverso: la
|
a current heir was no longer in the will). The opposite case was missing: the
|
||||||
**rimozione** di un erede. Aggiunto il ramo `else` che lancia
|
**removal** of an heir. Added the `else` branch that raises
|
||||||
`HeirNotFoundException` anche quando la will porta ancora un erede che non e piu
|
`HeirNotFoundException` also when the will still carries an heir that is no
|
||||||
presente nel set di eredi corrente. Cosi la ricostruzione dell'eredita scatta su
|
longer present in the current heir set. This way the inheritance rebuild is
|
||||||
**Check** e alla **chiusura di Electrum** (entrambi usano lo stesso percorso
|
triggered on **Check** and on **Electrum close** (both use the same
|
||||||
`BalBuildWillDialog.build_will_task()`), come deciso dall'utente: nessun
|
`BalBuildWillDialog.build_will_task()` path), as decided by the user: no
|
||||||
aggiornamento automatico dopo la modifica, solo manuale con Check / alla
|
automatic update after the change, only a manual one via Check / on close.
|
||||||
chiusura.
|
|
||||||
|
|
||||||
### FIX 2 - Check interroga i server anche per le will gia inviate
|
### FIX 2 - Check also queries the servers for already-sent wills
|
||||||
`core/will.py` (nuovo `Will.needs_server_check(w)`) e
|
`core/will.py` (new `Will.needs_server_check(w)`) and `gui/qt/lists.py`
|
||||||
`gui/qt/lists.py` (`PreviewList.check`): prima il Check interrogava i server solo
|
(`PreviewList.check`): previously Check queried the servers only for wills in
|
||||||
per le will in stato `PUSHED`. Le will gia inviate ma rimaste su "New / Not sent"
|
the `PUSHED` state. Wills already sent but left at "New / Not sent" were not
|
||||||
non venivano ricontrollate ("nothing to do"). Ora `needs_server_check` include
|
re-checked ("nothing to do"). Now `needs_server_check` includes every **VALID**
|
||||||
ogni will **VALID** con un will-executor e **non ancora CHECKED**, anche se non
|
will with a will-executor that is **not yet CHECKED**, even if not in the
|
||||||
in stato `PUSHED`. Stesso controllo usato sia dal pulsante Check sia da
|
`PUSHED` state. The same check is used both by the Check button and by
|
||||||
`on_close`.
|
`on_close`.
|
||||||
|
|
||||||
### FIX 3 - Hide invalidated/replaced da finestra Impostazioni aggiornava la lista
|
### FIX 3 - Hide invalidated/replaced from the Settings window updated the list
|
||||||
`core/plugin_base.py` (nuovo `sync_hide_filters()`) e
|
`core/plugin_base.py` (new `sync_hide_filters()`) and `gui/qt/window.py`
|
||||||
`gui/qt/window.py` (`update_all`): le checkbox "Hide Replaced" / "Hide
|
(`update_all`): the "Hide Replaced" / "Hide Invalidated" checkboxes in the
|
||||||
Invalidated" nella finestra Impostazioni scrivono direttamente la config
|
Settings window write the config directly (`BalConfig.set`) without touching the
|
||||||
(`BalConfig.set`) senza toccare i flag in cache `_hide_invalidated` /
|
cached flags `_hide_invalidated` / `_hide_replaced` that the list uses to
|
||||||
`_hide_replaced` usati dalla lista per filtrare. Risultato: la lista continuava
|
filter. Result: the list kept filtering with the old value until Electrum was
|
||||||
a filtrare col valore vecchio finche non si riavviava Electrum. Ora
|
restarted. Now `update_all()` calls `sync_hide_filters()`, which re-reads the
|
||||||
`update_all()` chiama `sync_hide_filters()` che ri-legge i flag dalla config,
|
flags from the config, so whatever the source of the change (toolbar or
|
||||||
quindi qualunque sorgente del cambiamento (toolbar o finestra Impostazioni)
|
Settings window) the list updates immediately.
|
||||||
aggiorna subito la lista.
|
|
||||||
|
|
||||||
### Rifinitura UI - Risultati in grassetto nel dialog "Building Will"
|
### UI polish - Bold results in the "Building Will" dialog
|
||||||
`gui/qt/dialogs.py` (`BalBuildWillDialog`): i **risultati** mostrati a destra di
|
`gui/qt/dialogs.py` (`BalBuildWillDialog`): the **results** shown to the right
|
||||||
ogni riga di stato (es. `Ok`, `Ko`, `Nothing to do`, `Skipped`, `Wait`,
|
of each status line (e.g. `Ok`, `Ko`, `Nothing to do`, `Skipped`, `Wait`,
|
||||||
`Timeout`) sono ora resi in **grassetto**, mantenendo i loro colori
|
`Timeout`) are now rendered in **bold**, keeping their colours
|
||||||
(verde/rosso/giallo). Le etichette di stato a sinistra restano in peso normale.
|
(green/red/yellow). The status labels on the left stay in normal weight. The
|
||||||
Modifica centralizzata negli helper `msg_ok`, `msg_error`, `msg_warning`,
|
change is centralized in the helpers `msg_ok`, `msg_error`, `msg_warning`,
|
||||||
`msg_set_status`, piu le righe dei will-executor (push e check) che ora mostrano
|
`msg_set_status`, plus the will-executor lines (push and check) that now show
|
||||||
`Ok/Ko` e `True/False` in grassetto + colore (verde/rosso).
|
`Ok/Ko` and `True/False` in bold + colour (green/red).
|
||||||
|
|
||||||
### Test
|
### Test
|
||||||
- 186 test ufficiali passano; smoke test, external-zip test e simulazione dei
|
- 186 official tests pass; smoke test, external-zip test and the update-flow
|
||||||
flussi di aggiornamento (`tests/sim_update_flows.py`) OK; `ruff` senza nuove
|
simulation (`tests/sim_update_flows.py`) OK; `ruff` with no new real warnings
|
||||||
segnalazioni reali (solo falsi positivi pre-esistenti da star-import).
|
(only pre-existing false positives from star-imports).
|
||||||
- Aggiunti test in `tests/test_core_will.py`:
|
- Added tests in `tests/test_core_will.py`:
|
||||||
`test_check_heirs_unchanged_is_coherent`,
|
`test_check_heirs_unchanged_is_coherent`,
|
||||||
`test_check_heir_removed_triggers_rebuild`,
|
`test_check_heir_removed_triggers_rebuild`,
|
||||||
`test_check_heir_added_triggers_rebuild`, `test_needs_server_check`.
|
`test_check_heir_added_triggers_rebuild`, `test_needs_server_check`.
|
||||||
|
|
||||||
Confermato dall'utente sui dati reali: dopo Sign -> Broadcast -> Check le
|
Confirmed by the user against real data: after Sign -> Broadcast -> Check the
|
||||||
transazioni gia inviate sono tornate verdi ("confirmed on server"); la lista
|
already-sent transactions turned green again ("confirmed on server"); the list
|
||||||
torna pulita; il grassetto e l'aggiornamento delle hide-flag funzionano.
|
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)
|
## 17. UI polish and bug fix — signed-tx colour, wizard, Building Will dialog (v0.3.3)
|
||||||
|
|
||||||
|
|||||||
319
DIAGNOSI_GUI.md
319
DIAGNOSI_GUI.md
@@ -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.
|
|
||||||
320
GUI_DIAGNOSIS.md
Normal file
320
GUI_DIAGNOSIS.md
Normal file
@@ -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.
|
||||||
Reference in New Issue
Block a user