Files
bal-electrum-plugin/bal/gui/qt/plugin.py
GenSpark AI Developer f28f1502e2 fix(gui): restore BAL status-bar icon (bottom-right) + open settings on click
Regression: create_status_bar had been turned into a no-op while chasing the
"condensed menu/tabs" bug. The real cause of that bug was a Windows
OverflowError (year 2038), already fixed separately -- so the no-op wrongly
removed the BAL icon from the status bar.

Restore the original (Gitea) behaviour:
- Build a StatusBarButton with the bal32x32 icon and add it via
  addPermanentWidget, so the icon shows the plugin is installed.
- Clicking the icon opens settings_dialog (quick access to plugin settings).
- Track buttons in self._statusbar_buttons keyed by id(sb.window()); remove the
  stale button before creating a new one to avoid a duplicated icon on restart
  / wallet switch.

Also:
- import StatusBarButton from electrum.gui.qt.main_window; ensure
  read_QIcon_from_bytes is imported; init self._statusbar_buttons in __init__.
- Update tests/gui_fixes_test.py: the regression check now asserts the icon IS
  added (StatusBarButton + addPermanentWidget + settings_dialog +
  _statusbar_buttons book-keeping), instead of the previous wrong no-op check.

Tests: 182 official tests pass; smoke/windows_overflow/parallel/external_zip OK.
Note: from now on all code and code-comments are in English.
2026-06-28 22:51:28 -04:00

331 lines
12 KiB
Python

"""
bal.gui.qt.plugin
=================
The Qt entry point of the plugin.
:class:`Plugin` subclasses :class:`bal.core.plugin_base.BalPlugin` and adds the
Electrum ``@hook`` methods that wire the plugin into the Qt GUI (status-bar
button, Tools menu, wallet load/close, settings dialog). Electrum instantiates
this class because the package ``manifest.json`` declares ``available_for:
["qt"]`` and the loader imports ``qt.py`` (a thin shim re-exporting this class).
One :class:`bal.gui.qt.window.BalWindow` is created per top-level wallet window
and cached in ``self.bal_windows``.
"""
from electrum.gui.qt.main_window import StatusBarButton
from .common import *
from .common import _, _logger # underscore names are not re-exported by "import *"
from .common import read_QIcon_from_bytes
from .widgets import BalCheckBox, BalLineEdit, BalTextEdit
from .window import BalWindow
from .dialogs import BalDialog
def _window_key(window):
"""Return a stable, hashable identity for an Electrum top-level window.
The original code used ``window.winId`` (the *bound method*, not its
result) as a dict key. That happened to work because the same window
object yields the same bound method, but it is semantically wrong and
fragile across window re-creation / multiple wallets. ``id(window)`` is a
stable, correct identity for the lifetime of the window object.
"""
return id(window)
class Plugin(BalPlugin):
def __init__(self, parent, config, name):
_logger.info("INIT BALPLUGIN")
BalPlugin.__init__(self, parent, config, name)
self.bal_windows = {}
# Status-bar buttons, keyed by id(sb.window()). Tracking them lets us
# remove a stale button before creating a fresh one when a wallet is
# switched / Electrum is restarted, so the icon is never duplicated.
self._statusbar_buttons = {}
@hook
def init_qt(self, gui_object):
# Called when the plugin is enabled, including *hot* (while a wallet is
# already open). The original code gave up here and asked the user to
# restart Electrum; instead we fully initialise the already-open
# window(s) so the plugin works immediately.
_logger.info("HOOK bal init qt")
try:
self.gui_object = gui_object
for window in gui_object.windows:
self._setup_window(window, load_open_wallet=True)
except Exception as e:
_logger.error("Error loading plugin {}".format(e))
raise e
def _setup_window(self, window, *, load_open_wallet):
"""Create the BalWindow for *window* and wire its menu (and, when
enabling hot, the already-open wallet).
This mirrors what the ``init_menubar`` + ``load_wallet`` hooks do at
normal startup, so enabling the plugin while a wallet is open no longer
requires restarting Electrum.
"""
w = self.get_window(window)
# Use Electrum's official tools_menu instead of searching the menubar
# for a menu whose *translated* title equals "&Tools" (which breaks
# under non-English locales).
tools_menu = getattr(window, "tools_menu", None)
if tools_menu is not None:
try:
w.init_menubar_tools(tools_menu)
except Exception as e:
_logger.error("init_qt: failed wiring tools menu: {}".format(e))
if load_open_wallet and getattr(window, "wallet", None):
# Replicate load_wallet() for the wallet that is already open.
try:
w.wallet = window.wallet
w.init_will()
w.willexecutors = Willexecutors.get_willexecutors(
self, update=False, bal_window=w
)
w.disable_plugin = False
w.ok = True
except Exception as e:
_logger.error("init_qt: failed initialising open wallet: {}".format(e))
return w
@hook
def create_status_bar(self, sb):
# Show the BAL icon in the status bar (bottom-right): it signals that
# the Bitcoin After Life plugin is installed and, when clicked, quickly
# opens the plugin settings (settings_dialog).
#
# NOTE: this was NOT the "condensed menu/tabs" bug under the Electrum
# logo -- that one was a Windows OverflowError (year 2038), fixed
# separately. The icon must therefore be kept.
#
# To avoid a duplicated icon on restart / wallet switch, we track the
# button by id(sb.window()) and remove the stale one before creating a
# fresh one.
_logger.info("HOOK create status bar")
key = id(sb.window())
old = self._statusbar_buttons.pop(key, None)
if old is not None:
try:
old.setParent(None)
old.deleteLater()
except Exception:
pass
b = StatusBarButton(
read_QIcon_from_bytes(self.read_file("icons/bal32x32.png")),
"Bal " + _("Bitcoin After Life"),
lambda: self.settings_dialog(sb.window()),
sb.height(),
)
sb.addPermanentWidget(b)
self._statusbar_buttons[key] = b
@hook
def init_menubar(self, window):
_logger.info("HOOK init_menubar")
w = self.get_window(window)
w.init_menubar_tools(window.tools_menu)
@hook
def load_wallet(self, wallet, main_window):
_logger.debug("HOOK load wallet")
w = self.get_window(main_window)
# havetoupdate = Util.fix_will_settings_tx_fees(wallet.db)
w.wallet = wallet
w.init_will()
w.willexecutors = Willexecutors.get_willexecutors(
self, update=False, bal_window=w
)
w.disable_plugin = False
w.ok = True
@hook
def close_wallet(self, wallet):
_logger.debug("HOOK close wallet")
# Iterate over a snapshot: on_close() may mutate the GUI/state.
for win in list(self.bal_windows.values()):
if getattr(win, "wallet", None) == wallet:
try:
win.on_close()
except Exception as e:
_logger.error("close_wallet: on_close failed: {}".format(e))
@hook
def init_keystore(self):
_logger.debug("init keystore")
@hook
def daemon_wallet_loaded(self, boh, wallet):
_logger.debug("daemon wallet loaded")
def get_window(self, window):
window = window.top_level_window()
key = _window_key(window)
w = self.bal_windows.get(key, None)
if w is None:
w = BalWindow(self, window)
self.bal_windows[key] = w
return w
def requires_settings(self):
return True
def settings_widget(self, window):
w = self.get_window(window.window)
widget = QWidget()
enterbutton = EnterButton(_("Settings"), partial(w.settings_dialog, window))
widget.setLayout(Buttons(enterbutton, widget))
return widget
def password_dialog(self, msg=None, parent=None):
parent = parent or self
d = PasswordDialog(parent, msg)
return d.run()
def get_seed(self):
password = None
if self.wallet.has_keystore_encryption():
password = self.password_dialog(parent=self.d.parent())
if not password:
raise UserCancelled()
keystore = self.wallet.get_keystore()
if not keystore or not keystore.has_seed():
return
self.extension = bool(keystore.get_passphrase(password))
return keystore.get_seed(password)
def settings_dialog(self, window=None, wallet=None):
d = BalDialog(window, self, self.get_window_title("Settings"))
d.setMinimumSize(100, 200)
qicon = read_QPixmap_from_bytes(self.read_file("icons/bal16x16.png"))
lbl_logo = QLabel()
lbl_logo.setPixmap(qicon)
# heir_ping_willexecutors = BalCheckBox(self.PING_WILLEXECUTORS)
# heir_ask_ping_willexecutors = BalCheckBox(self.ASK_PING_WILLEXECUTORS)
# heir_no_willexecutor = BalCheckBox(self.NO_WILLEXECUTOR)
def on_multiverse_change():
self.update_all()
# heir_enable_multiverse = BalCheckBox(self.ENABLE_MULTIVERSE,on_multiverse_change)
heir_hide_replaced = BalCheckBox(self.HIDE_REPLACED, on_multiverse_change)
heir_hide_invalidated = BalCheckBox(self.HIDE_INVALIDATED, on_multiverse_change)
heir_repush = QPushButton("Rebroadcast transactions")
heir_repush.clicked.connect(partial(self.broadcast_transactions, True))
bal_mode = QComboBox()
options = ["Easy", "Advanced", "Experimental"]
bal_mode.addItems(options)
grid = QGridLayout(d)
add_widget(
grid,
"Hide Replaced",
heir_hide_replaced,
1,
"Hide replaced transactions from will detail and list",
)
add_widget(
grid,
"Hide Invalidated",
heir_hide_invalidated,
2,
"Hide invalidated transactions from will detail and list",
)
add_widget(
grid,
"Calendar App",
BalLineEdit(self.CALENDAR_APP),
3,
"Default app used to open calendar",
)
add_widget(
grid,
"Event summary",
BalLineEdit(self.EVENT_SUMMARY),
4,
(
"Default message to be used in event summary\n"
"Variables:\n"
" $wallet_name: name of wallet\n"
" $heirs_complete: list of heirs name,address,amount\n"
#" $will_details_complete: will details(id transaction, mining fees, willexecutor, willexecutor fees, locktime)\n"
)
)
add_widget(
grid,
"Event sescription",
BalTextEdit(self.EVENT_DESCRIPTION),
5,
(
"Default message to be used in event description\n"
"Variables:\n"
" $wallet_name: name of wallet\n"
" $heirs_complete: list of heirs name,address,amount\n"
#" $will_details_complete: will details(id transaction, mining fees, willexecutor, willexecutor fees, locktime)\n"
)
)
#add_widget(grid, "Bal Mode", bal_mode, 4, "choose bal mode")
# add_widget(
# grid,
# "Ping Willexecutors",
# heir_ping_willexecutors,
# 3,
# "Ping willexecutors to get payment info before compiling will",
# )
# add_widget(
# grid,
# " - Ask before",
# heir_ask_ping_willexecutors,
# 4,
# "Ask before to ping willexecutor",
# )
# add_widget(
# grid,
# "Backup Transaction",
# heir_no_willexecutor,
# 5,
# "Add transactions without willexecutor",
# )
# add_widget(grid,"Enable Multiverse(EXPERIMENTAL/BROKEN)",heir_enable_multiverse,6,"enable multiple locktimes, will import.... ")
grid.addWidget(heir_repush, 7, 0)
grid.addWidget(
HelpButton(
"Broadcast all transactions to willexecutors including those already pushed"
),
7,
2,
)
if ret := bool(show_modal(d)):
try:
self.update_all()
return ret
except Exception:
pass
return False
def broadcast_transactions(self, force):
for _k, w in self.bal_windows.items():
w.broadcast_transactions(force)
def update_all(self):
for _k, w in self.bal_windows.items():
w.update_all()
def get_window_title(self, title):
return _("BAL - ") + _(title)