electrum-bitcoinprivate/gui/qt/util.py

552 lines
18 KiB
Python
Raw Normal View History

2013-06-01 02:06:51 -07:00
import os.path
2013-08-29 07:07:55 -07:00
import time
2014-06-21 12:38:42 -07:00
import traceback
import sys
import threading
2014-09-19 04:36:30 -07:00
import platform
from functools import partial
from electrum.i18n import _
from PyQt4.QtGui import *
from PyQt4.QtCore import *
2014-09-19 04:36:30 -07:00
if platform.system() == 'Windows':
MONOSPACE_FONT = 'Lucida Console'
elif platform.system() == 'Darwin':
MONOSPACE_FONT = 'Monaco'
else:
MONOSPACE_FONT = 'monospace'
2015-07-11 09:14:00 -07:00
GREEN_BG = "QWidget {background-color:#80ff80;}"
RED_BG = "QWidget {background-color:#ffcccc;}"
RED_FG = "QWidget {color:red;}"
BLUE_FG = "QWidget {color:blue;}"
BLACK_FG = "QWidget {color:black;}"
2015-07-11 09:14:00 -07:00
dialogs = []
2013-08-29 07:07:55 -07:00
class Timer(QThread):
stopped = False
2013-08-29 07:07:55 -07:00
def run(self):
while not self.stopped:
2013-08-29 07:07:55 -07:00
self.emit(SIGNAL('timersignal'))
time.sleep(0.5)
def stop(self):
self.stopped = True
self.wait()
2013-08-29 07:07:55 -07:00
class EnterButton(QPushButton):
def __init__(self, text, func):
QPushButton.__init__(self, text)
self.func = func
self.clicked.connect(func)
def keyPressEvent(self, e):
if e.key() == Qt.Key_Return:
apply(self.func,())
class ThreadedButton(QPushButton):
2015-04-21 02:00:40 -07:00
def __init__(self, text, func, on_success=None, before=None):
QPushButton.__init__(self, text)
2015-04-21 02:00:40 -07:00
self.before = before
self.run_task = func
self.on_success = on_success
self.clicked.connect(self.do_exec)
self.connect(self, SIGNAL('done'), self.done)
self.connect(self, SIGNAL('error'), self.on_error)
def done(self):
if self.on_success:
self.on_success()
self.setEnabled(True)
def on_error(self):
QMessageBox.information(None, _("Error"), self.error)
self.setEnabled(True)
2014-03-28 09:05:34 -07:00
def do_func(self):
self.setEnabled(False)
try:
self.result = self.run_task()
except BaseException as e:
2015-04-21 02:00:40 -07:00
traceback.print_exc(file=sys.stdout)
self.error = str(e.message)
self.emit(SIGNAL('error'))
return
self.emit(SIGNAL('done'))
def do_exec(self):
2015-04-21 02:00:40 -07:00
if self.before:
self.before()
t = threading.Thread(target=self.do_func)
t.setDaemon(True)
t.start()
2013-08-29 07:07:55 -07:00
class HelpLabel(QLabel):
def __init__(self, text, help_text):
QLabel.__init__(self, text)
self.help_text = help_text
self.app = QCoreApplication.instance()
2015-05-02 22:06:54 -07:00
self.font = QFont()
def mouseReleaseEvent(self, x):
QMessageBox.information(self, 'Help', self.help_text, 'OK')
def enterEvent(self, event):
2015-05-02 22:06:54 -07:00
self.font.setUnderline(True)
self.setFont(self.font)
self.app.setOverrideCursor(QCursor(Qt.PointingHandCursor))
return QLabel.enterEvent(self, event)
def leaveEvent(self, event):
2015-05-02 22:06:54 -07:00
self.font.setUnderline(False)
self.setFont(self.font)
self.app.setOverrideCursor(QCursor(Qt.ArrowCursor))
return QLabel.leaveEvent(self, event)
2013-08-29 07:07:55 -07:00
class HelpButton(QPushButton):
def __init__(self, text):
QPushButton.__init__(self, '?')
self.help_text = text
2013-08-29 07:07:55 -07:00
self.setFocusPolicy(Qt.NoFocus)
self.setFixedWidth(20)
2014-06-07 10:53:54 -07:00
self.clicked.connect(self.onclick)
2014-06-07 10:53:54 -07:00
def onclick(self):
2015-04-22 00:56:16 -07:00
QMessageBox.information(self, 'Help', self.help_text, 'OK')
2013-08-29 07:07:55 -07:00
2015-03-14 04:28:19 -07:00
class Buttons(QHBoxLayout):
def __init__(self, *buttons):
QHBoxLayout.__init__(self)
self.addStretch(1)
for b in buttons:
self.addWidget(b)
class CloseButton(QPushButton):
def __init__(self, dialog):
QPushButton.__init__(self, _("Close"))
self.clicked.connect(dialog.close)
self.setDefault(True)
class CopyButton(QPushButton):
2015-04-20 05:44:59 -07:00
def __init__(self, text_getter, app):
2015-03-14 04:28:19 -07:00
QPushButton.__init__(self, _("Copy"))
2015-04-20 05:44:59 -07:00
self.clicked.connect(lambda: app.clipboard().setText(text_getter()))
2015-03-14 04:28:19 -07:00
2015-07-19 01:54:45 -07:00
class CopyCloseButton(QPushButton):
def __init__(self, text_getter, app, dialog):
QPushButton.__init__(self, _("Copy and Close"))
self.clicked.connect(lambda: app.clipboard().setText(text_getter()))
self.clicked.connect(dialog.close)
self.setDefault(True)
2015-03-14 04:28:19 -07:00
class OkButton(QPushButton):
def __init__(self, dialog, label=None):
QPushButton.__init__(self, label or _("OK"))
self.clicked.connect(dialog.accept)
self.setDefault(True)
class CancelButton(QPushButton):
def __init__(self, dialog, label=None):
QPushButton.__init__(self, label or _("Cancel"))
self.clicked.connect(dialog.reject)
2013-06-01 02:06:51 -07:00
2016-01-02 18:18:20 -08:00
class MessageBoxMixin(object):
def top_level_window(self, window=None):
window = window or self
for n, child in enumerate(window.children()):
# Test for visibility as old closed dialogs may not be GC-ed
if isinstance(child, WindowModalDialog) and child.isVisible():
return self.top_level_window(child)
return window
def question(self, msg, parent=None, title=None, icon=None):
2015-12-23 01:31:36 -08:00
Yes, No = QMessageBox.Yes, QMessageBox.No
return self.msg_box(icon or QMessageBox.Question,
parent, title or '',
2015-12-23 03:05:09 -08:00
msg, buttons=Yes|No, defaultButton=No) == Yes
2015-12-23 01:31:36 -08:00
2015-12-22 22:10:15 -08:00
def show_warning(self, msg, parent=None, title=None):
return self.msg_box(QMessageBox.Warning, parent,
2015-12-23 03:05:09 -08:00
title or _('Warning'), msg)
2015-12-22 22:10:15 -08:00
def show_error(self, msg, parent=None):
return self.msg_box(QMessageBox.Warning, parent,
2015-12-23 03:05:09 -08:00
_('Error'), msg)
2015-12-22 22:10:15 -08:00
def show_critical(self, msg, parent=None, title=None):
return self.msg_box(QMessageBox.Critical, parent,
2015-12-23 03:05:09 -08:00
title or _('Critical Error'), msg)
2015-12-22 22:10:15 -08:00
def show_message(self, msg, parent=None, title=None):
return self.msg_box(QMessageBox.Information, parent,
2015-12-23 03:05:09 -08:00
title or _('Information'), msg)
def msg_box(self, icon, parent, title, text, buttons=QMessageBox.Ok,
2015-12-23 03:05:09 -08:00
defaultButton=QMessageBox.NoButton):
parent = parent or self.top_level_window()
2015-12-23 03:05:09 -08:00
d = QMessageBox(icon, title, text, buttons, parent)
d.setWindowModality(Qt.WindowModal)
d.setDefaultButton(defaultButton)
return d.exec_()
2015-12-22 22:10:15 -08:00
class WindowModalDialog(QDialog, MessageBoxMixin):
'''Handy wrapper; window modal dialogs are better for our multi-window
daemon model as other wallet windows can still be accessed.'''
def __init__(self, parent, title=None):
QDialog.__init__(self, parent)
self.setWindowModality(Qt.WindowModal)
if title:
self.setWindowTitle(title)
class WaitingDialog(QThread, MessageBoxMixin):
'''Shows a please wait dialog whilst runnning a task. It is not
necessary to maintain a reference to this dialog.'''
def __init__(self, parent, message, task, on_finished=None):
global dialogs
dialogs.append(self) # Prevent GC
QThread.__init__(self)
self.task = task
self.on_finished = on_finished
self.dialog = WindowModalDialog(parent, _("Please wait"))
vbox = QVBoxLayout(self.dialog)
vbox.addWidget(QLabel(message))
self.dialog.show()
self.dialog.connect(self, SIGNAL("finished()"), self.finished)
self.start()
def run(self):
try:
self.result = self.task()
self.error = None
except BaseException as e:
traceback.print_exc(file=sys.stdout)
self.error = str(e)
self.result = None
def finished(self):
global dialogs
dialogs.remove(self)
self.dialog.accept()
if self.error:
self.show_error(self.error, parent=self.dialog.parent())
if self.on_finished:
self.on_finished(self.result)
2014-07-11 10:29:18 -07:00
def line_dialog(parent, title, label, ok_label, default=None):
dialog = WindowModalDialog(parent, title)
2014-07-11 10:29:18 -07:00
dialog.setMinimumWidth(500)
l = QVBoxLayout()
dialog.setLayout(l)
l.addWidget(QLabel(label))
txt = QLineEdit()
if default:
txt.setText(default)
l.addWidget(txt)
2015-03-14 04:28:19 -07:00
l.addLayout(Buttons(CancelButton(dialog), OkButton(dialog, ok_label)))
2014-07-11 10:29:18 -07:00
if dialog.exec_():
return unicode(txt.text())
def text_dialog(parent, title, label, ok_label, default=None):
from qrtextedit import ScanQRTextEdit
dialog = WindowModalDialog(parent, title)
dialog.setMinimumWidth(500)
l = QVBoxLayout()
dialog.setLayout(l)
l.addWidget(QLabel(label))
txt = ScanQRTextEdit()
if default:
txt.setText(default)
l.addWidget(txt)
2015-03-14 04:28:19 -07:00
l.addLayout(Buttons(CancelButton(dialog), OkButton(dialog, ok_label)))
if dialog.exec_():
return unicode(txt.toPlainText())
class ChoicesLayout(object):
def __init__(self, msg, choices, on_clicked=None):
vbox = QVBoxLayout()
if len(msg) > 50:
label = QLabel(msg)
label.setWordWrap(True)
vbox.addWidget(label)
msg = ""
gb2 = QGroupBox(msg)
vbox.addWidget(gb2)
vbox2 = QVBoxLayout()
gb2.setLayout(vbox2)
self.group = group = QButtonGroup()
for i,c in enumerate(choices):
button = QRadioButton(gb2)
button.setText(c)
vbox2.addWidget(button)
group.addButton(button)
group.setId(button, i)
if i==0:
button.setChecked(True)
if on_clicked:
group.buttonClicked.connect(partial(on_clicked, self))
self.vbox = vbox
def layout(self):
return self.vbox
def selected_index(self):
return self.group.checkedId()
2014-05-05 00:58:29 -07:00
def address_field(addresses):
hbox = QHBoxLayout()
address_e = QLineEdit()
if addresses:
address_e.setText(addresses[0])
def func():
i = addresses.index(str(address_e.text())) + 1
i = i % len(addresses)
address_e.setText(addresses[i])
button = QPushButton(_('Address'))
button.clicked.connect(func)
hbox.addWidget(button)
hbox.addWidget(address_e)
return hbox, address_e
2014-05-05 02:31:04 -07:00
def filename_field(parent, config, defaultname, select_msg):
vbox = QVBoxLayout()
vbox.addWidget(QLabel(_("Format")))
gb = QGroupBox("format", parent)
b1 = QRadioButton(gb)
b1.setText(_("CSV"))
b1.setChecked(True)
b2 = QRadioButton(gb)
b2.setText(_("json"))
vbox.addWidget(b1)
vbox.addWidget(b2)
2014-05-05 02:31:04 -07:00
hbox = QHBoxLayout()
directory = config.get('io_dir', unicode(os.path.expanduser('~')))
path = os.path.join( directory, defaultname )
filename_e = QLineEdit()
filename_e.setText(path)
def func():
text = unicode(filename_e.text())
_filter = "*.csv" if text.endswith(".csv") else "*.json" if text.endswith(".json") else None
p = unicode( QFileDialog.getSaveFileName(None, select_msg, text, _filter))
if p:
filename_e.setText(p)
button = QPushButton(_('File'))
button.clicked.connect(func)
hbox.addWidget(button)
hbox.addWidget(filename_e)
vbox.addLayout(hbox)
def set_csv(v):
text = unicode(filename_e.text())
text = text.replace(".json",".csv") if v else text.replace(".csv",".json")
filename_e.setText(text)
b1.clicked.connect(lambda: set_csv(True))
b2.clicked.connect(lambda: set_csv(False))
return vbox, filename_e, b1
2014-05-05 00:58:29 -07:00
class ElectrumItemDelegate(QStyledItemDelegate):
def createEditor(self, parent, option, index):
return self.parent().createEditor(parent, option, index)
2014-05-05 00:58:29 -07:00
class MyTreeWidget(QTreeWidget):
2015-04-03 06:32:29 -07:00
def __init__(self, parent, create_menu, headers, stretch_column=None,
editable_columns=None):
QTreeWidget.__init__(self, parent)
2015-04-04 11:59:57 -07:00
self.parent = parent
self.stretch_column = stretch_column
self.setContextMenuPolicy(Qt.CustomContextMenu)
2015-04-04 11:59:57 -07:00
self.customContextMenuRequested.connect(create_menu)
self.setUniformRowHeights(True)
2015-04-03 06:32:29 -07:00
# extend the syntax for consistency
self.addChild = self.addTopLevelItem
self.insertChild = self.insertTopLevelItem
# Control which columns are editable
self.editor = None
self.pending_update = False
if editable_columns is None:
editable_columns = [stretch_column]
self.editable_columns = editable_columns
self.setItemDelegate(ElectrumItemDelegate(self))
self.itemActivated.connect(self.on_activated)
self.update_headers(headers)
def update_headers(self, headers):
self.setColumnCount(len(headers))
self.setHeaderLabels(headers)
self.header().setStretchLastSection(False)
for col in range(len(headers)):
sm = QHeaderView.Stretch if col == self.stretch_column else QHeaderView.ResizeToContents
self.header().setResizeMode(col, sm)
def editItem(self, item, column):
if column in self.editable_columns:
self.editing_itemcol = (item, column, unicode(item.text(column)))
# Calling setFlags causes on_changed events for some reason
item.setFlags(item.flags() | Qt.ItemIsEditable)
QTreeWidget.editItem(self, item, column)
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
def keyPressEvent(self, event):
if event.key() == Qt.Key_F2:
self.on_activated(self.currentItem(), self.currentColumn())
else:
QTreeWidget.keyPressEvent(self, event)
def permit_edit(self, item, column):
return (column in self.editable_columns
and self.on_permit_edit(item, column))
def on_permit_edit(self, item, column):
return True
def on_activated(self, item, column):
if self.permit_edit(item, column):
self.editItem(item, column)
else:
pt = self.visualItemRect(item).bottomLeft()
pt.setX(50)
self.emit(SIGNAL('customContextMenuRequested(const QPoint&)'), pt)
def createEditor(self, parent, option, index):
self.editor = QStyledItemDelegate.createEditor(self.itemDelegate(),
parent, option, index)
self.editor.connect(self.editor, SIGNAL("editingFinished()"),
self.editing_finished)
return self.editor
def editing_finished(self):
# Long-time QT bug - pressing Enter to finish editing signals
# editingFinished twice. If the item changed the sequence is
# Enter key: editingFinished, on_change, editingFinished
# Mouse: on_change, editingFinished
# This mess is the cleanest way to ensure we make the
# on_edited callback with the updated item
if self.editor:
(item, column, prior_text) = self.editing_itemcol
if self.editor.text() == prior_text:
self.editor = None # Unchanged - ignore any 2nd call
elif item.text(column) == prior_text:
pass # Buggy first call on Enter key, item not yet updated
else:
# What we want - the updated item
self.on_edited(*self.editing_itemcol)
self.editor = None
# Now do any pending updates
if self.editor is None and self.pending_update:
self.pending_update = False
self.on_update()
def on_edited(self, item, column, prior):
'''Called only when the text actually changes'''
2015-04-04 11:59:57 -07:00
key = str(item.data(0, Qt.UserRole).toString())
text = unicode(item.text(column))
self.parent.wallet.set_label(key, text)
self.parent.history_list.update()
self.parent.update_completions()
2014-05-24 13:06:43 -07:00
def update(self):
# Defer updates if editing
if self.editor:
self.pending_update = True
else:
self.on_update()
def on_update(self):
pass
2015-04-23 06:24:12 -07:00
def get_leaves(self, root):
2015-04-23 04:50:35 -07:00
child_count = root.childCount()
2015-04-23 06:24:12 -07:00
if child_count == 0:
yield root
2015-04-23 04:50:35 -07:00
for i in range(child_count):
item = root.child(i)
2015-04-23 06:24:12 -07:00
for x in self.get_leaves(item):
yield x
def filter(self, p, columns):
p = unicode(p).lower()
2015-04-23 06:24:12 -07:00
for item in self.get_leaves(self.invisibleRootItem()):
item.setHidden(all([unicode(item.text(column)).lower().find(p) == -1
for column in columns]))
2015-04-23 04:50:35 -07:00
2014-05-24 13:06:43 -07:00
class ButtonsWidget(QWidget):
def __init__(self):
super(QWidget, self).__init__()
self.buttons = []
2015-04-20 03:32:48 -07:00
def resizeButtons(self):
frameWidth = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth)
x = self.rect().right() - frameWidth
y = self.rect().bottom() - frameWidth
for button in self.buttons:
sz = button.sizeHint()
x -= sz.width()
button.move(x, y - sz.height())
def addButton(self, icon_name, on_click, tooltip):
button = QToolButton(self)
button.setIcon(QIcon(icon_name))
2015-04-20 05:15:18 -07:00
button.setStyleSheet("QToolButton { border: none; hover {border: 1px} pressed {border: 1px} padding: 0px; }")
button.setVisible(True)
button.setToolTip(tooltip)
button.clicked.connect(on_click)
self.buttons.append(button)
return button
2015-04-20 05:15:18 -07:00
def addCopyButton(self, app):
self.app = app
f = lambda: self.app.clipboard().setText(str(self.text()))
2015-04-24 20:20:34 -07:00
self.addButton(":icons/copy.png", f, _("Copy to Clipboard"))
2015-04-20 05:15:18 -07:00
class ButtonsLineEdit(QLineEdit, ButtonsWidget):
def __init__(self, text=None):
QLineEdit.__init__(self, text)
self.buttons = []
2015-04-20 03:32:48 -07:00
def resizeEvent(self, e):
o = QLineEdit.resizeEvent(self, e)
self.resizeButtons()
return o
class ButtonsTextEdit(QPlainTextEdit, ButtonsWidget):
def __init__(self, text=None):
QPlainTextEdit.__init__(self, text)
self.setText = self.setPlainText
2015-04-20 05:15:18 -07:00
self.text = self.toPlainText
self.buttons = []
2015-04-20 03:32:48 -07:00
def resizeEvent(self, e):
o = QPlainTextEdit.resizeEvent(self, e)
self.resizeButtons()
return o
2014-05-24 13:06:43 -07:00
if __name__ == "__main__":
app = QApplication([])
2014-05-25 20:40:04 -07:00
t = WaitingDialog(None, 'testing ...', lambda: [time.sleep(1)], lambda x: QMessageBox.information(None, 'done', "done", _('OK')))
t.start()
2014-05-24 13:06:43 -07:00
app.exec_()