forked from bitcoinafterlife/bal-electrum-plugin
1269 lines
46 KiB
Python
1269 lines
46 KiB
Python
"""
|
|
bal.gui.qt.lists
|
|
================
|
|
|
|
Tree/list views (subclasses of Electrum's ``MyTreeView``) and their toolbars.
|
|
|
|
* HeirListWidget - editable list of heirs (address / amount / locktime).
|
|
* PreviewList - preview of the will transactions before signing.
|
|
* WillExecutorListWidget- list of will-executor servers.
|
|
* WillExecutorWidget - container combining the list with add/import buttons.
|
|
|
|
These views call back into the :class:`BalWindow` controller (passed at
|
|
construction) for all business actions, so the heavy logic stays in ``window``
|
|
and ``dialogs``.
|
|
"""
|
|
|
|
from .common import *
|
|
from .common import _, _logger # underscore names are not re-exported by "import *"
|
|
from .widgets import BalCheckBox, PercAmountEdit, WillSettingsWidget
|
|
from PyQt6.QtWidgets import QMessageBox
|
|
from PyQt6.QtWidgets import QStyledItemDelegate, QLineEdit as _QLineEdit
|
|
from .dialogs import BalBuildWillDialog, BalDialog
|
|
|
|
|
|
class HeirListWidget(MyTreeView, MessageBoxMixin):
|
|
class Columns(MyTreeView.BaseColumnsEnum):
|
|
NAME = enum.auto()
|
|
ADDRESS = enum.auto()
|
|
AMOUNT = enum.auto()
|
|
|
|
headers = {
|
|
Columns.NAME: _("Name"),
|
|
Columns.ADDRESS: _("Address"),
|
|
Columns.AMOUNT: _("Amount"),
|
|
}
|
|
filter_columns = [Columns.NAME, Columns.ADDRESS]
|
|
|
|
ROLE_SORT_ORDER = Qt.ItemDataRole.UserRole + 1000
|
|
|
|
ROLE_HEIR_KEY = Qt.ItemDataRole.UserRole + 4000
|
|
key_role = ROLE_HEIR_KEY
|
|
|
|
def createEditor(self, parent, option, index):
|
|
return QLineEdit(parent)
|
|
|
|
def setEditorData(self, editor, index):
|
|
editor.setText(index.data())
|
|
|
|
def setModelData(self, editor, model, index):
|
|
model.setData(index, editor.text())
|
|
|
|
def __init__(self, bal_window: "BalWindow", parent):
|
|
super().__init__(
|
|
parent=parent,
|
|
main_window=bal_window.window,
|
|
stretch_column=self.Columns.NAME,
|
|
editable_columns=[
|
|
self.Columns.NAME,
|
|
self.Columns.ADDRESS,
|
|
self.Columns.AMOUNT,
|
|
],
|
|
)
|
|
self.decimal_point = bal_window.window.get_decimal_point()
|
|
self.bal_window = bal_window
|
|
|
|
try:
|
|
self.setModel(QStandardItemModel(self))
|
|
self.sortByColumn(self.Columns.NAME, Qt.SortOrder.AscendingOrder)
|
|
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
|
except Exception:
|
|
pass
|
|
|
|
self.setSortingEnabled(True)
|
|
self.std_model = self.model()
|
|
|
|
self.update()
|
|
|
|
def on_activated(self, idx):
|
|
self.on_double_click(idx)
|
|
|
|
def on_double_click(self, idx):
|
|
edit_key = self.get_edit_key_from_coordinate(idx.row(), idx.column())
|
|
self.bal_window.heirs.get(edit_key)
|
|
self.bal_window.new_heir_dialog(edit_key)
|
|
|
|
def on_edited(self, idx, edit_key, *, text):
|
|
original = prior_name = self.bal_window.heirs.get(edit_key)
|
|
if not prior_name:
|
|
return
|
|
col = idx.column()
|
|
try:
|
|
if col == 2:
|
|
text = Util.encode_amount(text, self.decimal_point)
|
|
elif col == 0:
|
|
self.bal_window.delete_heirs([edit_key])
|
|
edit_key = text
|
|
prior_name[col - 1] = text
|
|
prior_name.insert(0, edit_key)
|
|
prior_name = tuple(prior_name)
|
|
except Exception:
|
|
prior_name = (
|
|
(edit_key,) + prior_name[: col - 1] + (text,) + prior_name[col:]
|
|
)
|
|
|
|
try:
|
|
self.bal_window.set_heir(prior_name)
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
self.bal_window.set_heir((edit_key,) + original)
|
|
except Exception:
|
|
self.update()
|
|
|
|
def delete_heirs(self, selected_keys):
|
|
self.bal_window.delete_heirs(selected_keys)
|
|
self.update()
|
|
|
|
def create_menu(self, position):
|
|
menu = QMenu()
|
|
idx = self.indexAt(position)
|
|
column = idx.column() or self.Columns.NAME
|
|
selected_keys = []
|
|
for s_idx in self.selected_in_column(self.Columns.NAME):
|
|
sel_key = self.model().itemFromIndex(s_idx).data(0)
|
|
selected_keys.append(sel_key)
|
|
if selected_keys and idx.isValid():
|
|
column_title = self.model().horizontalHeaderItem(column).text()
|
|
# ok
|
|
column_data = "\n".join(
|
|
self.model().itemFromIndex(s_idx).text()
|
|
for s_idx in self.selected_in_column(column)
|
|
)
|
|
menu.addAction(
|
|
_("Copy {}").format(column_title),
|
|
lambda: self.place_text_on_clipboard(column_data, title=column_title),
|
|
)
|
|
if column in self.editable_columns:
|
|
item = self.model().itemFromIndex(idx)
|
|
if item.isEditable():
|
|
persistent = QPersistentModelIndex(idx)
|
|
menu.addAction(
|
|
_("Edit {}").format(column_title),
|
|
lambda p=persistent: self.edit(QModelIndex(p)),
|
|
)
|
|
menu.addAction(_("Delete"), lambda: self.delete_heirs(selected_keys))
|
|
menu.exec(self.viewport().mapToGlobal(position))
|
|
|
|
def update(self):
|
|
current_key = self.get_role_data_for_current_item(
|
|
col=self.Columns.NAME, role=self.ROLE_HEIR_KEY
|
|
)
|
|
self.model().clear()
|
|
self.update_headers(self.__class__.headers)
|
|
set_current = None
|
|
for key in sorted(self.bal_window.heirs.keys()):
|
|
heir = self.bal_window.heirs[key]
|
|
labels = [""] * len(self.Columns)
|
|
labels[self.Columns.NAME] = key
|
|
labels[self.Columns.ADDRESS] = heir[0]
|
|
labels[self.Columns.AMOUNT] = Util.decode_amount(
|
|
heir[1], self.decimal_point
|
|
)
|
|
|
|
items = [QStandardItem(x) for x in labels]
|
|
items[self.Columns.NAME].setEditable(True)
|
|
items[self.Columns.ADDRESS].setEditable(True)
|
|
items[self.Columns.AMOUNT].setEditable(True)
|
|
items[self.Columns.NAME].setData(
|
|
key, self.ROLE_HEIR_KEY + self.Columns.NAME
|
|
)
|
|
items[self.Columns.ADDRESS].setData(
|
|
key, self.ROLE_HEIR_KEY + self.Columns.ADDRESS
|
|
)
|
|
items[self.Columns.AMOUNT].setData(
|
|
key, self.ROLE_HEIR_KEY + self.Columns.AMOUNT
|
|
)
|
|
|
|
row_count = self.model().rowCount()
|
|
self.model().insertRow(row_count, items)
|
|
|
|
if key == current_key:
|
|
idx = self.model().index(row_count, self.Columns.NAME)
|
|
set_current = QPersistentModelIndex(idx)
|
|
try:
|
|
self.will_settings_widget.on_locktime_change()
|
|
except Exception as e:
|
|
pass
|
|
self.set_current_idx(set_current)
|
|
# FIXME refresh loses sort order; so set "default" here:
|
|
self.filter()
|
|
|
|
def refresh_row(self, key, row):
|
|
# nothing to update here
|
|
pass
|
|
|
|
def get_edit_key_from_coordinate(self, row, col):
|
|
a = self.get_role_data_from_coordinate(row, col, role=self.ROLE_HEIR_KEY + col)
|
|
return a
|
|
|
|
def create_toolbar(self, config):
|
|
toolbar, menu = self.create_toolbar_with_menu("")
|
|
menu.addAction(_("&New Heir"), self.bal_window.new_heir_dialog)
|
|
menu.addAction(_("Import"), self.bal_window.import_heirs)
|
|
menu.addAction(_("Export"), lambda: self.bal_window.export_heirs())
|
|
|
|
newHeirButton = QPushButton(_("New Heir"))
|
|
newHeirButton.clicked.connect(self.bal_window.new_heir_dialog)
|
|
|
|
widget = QWidget(self)
|
|
layout = QHBoxLayout(widget)
|
|
self.will_settings_widget = WillSettingsWidget(self.bal_window, self)
|
|
|
|
layout.addWidget(self.will_settings_widget)
|
|
layout.addWidget(newHeirButton)
|
|
|
|
toolbar.insertWidget(2, widget)
|
|
|
|
return toolbar
|
|
|
|
def build_transactions(self):
|
|
# will = self.bal_window.prepare_will()
|
|
self.bal_window.prepare_will()
|
|
|
|
|
|
|
|
class PreviewList(MyTreeView, MessageBoxMixin):
|
|
class Columns(MyTreeView.BaseColumnsEnum):
|
|
LOCKTIME = enum.auto()
|
|
TXID = enum.auto()
|
|
WILLEXECUTOR = enum.auto()
|
|
STATUS = enum.auto()
|
|
SERVER = enum.auto()
|
|
|
|
headers = {
|
|
Columns.LOCKTIME: _("Locktime"),
|
|
Columns.TXID: _("Txid"),
|
|
Columns.WILLEXECUTOR: _("Will-Executor"),
|
|
Columns.STATUS: _("Status"),
|
|
Columns.SERVER: _("Server"),
|
|
}
|
|
|
|
ROLE_HEIR_KEY = Qt.ItemDataRole.UserRole + 2000
|
|
key_role = ROLE_HEIR_KEY
|
|
|
|
def createEditor(self, parent, option, index):
|
|
return QLineEdit(parent)
|
|
|
|
def setEditorData(self, editor, index):
|
|
editor.setText(index.data())
|
|
|
|
def setModelData(self, editor, model, index):
|
|
model.setData(index, editor.text())
|
|
|
|
def __init__(self, bal_window: "BalWindow", parent, will):
|
|
super().__init__(
|
|
parent=parent,
|
|
main_window=bal_window.window,
|
|
stretch_column=self.Columns.TXID,
|
|
)
|
|
# self._bal_parent = parent
|
|
self.bal_window = bal_window
|
|
self.decimal_point = bal_window.window.get_decimal_point
|
|
|
|
if will is not None:
|
|
self.will = will
|
|
else:
|
|
self.will = bal_window.willitems
|
|
|
|
try:
|
|
self.setModel(QStandardItemModel(self))
|
|
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
|
self.sortByColumn(self.Columns.NAME, Qt.SortOrder.AscendingOrder)
|
|
except Exception as e:
|
|
pass
|
|
|
|
self.setSortingEnabled(True)
|
|
self.std_model = self.model()
|
|
|
|
self.update()
|
|
|
|
def on_activated(self, idx):
|
|
self.on_double_click(idx)
|
|
|
|
def on_double_click(self, idx):
|
|
idx = self.model().index(idx.row(), self.Columns.TXID)
|
|
sel_key = self.model().itemFromIndex(idx).data(0)
|
|
self.show_transaction([sel_key])
|
|
|
|
def create_menu(self, position):
|
|
menu = QMenu()
|
|
idx = self.indexAt(position)
|
|
column = idx.column() or self.Columns.TXID
|
|
selected_keys = []
|
|
for s_idx in self.selected_in_column(self.Columns.TXID):
|
|
sel_key = self.model().itemFromIndex(s_idx).data(0)
|
|
selected_keys.append(sel_key)
|
|
if selected_keys and idx.isValid():
|
|
column_title = self.model().horizontalHeaderItem(column).text()
|
|
# column_data = "\n".join(
|
|
# self.model().itemFromIndex(s_idx).text()
|
|
# for s_idx in self.selected_in_column(column)
|
|
# )
|
|
|
|
menu.addAction(
|
|
_("details").format(column_title),
|
|
lambda: self.show_transaction(selected_keys),
|
|
).setEnabled(len(selected_keys) < 2)
|
|
menu.addAction(
|
|
_("check ").format(column_title),
|
|
lambda: self.check_transactions(selected_keys),
|
|
)
|
|
if self.bal_window.bal_plugin.ENABLE_MULTIVERSE.get():
|
|
try:
|
|
self.importaction = self.menu.addAction(
|
|
_("Import"), self.import_will
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
menu.addSeparator()
|
|
menu.addAction(
|
|
_("delete").format(column_title), lambda: self.delete(selected_keys)
|
|
)
|
|
|
|
menu.exec(self.viewport().mapToGlobal(position))
|
|
|
|
def delete(self, selected_keys):
|
|
for key in selected_keys:
|
|
del self.will[key]
|
|
try:
|
|
del self.bal_window.willitems[key]
|
|
except Exception:
|
|
pass
|
|
try:
|
|
del self.bal_window.will[key]
|
|
except Exception:
|
|
pass
|
|
self.update()
|
|
|
|
def check_transactions(self, selected_keys):
|
|
wout = {}
|
|
for k in selected_keys:
|
|
wout[k] = self.will[k]
|
|
if wout:
|
|
self.bal_window.check_transactions(wout)
|
|
self.update()
|
|
|
|
def show_transaction(self, selected_keys):
|
|
for key in selected_keys:
|
|
self.bal_window.show_transaction(self.will[key].tx)
|
|
|
|
self.update()
|
|
|
|
def select(self, selected_keys):
|
|
self.selected += selected_keys
|
|
self.update()
|
|
|
|
def deselect(self, selected_keys):
|
|
for key in selected_keys:
|
|
self.selected.remove(key)
|
|
self.update()
|
|
|
|
def update_will(self, will):
|
|
self.will.update(will)
|
|
self.update()
|
|
|
|
def replace(self, set_current, current_key, txid, bal_tx):
|
|
if self.bal_window.bal_plugin._hide_replaced and bal_tx.get_status("REPLACED"):
|
|
return False
|
|
if self.bal_window.bal_plugin._hide_invalidated and bal_tx.get_status(
|
|
"INVALIDATED"
|
|
):
|
|
return False
|
|
|
|
if not isinstance(bal_tx, WillItem):
|
|
bal_tx = WillItem(bal_tx)
|
|
|
|
tx = bal_tx.tx
|
|
|
|
labels = [""] * len(self.Columns)
|
|
labels[self.Columns.LOCKTIME] = str(BalTimestamp(tx.locktime))
|
|
labels[self.Columns.TXID] = txid
|
|
we = "None"
|
|
if bal_tx.we:
|
|
we = bal_tx.we["url"]
|
|
labels[self.Columns.WILLEXECUTOR] = we
|
|
status = bal_tx.status
|
|
if len(bal_tx.status) > 53:
|
|
status = "...{}".format(status[-50:])
|
|
labels[self.Columns.STATUS] = status
|
|
# Dedicated, always-readable label describing whether the inheritance
|
|
# transaction is actually stored on the will-executor servers.
|
|
labels[self.Columns.SERVER] = server_status_text(bal_tx)
|
|
|
|
items = []
|
|
for e in labels:
|
|
if isinstance(e, list):
|
|
try:
|
|
items.append(QStandardItem(*e))
|
|
except Exception as e:
|
|
pass
|
|
else:
|
|
items.append(QStandardItem(str(e)))
|
|
|
|
items[-1].setBackground(QColor(status_color(bal_tx)))
|
|
|
|
# Group C / C6: emphasise the Locktime column by rendering it in bold,
|
|
# so the delivery time stands out at a glance in the list.
|
|
try:
|
|
locktime_item = items[self.Columns.LOCKTIME]
|
|
bold_font = locktime_item.font()
|
|
bold_font.setBold(True)
|
|
locktime_item.setFont(bold_font)
|
|
except Exception as bold_err:
|
|
_logger.debug(f"locktime bold error: {bold_err}")
|
|
|
|
# Tooltip on the Server column: shows the will-executor URL (if any)
|
|
# plus the current server state, so the user can always inspect details.
|
|
try:
|
|
items[self.Columns.SERVER].setToolTip(server_status_tooltip(bal_tx))
|
|
except Exception as tip_err:
|
|
_logger.debug(f"server tooltip error: {tip_err}")
|
|
|
|
row_count = self.model().rowCount()
|
|
self.model().insertRow(row_count, items)
|
|
if txid == current_key:
|
|
idx = self.model().index(row_count, self.Columns.TXID)
|
|
set_current = QPersistentModelIndex(idx)
|
|
self.set_current_idx(set_current)
|
|
return set_current
|
|
|
|
def update(self):
|
|
try:
|
|
self.menu.removeAction(self.importaction)
|
|
except Exception:
|
|
pass
|
|
|
|
if self.will is None:
|
|
return
|
|
|
|
current_key = self.get_role_data_for_current_item(
|
|
col=self.Columns.TXID, role=self.ROLE_HEIR_KEY
|
|
)
|
|
self.model().clear()
|
|
self.update_headers(self.__class__.headers)
|
|
|
|
set_current = None
|
|
for txid, bal_tx in self.will.items():
|
|
tmp = self.replace(set_current, current_key, txid, bal_tx)
|
|
if tmp:
|
|
set_current = tmp
|
|
self.sortByColumn(self.Columns.LOCKTIME, Qt.SortOrder.AscendingOrder)
|
|
self.setSortingEnabled(True)
|
|
try:
|
|
self.will_settings_widget.on_locktime_change()
|
|
except Exception as _e:
|
|
pass
|
|
|
|
def create_toolbar(self, config):
|
|
toolbar, menu = self.create_toolbar_with_menu("")
|
|
menu.addAction(_("Prepare"), self.build_transactions)
|
|
menu.addAction(_("Display"), self.bal_window.preview_modal_dialog)
|
|
menu.addAction(_("Sign"), self.ask_password_and_sign_transactions)
|
|
menu.addAction(_("Export"), self.export_will)
|
|
if self.bal_window.bal_plugin.ENABLE_MULTIVERSE.get():
|
|
self.importaction = menu.addAction(_("Import"), self.import_will)
|
|
menu.addAction(_("Broadcast"), self.broadcast)
|
|
menu.addAction(_("Check"), self.check)
|
|
menu.addAction(_("Invalidate"), self.invalidate_will)
|
|
|
|
# The Wizard is the main entry point to create an inheritance, so make
|
|
# it stand out: show a bold label next to a slightly larger icon (the
|
|
# plain icon-only button was too easy to overlook).
|
|
wizard = QPushButton(" " + _("Build Your Will"))
|
|
wizard.setIcon(
|
|
read_QIcon_from_bytes(
|
|
self.bal_window.bal_plugin.read_file("icons/wizard.png")
|
|
)
|
|
)
|
|
wizard.setIconSize(QSize(28, 28))
|
|
wizard.setMinimumHeight(40)
|
|
wizard.setStyleSheet("QPushButton{font-weight:bold;}")
|
|
# Tooltip so the button is self-explanatory when hovered.
|
|
wizard.setToolTip(_("Wizard - Build your will"))
|
|
wizard.clicked.connect(self.bal_window.init_wizard)
|
|
# display = QPushButton(_("Display"))
|
|
# display.clicked.connect(self.bal_window.preview_modal_dialog)
|
|
|
|
refresh = QPushButton()
|
|
refresh.setIcon(
|
|
read_QIcon_from_bytes(
|
|
self.bal_window.bal_plugin.read_file("icons/reload.png")
|
|
)
|
|
)
|
|
# Tooltip so the icon is self-explanatory when hovered. "Check
|
|
# Inheritance" makes it clear the button re-checks the inheritance/will
|
|
# state (not a generic refresh).
|
|
refresh.setToolTip(_("Check Inheritance"))
|
|
refresh.clicked.connect(self.check)
|
|
|
|
widget = QWidget(self)
|
|
hlayout = QHBoxLayout(widget)
|
|
hlayout.setContentsMargins(0, 0, 0, 0)
|
|
self.will_settings_widget = WillSettingsWidget(self.bal_window, self)
|
|
# Toolbar order (left -> right):
|
|
# Wizard | Delivery time | Check Alive | Calendar | Check (refresh)
|
|
# The Wizard button goes first (leftmost); the settings widget already
|
|
# lays out delivery/check-alive/calendar in that order internally.
|
|
hlayout.addWidget(wizard)
|
|
hlayout.addWidget(self.will_settings_widget)
|
|
hlayout.addWidget(refresh)
|
|
toolbar.insertWidget(2, widget)
|
|
|
|
self.menu = menu
|
|
self.toolbar = toolbar
|
|
return toolbar
|
|
|
|
def hide_replaced(self):
|
|
self.bal_window.bal_plugin.hide_replaced()
|
|
self.update()
|
|
|
|
def hide_invalidated(self):
|
|
self.bal_window.bal_plugin.hide_invalidated()
|
|
self.update()
|
|
|
|
def build_transactions(self):
|
|
will = self.bal_window.prepare_will()
|
|
if will:
|
|
self.update_will(will)
|
|
|
|
def export_json_file(self, path):
|
|
write_json_file(path, self.will)
|
|
|
|
def export_will(self):
|
|
self.bal_window.export_will()
|
|
self.update()
|
|
|
|
def import_will(self):
|
|
self.bal_window.import_will()
|
|
|
|
def ask_password_and_sign_transactions(self):
|
|
self.bal_window.ask_password_and_sign_transactions(callback=self.update)
|
|
|
|
def broadcast(self):
|
|
self.bal_window.broadcast_transactions()
|
|
self.update()
|
|
|
|
def check(self):
|
|
close_window = BalBuildWillDialog(self.bal_window)
|
|
close_window.build_will_task()
|
|
|
|
will = {}
|
|
for wid, w in self.bal_window.willitems.items():
|
|
# Query the will-executor server for every valid will that HAS a
|
|
# will-executor assigned and is not yet CHECKED. Previously only
|
|
# transactions already marked PUSHED were checked, so a will that
|
|
# had actually been sent in the past but whose saved status still
|
|
# read "New" (not PUSHED) was skipped and the Check button reported
|
|
# "nothing to do". Will.needs_server_check now also includes such
|
|
# non-PUSHED wills, so the server can confirm the transaction is
|
|
# present and correct the status (see set_check_willexecutor).
|
|
if Will.needs_server_check(w):
|
|
will[wid] = w
|
|
if will:
|
|
self.bal_window.check_transactions(will)
|
|
self.update()
|
|
# NOTE (Group B / B2): signing + broadcasting is already performed
|
|
# automatically by BalBuildWillDialog.build_will_task() above (called at
|
|
# the start of check()). We must NOT trigger a second sign/broadcast
|
|
# cycle here, otherwise the will would be broadcast twice. Whether the
|
|
# automatic sign/broadcast runs silently or shows the manual "next step"
|
|
# hints is controlled by the AUTO_SIGN setting inside that dialog.
|
|
|
|
def invalidate_will(self):
|
|
self.bal_window.invalidate_will()
|
|
self.update()
|
|
|
|
|
|
# class PreviewDialog(BalDialog, MessageBoxMixin):
|
|
# def __init__(self, bal_window, will):
|
|
# self._bal_parent = bal_window.window
|
|
# BalDialog.__init__(
|
|
# self, bal_window=bal_window, bal_plugin=bal_window.bal_plugin
|
|
# )
|
|
# self.bal_plugin = bal_window.bal_plugin
|
|
# self.gui_object = self.bal_plugin.gui_object
|
|
# self.config = self.bal_plugin.config
|
|
# self.bal_window = bal_window
|
|
# self.wallet = bal_window.window.wallet
|
|
# self.format_amount = bal_window.window.format_amount
|
|
# self.base_unit = bal_window.window.base_unit
|
|
# self.format_fiat_and_units = bal_window.window.format_fiat_and_units
|
|
# self.fx = bal_window.window.fx
|
|
# self.format_fee_rate = bal_window.window.format_fee_rate
|
|
# self.show_address = bal_window.window.show_address
|
|
# if not will:
|
|
# self.will = bal_window.willitems
|
|
# else:
|
|
# self.will = will
|
|
# self.setWindowTitle(_("Transactions Preview"))
|
|
# self.setMinimumSize(1000, 200)
|
|
# self.size_label = QLabel()
|
|
# self.transactions_list = PreviewList(self.bal_window,self, self.will)
|
|
#
|
|
# try:
|
|
# self.bal_window.init_class_variables()
|
|
# except Exception as e:
|
|
# _logger.error(f"PreviewDialog Exception: {e}")
|
|
# self.check_will()
|
|
#
|
|
# vbox = QVBoxLayout(self)
|
|
# vbox.addWidget(self.size_label)
|
|
# vbox.addWidget(self.transactions_list)
|
|
# buttonbox = QHBoxLayout()
|
|
#
|
|
# b = QPushButton(_("Sign"))
|
|
# b.clicked.connect(self.transactions_list.ask_password_and_sign_transactions)
|
|
# buttonbox.addWidget(b)
|
|
#
|
|
# b = QPushButton(_("Export Will"))
|
|
# b.clicked.connect(self.transactions_list.export_will)
|
|
# buttonbox.addWidget(b)
|
|
#
|
|
# b = QPushButton(_("Broadcast"))
|
|
# b.clicked.connect(self.transactions_list.broadcast)
|
|
# buttonbox.addWidget(b)
|
|
#
|
|
# b = QPushButton(_("Invalidate will"))
|
|
# b.clicked.connect(self.transactions_list.invalidate_will)
|
|
# buttonbox.addWidget(b)
|
|
#
|
|
# vbox.addLayout(buttonbox)
|
|
#
|
|
# self.update()
|
|
#
|
|
# def update_will(self, will):
|
|
# self.will.update(will)
|
|
# self.transactions_list.update_will(will)
|
|
# self.update()
|
|
#
|
|
# def update(self):
|
|
# self.transactions_list.update()
|
|
#
|
|
# def is_hidden(self):
|
|
# return self.isMinimized() or self.isHidden()
|
|
#
|
|
# def show_or_hide(self):
|
|
# if self.is_hidden():
|
|
# self.bring_to_top()
|
|
# else:
|
|
# self.hide()
|
|
#
|
|
# def bring_to_top(self):
|
|
# self.show()
|
|
# self.raise_()
|
|
#
|
|
# def closeEvent(self, event):
|
|
# event.accept()
|
|
|
|
|
|
|
|
class _FullUrlEditDelegate(QStyledItemDelegate):
|
|
"""Item delegate for the will-executor URL column.
|
|
|
|
The URL column may display a SHORTENED form of long .onion addresses. This
|
|
delegate ensures editing operates on the FULL url: the editor is preloaded
|
|
from the real key role (which always holds the complete address) rather than
|
|
from the visible (possibly shortened) cell text.
|
|
"""
|
|
|
|
def __init__(self, url_role, parent=None):
|
|
super().__init__(parent)
|
|
self._url_role = url_role
|
|
|
|
def createEditor(self, parent, option, index):
|
|
return _QLineEdit(parent)
|
|
|
|
def setEditorData(self, editor, index):
|
|
# Prefer the full URL stored in the key role; fall back to the visible
|
|
# text if for some reason the role is missing.
|
|
full = index.data(self._url_role)
|
|
if not full:
|
|
full = index.data()
|
|
editor.setText(full or "")
|
|
|
|
def setModelData(self, editor, model, index):
|
|
model.setData(index, editor.text())
|
|
|
|
|
|
class WillExecutorListWidget(MyTreeView):
|
|
class Columns(MyTreeView.BaseColumnsEnum):
|
|
SELECTED = enum.auto()
|
|
URL = enum.auto()
|
|
STATUS = enum.auto()
|
|
BASE_FEE = enum.auto()
|
|
INFO = enum.auto()
|
|
ADDRESS = enum.auto()
|
|
|
|
headers = {
|
|
Columns.SELECTED: _(""),
|
|
Columns.URL: _("Url"),
|
|
Columns.STATUS: _("S"),
|
|
Columns.BASE_FEE: _("Base fee"),
|
|
Columns.INFO: _("Info"),
|
|
Columns.ADDRESS: _("Default Address"),
|
|
}
|
|
|
|
filter_columns = [Columns.URL]
|
|
|
|
ROLE_SORT_ORDER = Qt.ItemDataRole.UserRole + 3000
|
|
ROLE_HEIR_KEY = Qt.ItemDataRole.UserRole + 3001
|
|
key_role = ROLE_HEIR_KEY
|
|
|
|
def __init__(self, parent: "WillExecutorWidget"):
|
|
super().__init__(
|
|
parent=parent,
|
|
stretch_column=self.Columns.ADDRESS,
|
|
editable_columns=[
|
|
self.Columns.URL,
|
|
self.Columns.BASE_FEE,
|
|
self.Columns.ADDRESS,
|
|
self.Columns.INFO,
|
|
],
|
|
)
|
|
self._bal_parent = parent
|
|
try:
|
|
self.setModel(QStandardItemModel(self))
|
|
self.sortByColumn(self.Columns.SELECTED, Qt.SortOrder.AscendingOrder)
|
|
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
|
except Exception:
|
|
pass
|
|
self.setSortingEnabled(True)
|
|
self.std_model = self.model()
|
|
self.config = parent.bal_plugin.config
|
|
self.get_decimal_point = parent.bal_plugin.get_decimal_point
|
|
|
|
# The URL column may DISPLAY a shortened form of long .onion addresses
|
|
# (see update()). Without this, double-clicking to edit would load the
|
|
# shortened text (with the ellipsis) into the editor and saving it would
|
|
# corrupt the will-executor key. This delegate makes the editor load the
|
|
# FULL url from the real key role instead of the visible (shortened)
|
|
# text, so edits always operate on the complete address.
|
|
try:
|
|
self.setItemDelegateForColumn(
|
|
self.Columns.URL,
|
|
_FullUrlEditDelegate(self.ROLE_HEIR_KEY + self.Columns.URL, self),
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
self.update()
|
|
|
|
def create_menu(self, position):
|
|
menu = QMenu()
|
|
idx = self.indexAt(position)
|
|
column = idx.column() or self.Columns.URL
|
|
selected_keys = []
|
|
for s_idx in self.selected_in_column(self.Columns.URL):
|
|
item = self.model().itemFromIndex(s_idx)
|
|
# Use the FULL url stored in the key role, NOT item.data(0): the
|
|
# latter is the (possibly shortened) DISPLAY text, and using it as a
|
|
# dict key breaks delete/select/deselect/ping for long .onion URLs
|
|
# (KeyError). Fall back to the display text only if the role is
|
|
# unexpectedly missing.
|
|
sel_key = item.data(self.ROLE_HEIR_KEY + self.Columns.URL) or item.data(0)
|
|
selected_keys.append(sel_key)
|
|
if selected_keys and idx.isValid():
|
|
column_title = self.model().horizontalHeaderItem(column).text()
|
|
# column_data = "\n".join(
|
|
# self.model().itemFromIndex(s_idx).text()
|
|
# for s_idx in self.selected_in_column(column)
|
|
# )
|
|
if Willexecutors.is_selected(self._bal_parent.willexecutors_list[sel_key]):
|
|
menu.addAction(
|
|
_("deselect").format(column_title),
|
|
lambda: self.deselect(selected_keys),
|
|
)
|
|
else:
|
|
menu.addAction(
|
|
_("select").format(column_title), lambda: self.select(selected_keys)
|
|
)
|
|
if column in self.editable_columns:
|
|
item = self.model().itemFromIndex(idx)
|
|
if item.isEditable():
|
|
persistent = QPersistentModelIndex(idx)
|
|
menu.addAction(
|
|
_("Edit {}").format(column_title),
|
|
lambda p=persistent: self.edit(QModelIndex(p)),
|
|
)
|
|
|
|
menu.addAction(
|
|
_("Ping").format(column_title),
|
|
lambda: self.ping_willexecutors(selected_keys),
|
|
)
|
|
menu.addSeparator()
|
|
menu.addAction(
|
|
_("delete").format(column_title), lambda: self.delete(selected_keys)
|
|
)
|
|
|
|
menu.exec(self.viewport().mapToGlobal(position))
|
|
|
|
def ping_willexecutors(self, selected_keys):
|
|
wout = {}
|
|
for k in selected_keys:
|
|
wout[k] = self._bal_parent.willexecutors_list[k]
|
|
self._bal_parent.update_willexecutors(wout)
|
|
|
|
self._bal_parent.save_willexecutors()
|
|
self.update()
|
|
|
|
def on_activated(self, idx):
|
|
self.on_double_click(idx)
|
|
|
|
def on_double_click(self, idx):
|
|
edit_key = self.get_edit_key_from_coordinate(
|
|
idx.row(), self.Columns.URL
|
|
)
|
|
if edit_key and edit_key in self._bal_parent.willexecutors_list:
|
|
self._bal_parent.add(edit_key)
|
|
|
|
def get_edit_key_from_coordinate(self, row, col):
|
|
role = self.ROLE_HEIR_KEY + col
|
|
a = self.get_role_data_from_coordinate(row, col, role=role)
|
|
return a
|
|
|
|
def delete(self, selected_keys):
|
|
for key in selected_keys:
|
|
del self._bal_parent.willexecutors_list[key]
|
|
|
|
self._bal_parent.save_willexecutors()
|
|
self.update()
|
|
|
|
def select(self, selected_keys):
|
|
for wid, w in self._bal_parent.willexecutors_list.items():
|
|
if wid in selected_keys:
|
|
w["selected"] = True
|
|
self._bal_parent.save_willexecutors()
|
|
self.update()
|
|
|
|
def deselect(self, selected_keys):
|
|
for wid, w in self._bal_parent.willexecutors_list.items():
|
|
if wid in selected_keys:
|
|
w["selected"] = False
|
|
self._bal_parent.save_willexecutors()
|
|
self.update()
|
|
|
|
def on_edited(self, idx, edit_key, *, text):
|
|
# prior_name = self._bal_parent.willexecutors_list[edit_key]
|
|
col = idx.column()
|
|
try:
|
|
if col == self.Columns.URL:
|
|
self._bal_parent.willexecutors_list[text] = self._bal_parent.willexecutors_list[
|
|
edit_key
|
|
]
|
|
del self._bal_parent.willexecutors_list[edit_key]
|
|
if col == self.Columns.BASE_FEE:
|
|
self._bal_parent.willexecutors_list[edit_key]["base_fee"] = (
|
|
Util.encode_amount(text, self.get_decimal_point())
|
|
)
|
|
if col == self.Columns.ADDRESS:
|
|
self._bal_parent.willexecutors_list[edit_key]["address"] = text
|
|
if col == self.Columns.INFO:
|
|
self._bal_parent.willexecutors_list[edit_key]["info"] = text
|
|
self._bal_parent.save_willexecutors()
|
|
self.update()
|
|
except Exception:
|
|
pass
|
|
|
|
def update(self):
|
|
if self._bal_parent.willexecutors_list is None:
|
|
return
|
|
try:
|
|
current_key = self.get_role_data_for_current_item(
|
|
col=self.Columns.URL, role=self.ROLE_HEIR_KEY
|
|
)
|
|
self.model().clear()
|
|
self.update_headers(self.__class__.headers)
|
|
|
|
set_current = None
|
|
|
|
for url, value in self._bal_parent.willexecutors_list.items():
|
|
labels = [""] * len(self.Columns)
|
|
# Long Tor (.onion) URLs overflow the column; show a shortened
|
|
# form (first 37 chars + ellipsis, matching the welist site)
|
|
# while keeping the FULL url as the real key data (setData below)
|
|
# and in the tooltip, so nothing downstream breaks. Short URLs
|
|
# are shown unchanged.
|
|
display_url = url if len(url) <= 40 else url[:37] + "\u2026"
|
|
labels[self.Columns.URL] = display_url
|
|
if Willexecutors.is_selected(value):
|
|
|
|
labels[self.Columns.SELECTED] = [
|
|
read_QIcon_from_bytes(
|
|
self._bal_parent.bal_plugin.read_file("icons/confirmed.png")
|
|
),
|
|
"",
|
|
]
|
|
else:
|
|
labels[self.Columns.SELECTED] = ""
|
|
labels[self.Columns.BASE_FEE] = Util.decode_amount(
|
|
value.get("base_fee", 0), self.get_decimal_point()
|
|
)
|
|
if str(value.get("status", 0)) == "200":
|
|
labels[self.Columns.STATUS] = [
|
|
read_QIcon_from_bytes(
|
|
self._bal_parent.bal_plugin.read_file(
|
|
"icons/status_connected.png"
|
|
)
|
|
),
|
|
"",
|
|
]
|
|
else:
|
|
labels[self.Columns.STATUS] = [
|
|
read_QIcon_from_bytes(
|
|
self._bal_parent.bal_plugin.read_file("icons/unconfirmed.png")
|
|
),
|
|
"",
|
|
]
|
|
labels[self.Columns.ADDRESS] = str(value.get("address", ""))
|
|
labels[self.Columns.INFO] = str(value.get("info", ""))
|
|
|
|
items = []
|
|
for e in labels:
|
|
if isinstance(e, list):
|
|
try:
|
|
items.append(QStandardItem(*e))
|
|
except Exception as e:
|
|
pass
|
|
else:
|
|
items.append(QStandardItem(e))
|
|
items[self.Columns.SELECTED].setEditable(False)
|
|
items[self.Columns.URL].setEditable(True)
|
|
items[self.Columns.ADDRESS].setEditable(True)
|
|
items[self.Columns.INFO].setEditable(True)
|
|
items[self.Columns.BASE_FEE].setEditable(True)
|
|
items[self.Columns.STATUS].setEditable(False)
|
|
|
|
items[self.Columns.URL].setData(
|
|
url, self.ROLE_HEIR_KEY + self.Columns.URL
|
|
)
|
|
# Full URL on hover (the visible text may be shortened above).
|
|
items[self.Columns.URL].setToolTip(url)
|
|
items[self.Columns.BASE_FEE].setData(
|
|
url, self.ROLE_HEIR_KEY + self.Columns.BASE_FEE
|
|
)
|
|
items[self.Columns.INFO].setData(
|
|
url, self.ROLE_HEIR_KEY + self.Columns.INFO
|
|
)
|
|
items[self.Columns.ADDRESS].setData(
|
|
url, self.ROLE_HEIR_KEY + self.Columns.ADDRESS
|
|
)
|
|
row_count = self.model().rowCount()
|
|
self.model().insertRow(row_count, items)
|
|
if url == current_key:
|
|
idx = self.model().index(row_count, self.Columns.URL)
|
|
set_current = QPersistentModelIndex(idx)
|
|
self.set_current_idx(set_current)
|
|
self.filter()
|
|
except Exception as e:
|
|
_logger.error(f"error updating willexcutor {e}")
|
|
raise e
|
|
|
|
|
|
|
|
class WillExecutorWidget(QWidget, MessageBoxMixin):
|
|
def __init__(self, parent, bal_window, willexecutors=None):
|
|
self.bal_window = bal_window
|
|
self.bal_plugin = bal_window.bal_plugin
|
|
self._bal_parent = parent
|
|
MessageBoxMixin.__init__(self)
|
|
QWidget.__init__(self, parent)
|
|
if willexecutors:
|
|
self.willexecutors_list = willexecutors
|
|
else:
|
|
self.willexecutors_list = Willexecutors.get_willexecutors(self.bal_plugin)
|
|
|
|
self.size_label = QLabel()
|
|
self.will_executor_list_widget = WillExecutorListWidget(self)
|
|
|
|
vbox = QVBoxLayout(self)
|
|
vbox.addWidget(self.size_label)
|
|
|
|
widget = QWidget()
|
|
hbox = QHBoxLayout(widget)
|
|
hbox.addWidget(QLabel(_("Add transactions without willexecutor")))
|
|
heir_no_willexecutor = BalCheckBox(self.bal_plugin.NO_WILLEXECUTOR)
|
|
hbox.addWidget(heir_no_willexecutor)
|
|
spacer_widget = QWidget()
|
|
spacer_widget.setSizePolicy(
|
|
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
|
|
)
|
|
hbox.addWidget(spacer_widget)
|
|
vbox.addWidget(widget)
|
|
|
|
vbox.addWidget(self.will_executor_list_widget)
|
|
buttonbox = QHBoxLayout()
|
|
|
|
b = QPushButton(_("Add"))
|
|
b.clicked.connect(self.add)
|
|
buttonbox.addWidget(b)
|
|
|
|
b = QPushButton(_("Download List"))
|
|
b.clicked.connect(self.download_list)
|
|
buttonbox.addWidget(b)
|
|
|
|
b = QPushButton(_("Import"))
|
|
b.clicked.connect(self.import_file)
|
|
buttonbox.addWidget(b)
|
|
|
|
b = QPushButton(_("Export"))
|
|
b.clicked.connect(self.export_file)
|
|
buttonbox.addWidget(b)
|
|
|
|
b = QPushButton(_("Ping All"))
|
|
b.clicked.connect(self.update_willexecutors)
|
|
buttonbox.addWidget(b)
|
|
|
|
vbox.addLayout(buttonbox)
|
|
# self.will_executor_list_widget.update()
|
|
|
|
def add(self, edit_key=None):
|
|
executor = None
|
|
if edit_key:
|
|
executor = self.willexecutors_list.get(edit_key)
|
|
title = _("Edit: {}").format(edit_key)
|
|
else:
|
|
title = _("New Will Executor")
|
|
d = BalDialog(
|
|
self.bal_window.window,
|
|
self.bal_plugin,
|
|
self.bal_plugin.get_window_title(title),
|
|
)
|
|
|
|
vbox = QVBoxLayout(d)
|
|
grid = QGridLayout()
|
|
|
|
url_edit = QLineEdit()
|
|
url_edit.setFixedWidth(32 * char_width_in_lineedit())
|
|
info_edit = QLineEdit("New Will Executor")
|
|
info_edit.setFixedWidth(32 * char_width_in_lineedit())
|
|
base_fee_spin = QSpinBox()
|
|
base_fee_spin.setRange(0, 1000000)
|
|
base_fee_spin.setValue(0)
|
|
address_edit = QLineEdit()
|
|
address_edit.setFixedWidth(32 * char_width_in_lineedit())
|
|
sync_btn = QPushButton("\u21BB")
|
|
sync_btn.setFixedWidth(32)
|
|
loading_label = QLabel()
|
|
d._sync_active = False
|
|
|
|
if executor:
|
|
url_edit.setText(edit_key)
|
|
info_edit.setText(str(executor.get("info", "New Will Executor")))
|
|
base_fee_spin.setValue(int(executor.get("base_fee", 0)))
|
|
address_edit.setText(str(executor.get("address", "")))
|
|
|
|
def on_sync():
|
|
url = url_edit.text().strip()
|
|
if not url:
|
|
self.show_error(_("URL is required"))
|
|
return
|
|
tmp = {}
|
|
sync_btn.setEnabled(False)
|
|
loading_label.setText("\u27F3")
|
|
d._sync_active = True
|
|
|
|
def task():
|
|
return Willexecutors.get_info_task(url, tmp)
|
|
|
|
def on_success(result):
|
|
if not getattr(d, "_sync_active", False):
|
|
return
|
|
if result.get("status") == 200:
|
|
info_edit.setText(str(result.get("info", "")))
|
|
base_fee_spin.setValue(int(result.get("base_fee", 0)))
|
|
address_edit.setText(str(result.get("address", "")))
|
|
if edit_key and edit_key in self.willexecutors_list:
|
|
self.willexecutors_list[edit_key].update({
|
|
"status": 200,
|
|
"info": result.get("info", ""),
|
|
"base_fee": result.get("base_fee", 0),
|
|
"address": result.get("address", ""),
|
|
"last_update": result.get(
|
|
"last_update", datetime.now().timestamp()
|
|
),
|
|
})
|
|
self.will_executor_list_widget.update()
|
|
else:
|
|
QMessageBox.warning(
|
|
d,
|
|
_("Error"),
|
|
_("Could not reach server at {}").format(url),
|
|
)
|
|
url_edit.setFocus()
|
|
url_edit.selectAll()
|
|
sync_btn.setEnabled(True)
|
|
loading_label.clear()
|
|
d._sync_active = False
|
|
|
|
def on_error(exc_info):
|
|
if not getattr(d, "_sync_active", False):
|
|
return
|
|
QMessageBox.warning(
|
|
d,
|
|
_("Error"),
|
|
_("Error contacting server: {}").format(
|
|
str(exc_info[1])
|
|
),
|
|
)
|
|
url_edit.setFocus()
|
|
url_edit.selectAll()
|
|
sync_btn.setEnabled(True)
|
|
loading_label.clear()
|
|
d._sync_active = False
|
|
|
|
def on_done():
|
|
pass
|
|
|
|
sync_thread = TaskThread(d)
|
|
sync_thread.add(
|
|
task, on_success=on_success, on_done=on_done, on_error=on_error
|
|
)
|
|
|
|
sync_btn.clicked.connect(on_sync)
|
|
|
|
if not edit_key:
|
|
add_another_btn = QPushButton(_("Add another"))
|
|
self._add_another = False
|
|
|
|
def add_another():
|
|
self._add_another = True
|
|
d.accept()
|
|
|
|
add_another_btn.clicked.connect(add_another)
|
|
else:
|
|
self._add_another = False
|
|
|
|
row = 0
|
|
grid.addWidget(QLabel(_("URL")), row, 0)
|
|
grid.addWidget(url_edit, row, 1)
|
|
grid.addWidget(sync_btn, row, 2)
|
|
grid.addWidget(loading_label, row, 3)
|
|
grid.addWidget(
|
|
HelpButton(_("Will executor server URL (e.g. http://192.168.1.100:8080)")),
|
|
row,
|
|
4,
|
|
)
|
|
|
|
row += 1
|
|
grid.addWidget(QLabel(_("Info")), row, 0)
|
|
grid.addWidget(info_edit, row, 1)
|
|
grid.addWidget(
|
|
HelpButton(_("A short description or name for this executor")),
|
|
row,
|
|
2,
|
|
)
|
|
|
|
row += 1
|
|
grid.addWidget(QLabel(_("Base Fee (sats)")), row, 0)
|
|
grid.addWidget(base_fee_spin, row, 1)
|
|
grid.addWidget(
|
|
HelpButton(_("Base fee in satoshis")),
|
|
row,
|
|
2,
|
|
)
|
|
|
|
row += 1
|
|
grid.addWidget(QLabel(_("Address")), row, 0)
|
|
grid.addWidget(address_edit, row, 1)
|
|
grid.addWidget(
|
|
HelpButton(_("Bitcoin address for fee payments (optional)")),
|
|
row,
|
|
2,
|
|
)
|
|
|
|
vbox.addLayout(grid)
|
|
buttons = [CancelButton(d), OkButton(d)]
|
|
if not edit_key:
|
|
buttons.append(add_another_btn)
|
|
vbox.addLayout(Buttons(*buttons))
|
|
|
|
while d.exec():
|
|
url = url_edit.text().strip()
|
|
if not url:
|
|
self.show_error(_("URL is required"))
|
|
continue
|
|
if edit_key:
|
|
old_url = edit_key
|
|
ex = self.willexecutors_list[old_url]
|
|
ex.update({
|
|
"info": info_edit.text().strip() or "New Will Executor",
|
|
"base_fee": base_fee_spin.value(),
|
|
"address": address_edit.text().strip(),
|
|
})
|
|
if url != old_url:
|
|
self.willexecutors_list[url] = ex
|
|
del self.willexecutors_list[old_url]
|
|
else:
|
|
self.willexecutors_list[url] = {
|
|
"info": info_edit.text().strip() or "New Will Executor",
|
|
"base_fee": base_fee_spin.value(),
|
|
"address": address_edit.text().strip(),
|
|
"selected": False,
|
|
"status": "-1",
|
|
}
|
|
self.will_executor_list_widget.update()
|
|
Willexecutors.save(self.bal_window.bal_plugin, self.willexecutors_list)
|
|
if not self._add_another:
|
|
break
|
|
self._add_another = False
|
|
url_edit.clear()
|
|
info_edit.setText("New Will Executor")
|
|
base_fee_spin.setValue(0)
|
|
address_edit.clear()
|
|
|
|
def download_list(self, wes=None):
|
|
# Both this button and the wizard go through the same code path on
|
|
# BalWindow, which shows a "Downloading..." dialog (non-blocking GUI),
|
|
# tries the configured + fallback servers, logs the technical details
|
|
# and shows a simple message on failure.
|
|
def on_success(result):
|
|
self.willexecutors_list.update(result)
|
|
self.will_executor_list_widget.update()
|
|
Willexecutors.save(self.bal_window.bal_plugin, self.willexecutors_list)
|
|
self.update()
|
|
|
|
self.bal_window.download_list(self.bal_window.willexecutors, on_success)
|
|
|
|
def export_file(self, path):
|
|
export_meta_gui(
|
|
self.bal_window.window, "willexecutors.json", self.export_json_file
|
|
)
|
|
|
|
def export_json_file(self, path):
|
|
write_json_file(path, self.willexecutors_list)
|
|
|
|
def import_file(self):
|
|
import_meta_gui(
|
|
self.bal_window.window,
|
|
_("willexecutors"),
|
|
self.import_json_file,
|
|
self.willexecutors_list.update,
|
|
)
|
|
|
|
def update_willexecutors(self, wes=None):
|
|
if not wes:
|
|
wes = self.willexecutors_list
|
|
self.bal_window.ping_willexecutors(wes, self.save_willexecutors)
|
|
|
|
def import_json_file(self, path):
|
|
data = read_json_file(path)
|
|
data = self._validate(data)
|
|
self.willexecutors_list.update(data)
|
|
self.will_executor_list_widget.update()
|
|
|
|
# TODO validate willexecutor json import file
|
|
def _validate(self, data):
|
|
return data
|
|
|
|
def save_willexecutors(self, wes=None):
|
|
if not wes:
|
|
wes = self.willexecutors_list
|
|
self.willexecutors_list.update(wes)
|
|
self.will_executor_list_widget.update()
|
|
Willexecutors.save(self.bal_window.bal_plugin, self.willexecutors_list)
|
|
|
|
|