import os.path import time import traceback import sys import threading import platform import Queue from collections import namedtuple from functools import partial from electrum.i18n import _ from PyQt4.QtGui import * from PyQt4.QtCore import * if platform.system() == 'Windows': MONOSPACE_FONT = 'Lucida Console' elif platform.system() == 'Darwin': MONOSPACE_FONT = 'Monaco' else: MONOSPACE_FONT = 'monospace' 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;}" dialogs = [] from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED pr_icons = { PR_UNPAID:":icons/unpaid.png", PR_PAID:":icons/confirmed.png", PR_EXPIRED:":icons/expired.png" } pr_tooltips = { PR_UNPAID:_('Pending'), PR_PAID:_('Paid'), PR_EXPIRED:_('Expired') } expiration_values = [ (_('1 hour'), 60*60), (_('1 day'), 24*60*60), (_('1 week'), 7*24*60*60), (_('Never'), None) ] def clean_text(seed_e): text = unicode(seed_e.toPlainText()).strip() text = ' '.join(text.split()) return text class Timer(QThread): stopped = False def run(self): while not self.stopped: self.emit(SIGNAL('timersignal')) time.sleep(0.5) def stop(self): self.stopped = True self.wait() 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): def __init__(self, text, task, on_success=None, on_error=None): QPushButton.__init__(self, text) self.task = task self.on_success = on_success self.on_error = on_error self.clicked.connect(self.run_task) def run_task(self): self.setEnabled(False) self.thread = TaskThread(self) self.thread.add(self.task, self.on_success, self.done, self.on_error) def done(self): self.setEnabled(True) self.thread.stop() class WWLabel(QLabel): def __init__ (self, text="", parent=None): QLabel.__init__(self, text, parent) self.setWordWrap(True) class HelpLabel(QLabel): def __init__(self, text, help_text): QLabel.__init__(self, text) self.help_text = help_text self.app = QCoreApplication.instance() self.font = QFont() def mouseReleaseEvent(self, x): QMessageBox.information(self, 'Help', self.help_text, 'OK') def enterEvent(self, event): self.font.setUnderline(True) self.setFont(self.font) self.app.setOverrideCursor(QCursor(Qt.PointingHandCursor)) return QLabel.enterEvent(self, event) def leaveEvent(self, event): self.font.setUnderline(False) self.setFont(self.font) self.app.setOverrideCursor(QCursor(Qt.ArrowCursor)) return QLabel.leaveEvent(self, event) class HelpButton(QPushButton): def __init__(self, text): QPushButton.__init__(self, '?') self.help_text = text self.setFocusPolicy(Qt.NoFocus) self.setFixedWidth(20) self.clicked.connect(self.onclick) def onclick(self): QMessageBox.information(self, 'Help', self.help_text, 'OK') 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): def __init__(self, text_getter, app): QPushButton.__init__(self, _("Copy")) self.clicked.connect(lambda: app.clipboard().setText(text_getter())) 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) 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) class MessageBoxMixin(object): def top_level_window_recurse(self, window=None): window = window or self classes = (WindowModalDialog, QMessageBox) for n, child in enumerate(window.children()): # Test for visibility as old closed dialogs may not be GC-ed if isinstance(child, classes) and child.isVisible(): return self.top_level_window_recurse(child) return window def top_level_window(self): return self.top_level_window_recurse() def question(self, msg, parent=None, title=None, icon=None): Yes, No = QMessageBox.Yes, QMessageBox.No return self.msg_box(icon or QMessageBox.Question, parent, title or '', msg, buttons=Yes|No, defaultButton=No) == Yes def show_warning(self, msg, parent=None, title=None): return self.msg_box(QMessageBox.Warning, parent, title or _('Warning'), msg) def show_error(self, msg, parent=None): return self.msg_box(QMessageBox.Warning, parent, _('Error'), msg) def show_critical(self, msg, parent=None, title=None): return self.msg_box(QMessageBox.Critical, parent, title or _('Critical Error'), msg) def show_message(self, msg, parent=None, title=None): return self.msg_box(QMessageBox.Information, parent, title or _('Information'), msg) def msg_box(self, icon, parent, title, text, buttons=QMessageBox.Ok, defaultButton=QMessageBox.NoButton): parent = parent or self.top_level_window() d = QMessageBox(icon, title, text, buttons, parent) d.setWindowModality(Qt.WindowModal) d.setDefaultButton(defaultButton) return d.exec_() 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(WindowModalDialog): '''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_success=None, on_error=None): assert parent if isinstance(parent, MessageBoxMixin): parent = parent.top_level_window() WindowModalDialog.__init__(self, parent, _("Please wait")) vbox = QVBoxLayout(self) vbox.addWidget(QLabel(message)) self.accepted.connect(self.on_accepted) self.show() self.thread = TaskThread(self) self.thread.add(task, on_success, self.accept, on_error) def wait(self): self.thread.wait() def on_accepted(self): self.thread.stop() def line_dialog(parent, title, label, ok_label, default=None): dialog = WindowModalDialog(parent, title) dialog.setMinimumWidth(500) l = QVBoxLayout() dialog.setLayout(l) l.addWidget(QLabel(label)) txt = QLineEdit() if default: txt.setText(default) l.addWidget(txt) l.addLayout(Buttons(CancelButton(dialog), OkButton(dialog, ok_label))) 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) 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, checked_index=0): vbox = QVBoxLayout() if len(msg) > 50: vbox.addWidget(WWLabel(msg)) 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==checked_index: 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() 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 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) 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 class ElectrumItemDelegate(QStyledItemDelegate): def createEditor(self, parent, option, index): return self.parent().createEditor(parent, option, index) class MyTreeWidget(QTreeWidget): def __init__(self, parent, create_menu, headers, stretch_column=None, editable_columns=None): QTreeWidget.__init__(self, parent) self.parent = parent self.config = self.parent.config self.stretch_column = stretch_column self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(create_menu) self.setUniformRowHeights(True) # 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.itemDoubleClicked.connect(self.on_doubleclick) 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() in [ Qt.Key_F2, Qt.Key_Return ] and self.editor is None: 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_doubleclick(self, item, column): if self.permit_edit(item, column): self.editItem(item, column) def on_activated(self, item, column): # on 'enter' we show the menu 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''' 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() def update(self): # Defer updates if editing if self.editor: self.pending_update = True else: self.on_update() def on_update(self): pass def get_leaves(self, root): child_count = root.childCount() if child_count == 0: yield root for i in range(child_count): item = root.child(i) for x in self.get_leaves(item): yield x def filter(self, p, columns): p = unicode(p).lower() for item in self.get_leaves(self.invisibleRootItem()): item.setHidden(all([unicode(item.text(column)).lower().find(p) == -1 for column in columns])) class ButtonsWidget(QWidget): def __init__(self): super(QWidget, self).__init__() self.buttons = [] 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)) 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 def addCopyButton(self, app): self.app = app f = lambda: self.app.clipboard().setText(str(self.text())) self.addButton(":icons/copy.png", f, _("Copy to clipboard")) class ButtonsLineEdit(QLineEdit, ButtonsWidget): def __init__(self, text=None): QLineEdit.__init__(self, text) self.buttons = [] 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 self.text = self.toPlainText self.buttons = [] def resizeEvent(self, e): o = QPlainTextEdit.resizeEvent(self, e) self.resizeButtons() return o class TaskThread(QThread): '''Thread that runs background tasks. Callbacks are guaranteed to happen in the context of its parent.''' Task = namedtuple("Task", "task cb_success cb_done cb_error") doneSig = pyqtSignal(object, object, object) def __init__(self, parent, on_error=None): super(TaskThread, self).__init__(parent) self.on_error = on_error self.tasks = Queue.Queue() self.doneSig.connect(self.on_done) self.start() def add(self, task, on_success=None, on_done=None, on_error=None): on_error = on_error or self.on_error self.tasks.put(TaskThread.Task(task, on_success, on_done, on_error)) def run(self): while True: task = self.tasks.get() if not task: break try: result = task.task() self.doneSig.emit(result, task.cb_done, task.cb_success) except BaseException: self.doneSig.emit(sys.exc_info(), task.cb_done, task.cb_error) def on_done(self, result, cb_done, cb): # This runs in the parent's thread. if cb_done: cb_done() if cb: cb(result) def stop(self): self.tasks.put(None) if __name__ == "__main__": app = QApplication([]) t = WaitingDialog(None, 'testing ...', lambda: [time.sleep(1)], lambda x: QMessageBox.information(None, 'done', "done", _('OK'))) t.start() app.exec_()