diff --git a/DIAGNOSI_GUI.md b/DIAGNOSI_GUI.md new file mode 100644 index 0000000..426e182 --- /dev/null +++ b/DIAGNOSI_GUI.md @@ -0,0 +1,293 @@ +# BAL — Diagnosi dei problemi GUI (Fase A) + +Documento di sola **diagnosi**: nessuna riga di codice funzionale è stata +modificata. 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.