diff --git a/electrum/gui/qt/balance_dialog.py b/electrum/gui/qt/balance_dialog.py new file mode 100644 index 000000000..023851a90 --- /dev/null +++ b/electrum/gui/qt/balance_dialog.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2013 ecdsa@github +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from typing import TYPE_CHECKING + +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QPixmap +from PyQt5.QtWidgets import (QVBoxLayout, QCheckBox, QHBoxLayout, QLineEdit, + QLabel, QCompleter, QDialog, QStyledItemDelegate, + QScrollArea, QWidget, QPushButton, QGridLayout, QToolButton) + +from PyQt5.QtCore import QRect, QEventLoop, Qt, pyqtSignal +from PyQt5.QtGui import QPalette, QPen, QPainter, QPixmap + + +from electrum.i18n import _ + +from .util import Buttons, CloseButton, WindowModalDialog, ColorScheme + + +# Todo: +# show lightning funds that are not usable +# pie chart mouse interactive, to prepare a swap + +COLOR_CONFIRMED = Qt.green +COLOR_UNCONFIRMED = Qt.red +COLOR_UNMATURED = Qt.magenta +COLOR_FROZEN = ColorScheme.BLUE.as_color(True) +COLOR_LIGHTNING = Qt.yellow + +class PieChartObject: + + def paintEvent(self, event): + bgcolor = self.palette().color(QPalette.Background) + pen = QPen(Qt.gray, 1, Qt.SolidLine) + qp = QPainter() + qp.begin(self) + qp.setPen(pen) + qp.setRenderHint(QPainter.Antialiasing) + qp.setBrush(Qt.gray) + total = sum([x[2] for x in self._list]) + if total == 0: + return + alpha = 0 + s = 0 + for name, color, amount in self._list: + delta = int(16 * 360 * amount/total) + qp.setBrush(color) + qp.drawPie(self.R, alpha, delta) + alpha += delta + qp.end() + +class PieChartWidget(QWidget, PieChartObject): + + def __init__(self, size, l): + QWidget.__init__(self) + self.size = size + self.R = QRect(0, 0, self.size, self.size) + self.setGeometry(self.R) + self.setMinimumWidth(self.size) + self.setMaximumWidth(self.size) + self.setMinimumHeight(self.size) + self.setMaximumHeight(self.size) + self._list = l # list[ (name, color, amount)] + self.update() + + def update_list(self, l): + self._list = l + self.update() + + +class BalanceToolButton(QToolButton, PieChartObject): + + def __init__(self): + QToolButton.__init__(self) + self.size = 18 + self._list = [] + self.R = QRect(6, 3, self.size, self.size) + + def update_list(self, l): + self._list = l + self.update() + + def setText(self, text): + # this is a hack + QToolButton.setText(self, ' ' + text) + + def paintEvent(self, event): + QToolButton.paintEvent(self, event) + PieChartObject.paintEvent(self, event) + + +class LegendWidget(QWidget): + size = 20 + + def __init__(self, color): + QWidget.__init__(self) + self.color = color + self.R = QRect(0, 0, self.size, int(self.size*0.75)) + self.setGeometry(self.R) + self.setMinimumWidth(self.size) + self.setMaximumWidth(self.size) + self.setMinimumHeight(self.size) + self.setMaximumHeight(self.size) + + def paintEvent(self, event): + bgcolor = self.palette().color(QPalette.Background) + pen = QPen(Qt.gray, 1, Qt.SolidLine) + qp = QPainter() + qp.begin(self) + qp.setPen(pen) + qp.setRenderHint(QPainter.Antialiasing) + qp.setBrush(self.color) + qp.drawRect(self.R) + qp.end() + + +class BalanceDialog(WindowModalDialog): + + def __init__(self, parent, wallet): + + WindowModalDialog.__init__(self, parent, _("Wallet Balance")) + self.wallet = wallet + self.config = parent.config + self.fx = parent.fx + + confirmed, unconfirmed, unmatured, frozen, lightning = self.wallet.get_balances_for_piechart() + + frozen_str = self.config.format_amount_and_units(frozen) + confirmed_str = self.config.format_amount_and_units(confirmed) + unconfirmed_str = self.config.format_amount_and_units(unconfirmed) + unmatured_str = self.config.format_amount_and_units(unmatured) + lightning_str = self.config.format_amount_and_units(lightning) + + frozen_fiat_str = self.fx.format_amount_and_units(frozen) if self.fx else '' + confirmed_fiat_str = self.fx.format_amount_and_units(confirmed) if self.fx else '' + unconfirmed_fiat_str = self.fx.format_amount_and_units(unconfirmed) if self.fx else '' + unmatured_fiat_str = self.fx.format_amount_and_units(unmatured) if self.fx else '' + lightning_fiat_str = self.fx.format_amount_and_units(lightning) if self.fx else '' + + piechart = PieChartWidget(120, [ + (_('Frozen'), COLOR_FROZEN, frozen), + (_('Unmatured'), COLOR_UNMATURED, unmatured), + (_('Unconfirmed'), COLOR_UNCONFIRMED, unconfirmed), + (_('Confirmed'), COLOR_CONFIRMED, confirmed), + (_('Lightning'), COLOR_LIGHTNING, lightning), + ]) + + vbox = QVBoxLayout() + vbox.addWidget(piechart) + grid = QGridLayout() + #grid.addWidget(QLabel(_("Onchain") + ':'), 0, 1) + #grid.addWidget(QLabel(onchain_str), 0, 2, alignment=Qt.AlignRight) + #grid.addWidget(QLabel(onchain_fiat_str), 0, 3, alignment=Qt.AlignRight) + + if frozen: + grid.addWidget(LegendWidget(COLOR_FROZEN), 0, 0) + grid.addWidget(QLabel(_("Frozen") + ':'), 0, 1) + grid.addWidget(QLabel(frozen_str), 0, 2, alignment=Qt.AlignRight) + grid.addWidget(QLabel(frozen_fiat_str), 0, 3, alignment=Qt.AlignRight) + if unconfirmed: + grid.addWidget(LegendWidget(COLOR_UNCONFIRMED), 2, 0) + grid.addWidget(QLabel(_("Unconfirmed") + ':'), 2, 1) + grid.addWidget(QLabel(unconfirmed_str), 2, 2, alignment=Qt.AlignRight) + grid.addWidget(QLabel(unconfirmed_fiat_str), 2, 3, alignment=Qt.AlignRight) + if unmatured: + grid.addWidget(LegendWidget(COLOR_UNMATURED), 3, 0) + grid.addWidget(QLabel(_("Unmatured") + ':'), 3, 1) + grid.addWidget(QLabel(unmatured_str), 3, 2, alignment=Qt.AlignRight) + grid.addWidget(QLabel(unmatured_fiat_str), 3, 3, alignment=Qt.AlignRight) + if confirmed: + grid.addWidget(LegendWidget(COLOR_CONFIRMED), 1, 0) + grid.addWidget(QLabel(_("Confirmed") + ':'), 1, 1) + grid.addWidget(QLabel(confirmed_str), 1, 2, alignment=Qt.AlignRight) + grid.addWidget(QLabel(confirmed_fiat_str), 1, 3, alignment=Qt.AlignRight) + if lightning: + grid.addWidget(LegendWidget(COLOR_LIGHTNING), 4, 0) + grid.addWidget(QLabel(_("Lightning") + ':'), 4, 1) + grid.addWidget(QLabel(lightning_str), 4, 2, alignment=Qt.AlignRight) + grid.addWidget(QLabel(lightning_fiat_str), 4, 3, alignment=Qt.AlignRight) + + vbox.addLayout(grid) + vbox.addStretch(1) + btn_close = CloseButton(self) + btns = Buttons(btn_close) + vbox.addLayout(btns) + self.setLayout(vbox) + + def run(self): + self.exec_() diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 8a2492c3c..94c4bbd08 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -106,6 +106,7 @@ from .transaction_dialog import PreviewTxDialog from .rbf_dialog import BumpFeeDialog, DSCancelDialog from .qrreader import scan_qrcode from .swap_dialog import SwapDialog +from .balance_dialog import BalanceToolButton, COLOR_FROZEN, COLOR_UNMATURED, COLOR_UNCONFIRMED, COLOR_CONFIRMED, COLOR_LIGHTNING if TYPE_CHECKING: from . import ElectrumGui @@ -999,18 +1000,19 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): icon = read_QIcon("status_lagging%s.png"%fork_str) else: network_text = _("Connected") - c, u, x = self.wallet.get_balance() - balance_text = _("Balance") + ": %s "%(self.format_amount_and_units(c)) - if u: - balance_text += " [%s unconfirmed]"%(self.format_amount(u, is_diff=True).strip()) - if x: - balance_text += " [%s unmatured]"%(self.format_amount(x, is_diff=True).strip()) - if self.wallet.has_lightning(): - l = self.wallet.lnworker.get_balance() - balance_text += u' \U000026a1 %s'%(self.format_amount_and_units(l).strip()) + confirmed, unconfirmed, unmatured, frozen, lightning = self.wallet.get_balances_for_piechart() + self.balance_label.update_list([ + (_('Frozen'), COLOR_FROZEN, frozen), + (_('Unmatured'), COLOR_UNMATURED, unmatured), + (_('Unconfirmed'), COLOR_UNCONFIRMED, unconfirmed), + (_('Confirmed'), COLOR_CONFIRMED, confirmed), + (_('Lightning'), COLOR_LIGHTNING, lightning), + ]) + balance = confirmed + unconfirmed + unmatured + frozen + lightning + balance_text = _("Balance") + ": %s "%(self.format_amount_and_units(balance)) # append fiat balance and price if self.fx.is_enabled(): - balance_text += self.fx.get_fiat_status_text(c + u + x, + balance_text += self.fx.get_fiat_status_text(balance, self.base_unit(), self.get_decimal_point()) or '' if not self.network.proxy: icon = read_QIcon("status_connected%s.png"%fork_str) @@ -2410,16 +2412,23 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): console.updateNamespace(methods) - def create_status_bar(self): + def show_balance_dialog(self): + from .balance_dialog import BalanceDialog + d = BalanceDialog(self, self.wallet) + d.run() + def create_status_bar(self): sb = QStatusBar() sb.setFixedHeight(35) - - self.balance_label = QLabel("Loading wallet...") - self.balance_label.setTextInteractionFlags(Qt.TextSelectableByMouse) - self.balance_label.setStyleSheet("""QLabel { padding: 0 }""") + self.balance_label = BalanceToolButton() + self.balance_label.setText("Loading wallet...") + self.balance_label.setAutoRaise(True) + self.balance_label.clicked.connect(self.show_balance_dialog) sb.addWidget(self.balance_label) + # remove border of all items in status bar + self.setStyleSheet("QStatusBar::item { border: 0px;} ") + self.search_box = QLineEdit() self.search_box.textChanged.connect(self.do_search) self.search_box.hide() diff --git a/electrum/wallet.py b/electrum/wallet.py index b402bd1d7..a8d21121f 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -299,7 +299,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC): self.invoices = db.get_dict('invoices') # type: Dict[str, Invoice] self._reserved_addresses = set(db.get('reserved_addresses', [])) - self._freeze_lock = threading.Lock() # for mutating/iterating frozen_{addresses,coins} + self._freeze_lock = threading.RLock() # for mutating/iterating frozen_{addresses,coins} self._prepare_onchain_invoice_paid_detection() self.calc_unused_change_addresses() @@ -723,6 +723,24 @@ class Abstract_Wallet(AddressSynchronizer, ABC): ) return c1-c2, u1-u2, x1-x2 + def get_balances_for_piechart(self): + # return only positive values + # todo: add lightning frozen + c, u, x = self.get_balance() + fc, fu, fx = self.get_frozen_balance() + lightning = self.lnworker.get_balance() if self.has_lightning() else 0 + # subtract frozen funds + cc = c - fc + uu = u - fu + xx = x - fx + frozen = fc + fu + fx + # subtract unconfirmed if negative. + # (this does not make sense if positive and negative tx cancel eachother out) + if uu < 0: + cc = cc + uu + uu = 0 + return cc, uu, xx, frozen, lightning + def balance_at_timestamp(self, domain, target_timestamp): # we assume that get_history returns items ordered by block height # we also assume that block timestamps are monotonic (which is false...!)