# 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.