13 KiB
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, squashdd6f677). La logica di business resta byte-identica (nessuna modifica abal/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 correttoB3 ✅ FIXED init a caldo: _setup_window()replicaload_wallet, niente "restart Electrum"B4 ✅ FIXED chiave finestra stabile _window_key()=id(window)B5 ✅ FIXED on_closeriscritto: nienteexcept:pass, log per-step, reset statoB6 ✅ FIXED BalBlockingWaitingDialog:processEvents()ripristinatoB7 ✅ 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&ToolsHelper 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.
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
parentla 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:148show_willexecutor_dialog→self.willexecutor_dialog.show()window.py:936preview_modal_dialog→self.dw.show()(il nome dice "modal" ma usashow()!)window.py:566show_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) oshow()+ 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).
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 inload_wallet(creareBalWindow, tab, menu, caricare il will) senza richiedere il riavvio. - Simmetricamente, gestire bene
close_walletper 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).
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())oppureid(window), in modo coerente sia in scrittura sia in lettura.
B5 — on_close ingoia tutti gli errori 🟠
Dove: window.py:664-677.
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 aNone) a fineon_close.
B6 — BalBlockingWaitingDialog blocca il thread della GUI 🟠
Dove: dialogs.py:445-462.
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()doposhow()per far disegnare la finestra prima del task.
B7 — closeEvent/hideEvent con cleanup thread commentato 🟠
Dove: dialogs.py:48-58 (BalDialog).
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 suNone.
B8 — BalBuildWillDialog.closeEvent incompleto 🟠
Dove: dialogs.py:828-830.
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 suself.thread is None.
B9 — show()+raise_() senza activateWindow()/modalità 🟡
Dove: dialogs.py:1121-1122 (es. WillExecutorDialog/dettaglio).
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()doporaise_(), e valutaresetWindowModality(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 ininit_menubar,plugin.py:79) invece di cercare il menu per titolo tradotto. - Unificare la creazione/lookup di
BalWindowsu 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
- B3 + B4 (init a caldo + chiave finestre): risolvono la radice di S2.
- B1 + B2 + B9 (parent/modalità/z-order): risolvono S1.
- B5 + B7 + B8 (cleanup robusto + thread): chiudono i residui di S2.
- 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.