- CHANGELOG_REFACTOR.md: translated sections 1-16 (header + §1-§16) from Italian to English; sections 17-18 were already English and left untouched. All code blocks, commit hashes, tables, names, versions and structure kept. - DIAGNOSI_GUI.md -> GUI_DIAGNOSIS.md: renamed and fully translated to English (title included), preserving code, line refs, emojis and tables. - REPORT_NETWORKING_PARALLELO.md -> PARALLEL_NETWORKING_REPORT.md: renamed only (content was already in English). - CHANGELOG.md: added entry #12 documenting this task. Documentation-only change; no plugin code touched (zip-first not applicable).
13 KiB
BAL — Diagnosis of the GUI problems (Phase A) → ✅ RESOLVED (Phase B)
STATUS: all bugs B1-B10 have been FIXED and merged into
main(PR #2, squashdd6f677). The business logic remains byte-identical (no changes tobal/core/*): only the presentation, parent, modality, lifecycle and cleanup of the windows changed.
ID Status Applied fix B1 ✅ FIXED self.parent→self._bal_parent(dialogs/lists/widgets); parent =top_level_of(parent)B2 ✅ FIXED .show()→show_on_top()/show_modal()with the correct parentB3 ✅ FIXED hot init: _setup_window()replicatesload_wallet, no more "restart Electrum"B4 ✅ FIXED stable window key _window_key()=id(window)B5 ✅ FIXED on_closerewritten: noexcept:pass, per-step logging, state resetB6 ✅ FIXED BalBlockingWaitingDialog:processEvents()restoredB7 ✅ FIXED closeEvent/hideEvent:stop_thread()+super()B8 ✅ FIXED closeEvent:stop_thread()(stop+wait) +super()B9 ✅ FIXED bring_to_front()=raise_()+activateWindow()B10 ✅ FIXED use of window.tools_menu(official API), no lookup by the&ToolstitleHelpers centralized in
bal/gui/qt/window_utils.py:top_level_of,bring_to_front,stop_thread,show_modal,show_on_top. Regression test:tests/gui_fixes_test.py(in addition to smoke + external_zip).
(Historical) Original diagnosis
A diagnosis-only document: no line of functional code had been modified in Phase A. It lists the graphical/lifecycle problems found in the code, their technical cause and the proposed fix, with line references.
The two symptoms you reported:
- (S1) The plugin windows disappear behind the Electrum window.
- (S2) Some mechanisms work only after closing and "cleaning up" Electrum.
Both are explained by the bugs below.
Summary (table)
| ID | Severity | Symptom | File:line | Short cause |
|---|---|---|---|---|
| B1 | 🔴 High | S1 | dialogs.py:40,69,475 |
self.parent = parent overrides the QWidget.parent() method |
| B2 | 🔴 High | S1 | window.py:148,936, window.py:566 |
dialogs opened with .show() (non-modal, not staying in the foreground) |
| B3 | 🔴 High | S2 | plugin.py:38-42 |
"Please restart Electrum" message = unhandled hot init |
| B4 | 🔴 High | S2 | plugin.py:45,111 |
dictionary key winId (method) instead of winId() (value) |
| B5 | 🟠 Medium | S2 | window.py:664-677 |
on_close with except: pass that hides cleanup errors |
| B6 | 🟠 Medium | S1/S2 | dialogs.py:445-462 |
BalBlockingWaitingDialog blocks the GUI thread, processEvents commented out |
| B7 | 🟠 Medium | S2 | dialogs.py:48-58 |
closeEvent/hideEvent with the thread cleanup commented out |
| B8 | 🟠 Medium | S2 | dialogs.py:828-830 |
closeEvent calls thread.stop() but not thread.wait() nor super() |
| B9 | 🟡 Low | S1 | dialogs.py:1121-1122 |
show()+raise_() without activateWindow() nor modality |
| B10 | 🟡 Low | — | plugin.py:36 (init), various |
fragile multiple-window / multi-wallet handling |
Detail of the problems
B1 — self.parent = parent breaks Qt's window system 🔴
Where: dialogs.py:40 (in BalDialog.__init__), repeated at :69 and :475;
similar in other dialogs.
self.parent = parent # <-- PROBLEM
super().__init__(parent)
Cause: in Qt, parent() is a method of QWidget that returns the
parent widget. By assigning an attribute self.parent, you mask it: from
that point on self.parent is no longer the method but the saved value. Any
code (even inside Qt or Electrum) that expects widget.parent() as a method
may behave unexpectedly. In addition, the parent that is passed is not always
the correct top-level window, so the dialog is not attached hierarchically
to the Electrum window and ends up behind it (S1).
Proposed fix:
- Do not override
parent: rename the attribute (e.g.self._bal_parent). - Always pass as
parentElectrum's top-level window (window.top_level_window()), so the dialog stays in the foreground relative to it.
B2 — Dialogs opened with .show() instead of modally 🔴
Where:
window.py:148show_willexecutor_dialog→self.willexecutor_dialog.show()window.py:936preview_modal_dialog→self.dw.show()(the name says "modal" but it usesshow()!)window.py:566show_transaction_real→d.show()
Cause: show() opens a non-modal, independent window: if the parent
is not set correctly (see B1), the window does not stay above Electrum and
"disappears behind it" (S1). The inconsistency is noticeable: elsewhere .exec()
is used correctly (e.g. init_wizard at window.py:144, settings_dialog at
plugin.py:254), which is modal and stays in the foreground.
Proposed fix:
- For the dialogs that must stay in the foreground: use
exec()(modal) orshow()+ correct parent +setWindowModality(Qt.WindowModal)+raise_()+activateWindow(). - Keep the same logic of "what the dialog does" (no change of functional behaviour, only z-order/modality).
B3 — "Please restart Electrum to activate the BAL plugin" 🔴
Where: plugin.py:38-42 (init_qt hook).
if wallet:
window.show_warning(_("Please restart Electrum to activate the BAL plugin"), ...)
return
Cause: when the plugin is enabled hot (wallet already open), the
init_qt hook gives up and asks for a restart instead of initializing the tabs
and menus on the already-loaded wallet. It is the direct cause of symptom
S2: "you have to close/restart Electrum for it to work".
Proposed fix:
- In
init_qt, if there is already an open wallet, run the same initialization that normally happens inload_wallet(createBalWindow, tabs, menu, load the will) without requiring a restart. - Symmetrically, handle
close_walletproperly to tear down the tabs/menu, so that re-enabling/reloading does not leave dirty state.
B4 — Dictionary key winId (method) instead of winId() 🔴
Where: plugin.py:45 (write) and plugin.py:111 (read).
self.bal_windows[top_level_window.winId] = w # writes with the *function* winId
...
w = self.bal_windows.get(window.winId, None) # reads with the *function* winId
Cause: winId without parentheses is the bound method, not the window
identifier. Used as a key it "works by accident" because the same window object
produces the same bound method; but it is fragile and semantically wrong: with
more windows/wallets or after reopening, the matching can break, creating
duplicate BalWindow objects or failing to find the right one → inconsistent
state (contributes to S2).
Proposed fix:
- Use a stable, correct key, e.g.
int(window.winId())orid(window), consistently both when writing and when reading.
B5 — on_close swallows all errors 🟠
Where: 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 # <-- hides any cleanup error
Cause: if any of these operations fails, the exception is silenced:
tabs/menu are not removed, the state (willitems, heirs, tabs) stays in
memory and "dirty" until Electrum is restarted (S2).
Proposed fix:
- Do not silence it: log the error with
_logger. - Make the cleanup robust and idempotent (each step in a separate try/except with logging), so a partial failure does not block the other steps.
- Explicitly reset the state (
willitems={}, references to tabs/menu set toNone) at the end ofon_close.
B6 — BalBlockingWaitingDialog blocks the GUI thread 🟠
Where: dialogs.py:445-462.
self.show()
# QCoreApplication.processEvents() # <-- commented out
# QCoreApplication.processEvents()
try:
task() # runs the task ON the GUI thread -> "frozen" window
finally:
self.accept()
Cause: after show() the GUI is not given time to paint itself
(processEvents is commented out) and then task() is run blocking the
interface thread. Result: the "Please wait" window can appear empty, fail to
repaint, and the app seems stuck (contributes to S1/the perception of a
freeze).
Proposed fix:
- Either run the task in a
TaskThread(asBalWaitingDialogalready does), - or, if it must stay blocking, restore a
processEvents()aftershow()so the window is painted before the task.
B7 — closeEvent/hideEvent with the thread cleanup commented out 🟠
Where: dialogs.py:48-58 (BalDialog).
def closeEvent(self, event):
self._stopping = True
#if self.thread:
# self.thread.stop() # <-- disabled
super().closeEvent(event)
Cause: when the dialog closes, any active threads are not stopped. They keep running in the background, can write to already-destroyed widgets or hold resources/connections → erratic behaviour until a restart (S2).
Proposed fix:
- Safely restore stopping the threads:
if self.thread: self.thread.stop(); self.thread.wait()with a guard onNone.
B8 — BalBuildWillDialog.closeEvent incomplete 🟠
Where: dialogs.py:828-830.
def closeEvent(self, event):
self._stopping = True
self.thread.stop()
# missing self.thread.wait() and missing super().closeEvent(event)
Cause: stop() signals the stop but does not wait for the thread to finish
(wait()), and super().closeEvent(event) is not called: the close event is
not propagated correctly. Possible orphan threads and windows that do not close
cleanly.
Proposed fix:
self.thread.stop(); self.thread.wait(); super().closeEvent(event)with a guard onself.thread is None.
B9 — show()+raise_() without activateWindow()/modality 🟡
Where: dialogs.py:1121-1122 (e.g. WillExecutorDialog/detail).
self.show()
self.raise_()
# missing self.activateWindow(); no modality set
Cause: raise_() raises the window in the stack but on some window
managers (including Windows) without activateWindow() it does not receive
focus and may still end up behind. Without modality, the user can go back to the
main window leaving the dialog hidden.
Proposed fix:
- Add
self.activateWindow()afterraise_(), and considersetWindowModality(Qt.WindowModal)where it makes sense.
B10 — Fragile multiple-window / multi-wallet handling 🟡
Where: plugin.py:30-62 (init_qt), get_window (plugin.py:109-115).
Cause: the bal_windows map and the menu attachment rely on assumptions
(B4) and on iterating the menubar's children by name ("&Tools"), which is
sensitive to localization (you use Locale: Italian_Italy!). If the menu
is not named exactly &Tools in the current language, the attachment can fail
silently.
Proposed fix:
- Use the official
window.tools_menuAPI (already used ininit_menubar,plugin.py:79) instead of looking up the menu by its translated title. - Unify the creation/lookup of
BalWindowon a stable key (B4).
Proposed correction strategy (for Phase B/C)
In order to not change the operating logic and reduce the risks, I propose
to introduce a single centralized point for window management (a small
helper, e.g. gui/qt/window_utils.py) with functions such as:
show_modal(dialog)→ sets the correct parent, modality,exec().show_on_top(dialog)→show()+raise_()+activateWindow()for the few cases that must stay non-modal.
And then replace the scattered .show()/.exec() calls with these functions.
Advantages:
- the business logic stays intact (what the dialog does does not change);
- only the "how" it is shown/closed is touched;
- easier to test and to review (small, localized diff).
Recommended order
- B3 + B4 (hot init + window key): fix the root of S2.
- B1 + B2 + B9 (parent/modality/z-order): fix S1.
- B5 + B7 + B8 (robust cleanup + threads): close the remaining S2 issues.
- B6 + B10 (waiting dialog + localized menus): polish.
What is needed from you for Phase B/C
- Confirmation that I may modify the GUI behaviour (parent, modality, cleanup, hot init) while keeping the business logic unchanged.
- Testing on Electrum portable Windows after each group of fixes, with a description/screenshot of what happens (opening dialogs, hot enabling, closing the wallet).
Note: bugs B1–B10 exist identically in the original — this refactor preserved them faithfully (that was the goal of the previous phase). Phase B/C fixes them.