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