Files
bal-electrum-plugin/DIAGNOSI_GUI.md
2026-06-20 09:50:43 -04:00

320 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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()``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 B1B10 esistono **identici nell'originale** — questo refactor li
> ha preservati fedelmente (era l'obiettivo della fase precedente). La Fase B/C
> li corregge.