* fix(gui): correct window z-order and lifecycle bugs (B1-B10)
Sintomi risolti:
- S1: le finestre del plugin sparivano dietro Electrum
- S2: alcuni meccanismi funzionavano solo dopo chiusura+pulizia di Electrum
La logica di business resta BYTE-IDENTICA (nessuna modifica a bal/core/*):
cambiano solo parent, modalità, ciclo di vita, cleanup e presentazione.
Nuovo modulo bal/gui/qt/window_utils.py con helper centralizzati:
- top_level_of, bring_to_front, stop_thread, show_modal, show_on_top
Fix per bug:
- B1: self.parent -> self._bal_parent (dialogs/lists/widgets); parent = top_level_of(parent)
- B2/B9: .show() -> show_on_top()/show_modal()/bring_to_front()
- B3: init a caldo _setup_window() replica load_wallet (niente 'restart Electrum')
- B4: chiave finestra stabile _window_key() = id(window)
- B5: on_close riscritto (no except:pass, log per-step, reset stato)
- B6: BalBlockingWaitingDialog ripristina processEvents()
- B7/B8: closeEvent/hideEvent -> stop_thread() (stop+wait) + super()
- B10: uso di window.tools_menu (no ricerca per titolo localizzato '&Tools')
Test: smoke + gui_fixes (regressione B1-B10) + external_zip tutti verdi.
Doc aggiornata: DIAGNOSI_GUI.md marca B1-B10 come FIXED.
* fix(gui): do not kill task thread on dialog close (download list regression)
The B7/B8 change added stop_thread() to BalDialog.closeEvent/hideEvent.
But Electrum's TaskThread.on_done runs cb_done (often self.accept, which
closes the waiting dialog) BEFORE cb_result (on_success, which updates the
will-executor list). Stopping/joining the thread inside closeEvent therefore
tore the thread down before on_success ran, silently dropping the downloaded
will-executor list ('Download List' appeared to do nothing).
Restore the original safe behavior: the base BalDialog no longer stops the
thread on close/hide (matching the original plugin, which deliberately left
this commented out). Long-lived dialogs that own a thread still stop it
explicitly in their own handlers.
Adds a regression test asserting BalDialog.closeEvent/hideEvent never call
stop_thread.
* fix(gui): restore modal exec for waiting dialog + surface download failures
Two changes to fix 'Download List' doing nothing:
1) BalWaitingDialog.exe() now keeps the original application-modal exec()
(only adding raise/activate for visibility). The earlier switch to
window-modal could interfere with how the TaskThread result (on_success,
which populates the will-executor list) is delivered via a queued signal
while the modal loop is spinning.
2) BalWindow.download_list now logs how many entries were received and, when
the result is empty (the core download_list swallows errors and returns {}),
shows a warning to the user instead of failing silently. This makes any
future network/parse failure visible in the Electrum log and to the user.
Business logic in bal/core/* is unchanged.
* fix(gui): show the real download error reason in the warning popup
When 'Download List' fails, the core download_list returns {} and the cause
was only visible in the Electrum log (which is hard to capture). The GUI now
re-issues the same raw request when the result is empty and shows the actual
exception/reason in the warning popup (e.g. SSL error, timeout, empty server
response). Pure GUI-side diagnostics; bal/core/* logic unchanged.
* fix(gui): download will-executor list synchronously like the original
ROOT CAUSE: the original plugin's 'Download List' button called
Willexecutors.download_list() DIRECTLY on the GUI thread
(qt.py: WillExecutorWidget.download_list). The refactor instead routed the
button through BalWaitingDialog + TaskThread. Electrum's
Network.send_http_on_proxy behaves differently depending on the calling
thread, and on the user's setup the TaskThread path timed out ('No response
from the server'), while the original direct call worked fine.
FIX: WillExecutorWidget.download_list now performs the same direct,
synchronous download as the original, updates and saves the list, and shows a
warning only on genuine failure. The wizard download path (which the original
also ran via TaskThread) is left unchanged.
Compared against upstream original source (kaibot/bal-electrum-plugin):
send_request, handle_response, download_list core logic are byte-identical;
only the GUI call site is restored to the original behaviour.
* diag(gui): detailed download diagnostics + hardcoded-URL fallback
The will-executor download still times out on the user's setup even with the
direct (original-style) GUI-thread call, and the URL/request are byte-identical
to the working original. To pinpoint the real cause, the Download List button
now:
- logs whether a Network instance is present;
- tries the configured WELIST_SERVER URL AND falls back to the original
hardcoded https://welist.bitcoin-after.life/ endpoint (so a stale/bad config
value can't break it);
- shows the EXACT URL(s) tried and the precise exception per attempt in the
warning popup instead of a generic timeout message.
bal/core networking remains unchanged.
* diag(gui): add direct-HTTPS control probe to download diagnostics
When the Electrum-network download fails, also run a plain urllib HTTPS GET
(bypassing Electrum's Network/proxy layer) and show its result in the popup.
This distinguishes a real connectivity/DNS/firewall problem from an
Electrum-network-state problem, so we can finally pinpoint why the request
times out only in this build.
* fix(gui): unify wizard + button download on one synchronous path with diagnostics
The 'No response from the server (timeout?)' popup was coming from the WIZARD
download path in window.py (BalWaitingDialog + TaskThread), which was never
switched to the original direct call - only the list button had been.
Both paths now share BalWindow.fetch_will_executors_list(): a direct,
synchronous GUI-thread download (like the original), trying the configured
server then the hardcoded fallback, with full diagnostics (exact URL/error per
attempt + a direct-HTTPS control probe) shown in the failure popup.
This both fixes the wizard timeout and guarantees the same diagnostic popup
('Details (via Electrum network)' + 'Direct connection test') regardless of
which UI entry point is used.
* fix(gui): clean up will-executor download (waiting dialog + simple message)
Root cause of the 'download not working' reports was environmental (the user's
network/ISP was resetting the connection to the IPv6/IPv4 host; a VPN fixes
it), NOT a plugin bug. Final cleanup of the diagnostic code:
- download_list (button + wizard) again uses BalWaitingDialog + TaskThread so
the GUI is not frozen and shows a 'Downloading will-executors list...' dialog.
- Keep the configured + hardcoded-fallback server URLs and detailed per-attempt
diagnostics, but write them to the Electrum log only.
- On failure the user now sees a simple English message explaining it is most
likely a connection/firewall issue (a VPN often helps), instead of a technical
timeout/exception dump.
- Removed the urllib control probe from the user-facing popup.
bal/core networking unchanged.
---------
Co-authored-by: GenSpark AI Developer <ai@genspark.dev>
13 KiB
BAL — Diagnosi dei problemi GUI (Fase A) → ✅ RISOLTI (Fase B)
STATO: tutti i bug B1-B10 sono stati CORRETTI sul branch
fix/gui-window-lifecycle. 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.