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

13 KiB
Raw Blame History

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.parentself._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.

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_dialogself.willexecutor_dialog.show()
  • window.py:936 preview_modal_dialogself.dw.show() (il nome dice "modal" ma usa show()!)
  • window.py:566 show_transaction_reald.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).

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

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.

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.

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

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.

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

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.